"""The defined return values an answer can take.

A question that names a figure should come back as that figure, not as a
sentence containing it. These models are the shapes an answer is allowed to
have. The caller's `want` picks the shape in `expectation`. Under `auto`
the `fixed` agent picks the payload from the `Payload` union in the same call
that answers.

Every shape carries the ids it was read from. Table cells arrive tagged
(`<td id="t0-15-1">$2.97</td>`), so a scalar can name the exact cell its
value came from, and the value it reports can be checked against the text
printed in that cell — a grounding test no prose answer admits.
"""

from __future__ import annotations

from decimal import Decimal
from typing import List, Literal, Optional, Union

from pydantic import BaseModel, Field, computed_field

from quber.playground.answers.figures import as_number


class Scalar(BaseModel):
    """One figure, as the source prints it."""

    kind: Literal["scalar"] = "scalar"
    value: str = Field(
        description=(
            "The figure exactly as printed in the source, including its currency "
            "sign, separators, and any trailing sign: '$14.47', '4.6x', '92.6%', "
            "'(0.02)'. No words, no sentence, no explanation."
        )
    )

    unit: Optional[str] = Field(
        default=None,
        description="What the number counts: 'USD per share', 'USD thousands', 'percent', 'x'.",
    )
    period: Optional[str] = Field(
        default=None,
        description="The period or as-of date the figure belongs to, as printed: 'March 31, 2026'.",
    )
    label: Optional[str] = Field(
        default=None,
        description="The line item as printed in the source: 'Book Value Per Share of Common Stock'.",
    )
    source_id: Optional[str] = Field(
        default=None,
        description=(
            "The id of the single cell or chunk this figure was read from. Must "
            "appear verbatim in the context."
        ),
    )

    @computed_field  # type: ignore[prop-decorator]
    @property
    def number(self) -> Optional[Decimal]:
        """The figure as a plain number, sign applied and separators removed.

        Derived from `value` rather than asked of the model. The conversion is
        deterministic, and a model asked for it alongside the figure returns the
        figure every time and the number only most of the time: one question put
        four times came back with the figure four times and the number three.
        A numeric column with unpredictable holes is worse than one computed in
        a single place and tested there.
        """
        return as_number(self.value)


class Point(BaseModel):
    """One labelled figure inside a series."""

    label: str = Field(description="What this point is: a period, a segment, a category.")
    value: str = Field(description="The figure as printed.")
    source_id: Optional[str] = None

    @computed_field  # type: ignore[prop-decorator]
    @property
    def number(self) -> Optional[Decimal]:
        """The figure as a number, read from `value` on the same terms a scalar's is."""
        return as_number(self.value)


class Series(BaseModel):
    """One measure across several labels — periods, segments, buckets."""

    kind: Literal["series"] = "series"
    measure: str = Field(description="What is being measured across the points.")
    unit: Optional[str] = None
    points: List[Point] = Field(min_length=1)


class Grid(BaseModel):
    """A rectangle of values, when the question asks for a whole table."""

    kind: Literal["grid"] = "grid"
    columns: List[str]
    rows: List[List[str]] = Field(description="Each row holds one string per column, as printed.")
    unit: Optional[str] = None


class Prose(BaseModel):
    """Free text, for questions that ask for explanation rather than a figure."""

    kind: Literal["prose"] = "prose"
    text: str


class Unanswerable(BaseModel):
    """The context does not contain the answer.

    A distinct shape rather than an empty scalar: a caller in code must be able
    to tell 'not present' from 'zero' without reading English.
    """

    kind: Literal["unanswerable"] = "unanswerable"
    reason: str = Field(description="One sentence: what was missing from the context.")


Payload = Union[Scalar, Series, Grid, Prose, Unanswerable]


class Answer(BaseModel):
    """The single return value. Always this type; the payload varies."""

    payload: Payload = Field(discriminator="kind")
    cited_ids: List[str] = Field(
        default_factory=list,
        description="Chunk ids and/or table cell ids supporting the payload.",
    )
