"""ADR-0207 §5 step 2 / R4 — goal-residual reading (single-referent).

The first **residual-to-target** comprehension reading, distinct from GB-3b.1
accumulation (:mod:`generate.derivation.accumulate`):

- accumulation reads a **possession** that changes — ``start ± changes`` (``Sam has
  14 apples. He buys 9 more.`` -> ``14 + 9``);
- this reads a **goal** and the **progress** toward it — ``goal − Σprogress``
  (``Michael wants to lose 10 pounds. He lost 3, then 4. How much more to meet his
  goal?`` -> ``10 − 3 − 4``).

**Critical wrong=0 distinction — the coincidental-correctness trap.** Goal-residual is
*not* possession-accumulation. For a **loss** goal the two arithmetics coincide
(``10 − 3 − 4`` == ``10 − (3+4)``), so a naive "make accumulation fire" change would
pass ``cv-0005`` for the wrong reason — reading the goal ``10`` as a possession start.
For a **gain/save** goal they **diverge**: ``wants to save 20, saved 5, saved 6`` ->
residual ``20 − 5 − 6 = 9``, whereas possession-accumulation gives ``20 + 5 + 6 = 31``.
This production fires **only** on goal-language and **always subtracts** progress
(progress reduces the residual regardless of its world-polarity), so it reads the goal,
never the possession. The gain-goal divergence is the wrong=0 firewall test.

Lexeme-level (ADR-0165): goal-intent and residual-question cues are closed token sets,
not sentence-shape grammar. The constructed chain runs through the unchanged
self-verification gate (grounding ∧ unit ∧ completeness ∧ uniqueness) — refuse-
preferring; this only proposes a structurally-licensed candidate.

Sealed (no ``chat/`` import); deterministic.
"""

from __future__ import annotations

from typing import Final

from generate.derivation.clauses import segment_clauses
from generate.derivation.extract import extract_quantities
from generate.derivation.model import GroundedDerivation, Quantity, Step
from generate.derivation.state.bind import continues_anchor_referent, leading_subject_token
from generate.derivation.state.change import classify_change_polarity, select_change_cue
from generate.derivation.target import _question_clause
from generate.derivation.verify import Resolution, select_self_verified
from generate.math_roundtrip import _tokens

# Goal-intent lexemes: the anchor quantity is a *target*, not a possession. Closed set.
_GOAL_INTENT: Final[frozenset[str]] = frozenset(
    {"want", "wants", "wanted", "need", "needs", "hoping", "hopes", "plans", "aims", "goal"}
)
# Residual-question lexemes: the question asks the remaining distance to the goal.
_RESIDUAL_CUES: Final[frozenset[str]] = frozenset({"more", "left", "remaining"})
_GOAL_REACH: Final[frozenset[str]] = frozenset({"meet", "reach", "hit", "achieve"})
# Comparative questions may also contain "more", but their target is a difference
# between progress clauses, not distance-to-goal. Prefer refusal over promotion.
_COMPARATIVE_TARGET_CUES: Final[frozenset[str]] = frozenset({"than"})


def _asks_residual(question_clause: str) -> bool:
    """The question asks the remaining distance to a goal (lexeme-level)."""
    q = _tokens(question_clause)
    if _COMPARATIVE_TARGET_CUES & q:
        return False
    return bool(_RESIDUAL_CUES & q) or ("goal" in q and bool(_GOAL_REACH & q))


def build_goal_residual(problem_text: str) -> GroundedDerivation | None:
    """Construct the ungated goal-residual chain ``goal − Σprogress``, or ``None``.

    Fires only when: (a) the question asks a residual-to-goal; (b) the first
    quantity-clause carries a goal-intent lexeme and establishes exactly one goal
    quantity; (c) every later quantity-clause stays on the same referent and carries a
    licensed change cue (its quantities are *progress*). Progress is **subtracted**
    (it reduces the residual) regardless of the change's world-polarity — this is what
    makes the reading goal-residual, not possession-accumulation.
    """
    if not _asks_residual(_question_clause(problem_text)):
        return None

    clauses = segment_clauses(problem_text)
    quantity_clauses = [c for c in clauses if extract_quantities(c)]
    if len(quantity_clauses) < 2:
        return None

    anchor_clause, *progress_clauses = quantity_clauses
    if not (_GOAL_INTENT & _tokens(anchor_clause)):
        return None  # the anchor must be goal-language, not a possession
    anchor_quantities = extract_quantities(anchor_clause)
    if len(anchor_quantities) != 1:
        return None  # exactly one goal quantity
    goal = anchor_quantities[0]
    anchor_subject = leading_subject_token(anchor_clause)

    steps: list[Step] = []
    for clause in progress_clauses:
        if not continues_anchor_referent(clause, anchor_subject):
            return None  # new named actor -> referent hazard -> refuse
        polarity = classify_change_polarity(clause)
        if polarity is None:
            return None  # progress must carry a licensed change cue
        cue = select_change_cue(clause, polarity)
        progress = [q for q in extract_quantities(clause) if (not q.unit) or q.unit == goal.unit]
        if not progress:
            return None  # no same-unit progress quantity -> refuse
        for q in progress:
            operand = Quantity(value=q.value, unit=goal.unit, source_token=q.source_token)
            steps.append(Step(op="subtract", operand=operand, cue=cue))

    if not steps:
        return None
    return GroundedDerivation(start=goal, steps=tuple(steps))


def compose_goal_residual(problem_text: str) -> Resolution | None:
    """R4 goal-residual composer. Refuse-preferring: gates the chain through the
    unchanged self-verification gate (grounding ∧ unit ∧ completeness ∧ uniqueness)."""
    derivation = build_goal_residual(problem_text)
    if derivation is None:
        return None
    return select_self_verified([derivation], problem_text, target_units=())


def resolve_promotable_goal_residual(problem_text: str) -> Resolution | None:
    """Serving promotion bridge (ADR-0207 §5 step 2), parallel to
    :func:`generate.derivation.product_bridge.resolve_promotable_product`.

    Returns a serving-safe goal-residual resolution, or ``None``.  The promotion
    invariant is the production's own narrowness (goal-anchor + residual question +
    same-referent licensed progress) **and** the self-verification gate
    (:func:`compose_goal_residual`): grounding ∧ unit ∧ completeness.  Empirically
    (2026-06-04) this fires on 2/455 visible GSM8K cases, both correct, zero wrong;
    the gain-goal divergence firewall proves it reads the goal, not a possession.
    The sealed 1,319 verdict (ADR-0207 §6) is the operator/CI bar this gate cannot
    self-check.
    """
    return compose_goal_residual(problem_text)
