"""ProblemFrame skeleton — intermediate representation (IR) skeleton.

Tranche 1 — broad base-layer foundations.

Defines the target IR shape for future cognitive and math derivation organs.
Allows representing problem state and candidate structures without triggering
arithmetic solvers.
"""
from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING, Literal

if TYPE_CHECKING:
    from generate.kernel_facts import (
        BoundRelation,
        GroundedScalar,
        GroundedMention,
        GroundedUnit,
        MentionBinding,
        CandidateRelation,
        KernelHazard,
        KernelProvenance,
        SourceSpan,
    )
    from language_packs.scalar_equivalence import ScalarCandidate
    from generate.process_frames import ProcessFrame
    from generate.construction_affordances import ConstructionProposal


@dataclass(frozen=True, slots=True)
class QuestionTarget:
    """Declared question target of a problem."""
    surface: str
    target_type: str  # "quantity" | "count" | "comparison" | "unknown"
    unit: str | None = None

    def __post_init__(self) -> None:
        valid_types = {"quantity", "count", "comparison", "unknown"}
        if self.target_type not in valid_types:
            raise ValueError(
                f"QuestionTarget.target_type must be one of {sorted(valid_types)}, "
                f"got {self.target_type!r}"
            )


@dataclass(frozen=True, slots=True)
class BoundQuestionTarget:
    """Question target grounded to a mention, or explicitly unresolved."""

    target_type: str
    requested_surface: str
    target_mention_id: str | None
    unknown_slot: str
    evidence_spans: tuple[SourceSpan, ...]
    target_operator: str = "unknown"
    target_state: str = "unknown"
    target_direction: str = "unknown"

    def __post_init__(self) -> None:
        valid_types = {"count", "difference", "remaining", "total", "unknown"}
        valid_operators = {"count", "difference", "comparison", "unknown"}
        valid_states = {"aggregate", "current", "delta", "final", "initial", "unknown"}
        valid_directions = {"decrease", "forward", "inverse", "remaining", "unknown"}
        if self.target_type not in valid_types:
            raise ValueError(
                f"BoundQuestionTarget.target_type must be one of {sorted(valid_types)}, "
                f"got {self.target_type!r}"
            )
        if self.target_operator not in valid_operators:
            raise ValueError(
                "BoundQuestionTarget.target_operator must be one of "
                f"{sorted(valid_operators)}, got {self.target_operator!r}"
            )
        if self.target_state not in valid_states:
            raise ValueError(
                "BoundQuestionTarget.target_state must be one of "
                f"{sorted(valid_states)}, got {self.target_state!r}"
            )
        if self.target_direction not in valid_directions:
            raise ValueError(
                "BoundQuestionTarget.target_direction must be one of "
                f"{sorted(valid_directions)}, got {self.target_direction!r}"
            )
        if self.target_operator == "difference" and self.target_state != "delta":
            raise ValueError("difference targets must bind a delta target_state")
        if self.target_state == "delta" and self.target_operator != "difference":
            raise ValueError("delta targets must use the difference operator")
        if self.target_direction == "decrease" and self.target_operator != "difference":
            raise ValueError("decrease-directed targets must use the difference operator")

    @property
    def grounded(self) -> bool:
        return self.target_mention_id is not None


QuantityKind = Literal["count", "measurement"]


@dataclass(frozen=True, slots=True)
class QuantityKindDisposition:
    """Closed local count/measurement disposition for one quantity binding.

    This is family-local grounding evidence, not a general entity ontology.
    Count dispositions explicitly carry no unit; measurement dispositions
    require the exact grounded unit mention that licenses them.
    """

    quantity_mention_id: str
    entity_mention_id: str
    quantity_kind: QuantityKind
    unit_mention_id: str | None
    evidence_spans: tuple[SourceSpan, ...]

    def __post_init__(self) -> None:
        if self.quantity_kind not in {"count", "measurement"}:
            raise ValueError(
                "quantity_kind must be 'count' or 'measurement', "
                f"got {self.quantity_kind!r}"
            )
        if self.quantity_kind == "count" and self.unit_mention_id is not None:
            raise ValueError("count quantity dispositions must not carry a unit")
        if self.quantity_kind == "measurement" and self.unit_mention_id is None:
            raise ValueError("measurement quantity dispositions require a unit")
        if not self.evidence_spans:
            raise ValueError("quantity kind dispositions require exact evidence spans")


UnaryDeltaDirection = Literal["increase", "decrease"]
UnaryDeltaActionKind = Literal["gain", "loss"]


@dataclass(frozen=True, slots=True)
class GroundedUnaryDeltaCue:
    cue_id: str
    surface: str
    action_kind: UnaryDeltaActionKind
    direction: UnaryDeltaDirection
    span: SourceSpan

    def __post_init__(self) -> None:
        if self.surface not in {"gained", "lost"}:
            raise ValueError(f"invalid surface: {self.surface!r}")
        if self.surface == "gained":
            if self.action_kind != "gain" or self.direction != "increase":
                raise ValueError("invalid gained triple")
        elif self.surface == "lost":
            if self.action_kind != "loss" or self.direction != "decrease":
                raise ValueError("invalid lost triple")
        if self.surface != self.span.text:
            raise ValueError(
                f"surface {self.surface!r} must match span text {self.span.text!r}"
            )


@dataclass(frozen=True, slots=True)
class ProblemFrame:
    """Immutable target representation of a mathematical word problem.

    Accumulates facts, units, actors, objects, candidate relations,
    process frames, hazards, and question targets.
    """
    quantities: tuple[GroundedScalar, ...]
    scalars: tuple[ScalarCandidate, ...]
    units: tuple[GroundedUnit, ...]
    actors: tuple[str, ...]
    objects: tuple[str, ...]
    candidate_relations: tuple[CandidateRelation, ...]
    process_frames: tuple[ProcessFrame, ...]
    question_target: QuestionTarget | None
    hazards: tuple[KernelHazard, ...]
    provenance: tuple[KernelProvenance, ...]
    mentions: tuple[GroundedMention, ...] = ()
    bindings: tuple[MentionBinding, ...] = ()
    bound_relations: tuple[BoundRelation, ...] = ()
    bound_question_target: BoundQuestionTarget | None = None
    proposals: tuple[ConstructionProposal, ...] = ()
    quantity_kind_dispositions: tuple[QuantityKindDisposition, ...] = ()
    unary_delta_cues: tuple[GroundedUnaryDeltaCue, ...] = ()
    problem_text: str = ""


class ProblemFrameBuilder:
    """Mutable builder that produces an immutable ProblemFrame."""

    def __init__(self) -> None:
        self._quantities: list[GroundedScalar] = []
        self._scalars: list[ScalarCandidate] = []
        self._units: list[GroundedUnit] = []
        self._actors: list[str] = []
        self._objects: list[str] = []
        self._candidate_relations: list[CandidateRelation] = []
        self._process_frames: list[ProcessFrame] = []
        self._question_target: QuestionTarget | None = None
        self._hazards: list[KernelHazard] = []
        self._provenance: list[KernelProvenance] = []
        self._mentions: list[GroundedMention] = []
        self._bindings: list[MentionBinding] = []
        self._bound_relations: list[BoundRelation] = []
        self._bound_question_target: BoundQuestionTarget | None = None
        self._proposals: list[ConstructionProposal] = []
        self._quantity_kind_dispositions: list[QuantityKindDisposition] = []
        self._unary_delta_cues: list[GroundedUnaryDeltaCue] = []
        self._problem_text = ""

    def set_problem_text(self, problem_text: str) -> None:
        self._problem_text = problem_text

    def add_quantity(self, scalar: GroundedScalar) -> None:
        """Add a GroundedScalar to the frame, collecting hazards and provenance."""
        self._quantities.append(scalar)
        if scalar.provenance:
            self._provenance.append(scalar.provenance)
        for h in scalar.hazards:
            self._hazards.append(h)

    def add_scalar(self, scalar: ScalarCandidate) -> None:
        """Add a ScalarCandidate to the frame."""
        self._scalars.append(scalar)

    def add_unit(self, unit: GroundedUnit) -> None:
        """Add a GroundedUnit to the frame, collecting its provenance."""
        self._units.append(unit)
        if unit.provenance:
            self._provenance.append(unit.provenance)

    def add_actor(self, actor: str) -> None:
        """Add an actor to the frame."""
        self._actors.append(actor)

    def add_object(self, obj: str) -> None:
        """Add an object to the frame."""
        self._objects.append(obj)

    def add_relation(self, rel: CandidateRelation) -> None:
        """Add a CandidateRelation, collecting its hazards and provenance."""
        self._candidate_relations.append(rel)
        if rel.provenance:
            self._provenance.append(rel.provenance)
        for h in rel.hazards:
            self._hazards.append(h)

    def add_process_frame(self, frame: ProcessFrame) -> None:
        """Add a ProcessFrame to the frame."""
        self._process_frames.append(frame)

    def set_question_target(self, target: QuestionTarget) -> None:
        """Set the question target of the frame."""
        self._question_target = target

    def add_hazard(self, hazard: KernelHazard) -> None:
        """Add a hazard annotation directly to the frame."""
        self._hazards.append(hazard)

    def add_provenance(self, provenance: KernelProvenance) -> None:
        """Add a provenance record directly to the frame."""
        self._provenance.append(provenance)

    def add_mention(self, mention: GroundedMention) -> None:
        self._mentions.append(mention)

    def add_binding(self, binding: MentionBinding) -> None:
        self._bindings.append(binding)

    def add_bound_relation(self, relation: BoundRelation) -> None:
        self._bound_relations.append(relation)

    def set_bound_question_target(self, target: BoundQuestionTarget) -> None:
        self._bound_question_target = target

    def add_proposal(self, proposal: ConstructionProposal) -> None:
        self._proposals.append(proposal)

    def add_quantity_kind_disposition(
        self,
        disposition: QuantityKindDisposition,
    ) -> None:
        self._quantity_kind_dispositions.append(disposition)

    def add_unary_delta_cue(self, cue: GroundedUnaryDeltaCue) -> None:
        self._unary_delta_cues.append(cue)

    @property
    def unary_delta_cue_count(self) -> int:
        return len(self._unary_delta_cues)

    @property
    def unary_delta_cues(self) -> tuple[GroundedUnaryDeltaCue, ...]:
        return tuple(self._unary_delta_cues)

    def build(self) -> ProblemFrame:
        """Produce the immutable ProblemFrame."""
        return ProblemFrame(
            quantities=tuple(self._quantities),
            scalars=tuple(self._scalars),
            units=tuple(self._units),
            actors=tuple(self._actors),
            objects=tuple(self._objects),
            candidate_relations=tuple(self._candidate_relations),
            process_frames=tuple(self._process_frames),
            question_target=self._question_target,
            hazards=tuple(self._hazards),
            provenance=tuple(self._provenance),
            mentions=tuple(self._mentions),
            bindings=tuple(self._bindings),
            bound_relations=tuple(self._bound_relations),
            bound_question_target=self._bound_question_target,
            proposals=tuple(self._proposals),
            quantity_kind_dispositions=tuple(self._quantity_kind_dispositions),
            unary_delta_cues=tuple(self._unary_delta_cues),
            problem_text=self._problem_text,
        )
