"""Gate A2g — duration segment total (comparative middle leg + fixed legs).

Experience Flywheel Sprint 6 selected this family after scout showed
``lift_refused_to_correct == 0`` (prior lifts already served) and the
``multiplicative_aggregate`` cluster lacked end-to-end sealed signal.  The
composition-validation corpus pins train_sample **0015** as R5
(``cv-0022``): fixed leg + comparative middle leg referencing the first
fixed leg + optional trailing fixed leg, question asks total time.

This is a first-principles organ — not broad DCS injection, not
``resolve_pooled``.  Promotion requires:

- question binds ``total`` + ``time``;
- exactly one comparative scalar (``twice``, ``double``, ``thrice``, …);
- two or three grounded duration quantities in the body;
- comparative cue appears between the first and last duration anchors;
- hazard refusal (fractions, percent, money, profit, remaining, per-day tariffs).

Deterministic; sealed module (no ``chat/`` import).
"""

from __future__ import annotations

import re
from typing import Final

from generate.derivation.clauses import segment_clauses
from generate.derivation.comparatives import comparative_step, extract_comparative_scalars
from generate.derivation.extract import extract_quantities
from generate.derivation.model import GroundedDerivation, Quantity, Step
from generate.derivation.target import _question_clause
from generate.derivation.verify import Resolution, select_self_verified
from generate.math_roundtrip import _tokens

_FRACTION_RE: Final[re.Pattern[str]] = re.compile(r"\d+/\d+")
_DURATION_UNITS: Final[frozenset[str]] = frozenset(
    {"hour", "hours", "hr", "hrs", "minute", "minutes", "min", "mins"}
)
_TEXT_BLOCKERS: Final[frozenset[str]] = frozenset(
    {
        "each",
        "insurance",
        "money",
        "overtime",
        "percent",
        "percentage",
        "profit",
        "rest",
        "salary",
        "wage",
    }
)
_REMAINING_DISTANCE_RE: Final[re.Pattern[str]] = re.compile(
    r"\bremaining\s+distance\b", re.IGNORECASE
)
_QUESTION_BLOCKERS: Final[frozenset[str]] = frozenset(
    {"longer", "more", "less", "per", "profit", "remaining"}
)


def _asks_total_time(question_clause: str) -> bool:
    tokens = _tokens(question_clause)
    return "total" in tokens and "time" in tokens


def _has_hazard_surface(problem_text: str, question_clause: str) -> bool:
    if _FRACTION_RE.search(problem_text):
        return True
    text_tokens = _tokens(problem_text)
    question_tokens = _tokens(question_clause)
    if "%" in problem_text or "percent" in text_tokens or "percentage" in text_tokens:
        return True
    if text_tokens & _TEXT_BLOCKERS:
        return True
    if "remaining" in text_tokens and not _REMAINING_DISTANCE_RE.search(problem_text):
        return True
    if question_tokens & _QUESTION_BLOCKERS:
        return True
    return False


def _duration_quantities(clause: str) -> tuple[Quantity, ...]:
    return tuple(
        q
        for q in extract_quantities(clause)
        if q.unit in _DURATION_UNITS
        or q.unit.rstrip("s") in {"hour", "hr", "minute", "min"}
    )


def _duration_family(unit: str) -> str | None:
    stem = unit.rstrip("s")
    if stem in {"hour", "hr"}:
        return "hour"
    if stem in {"minute", "min"}:
        return "minute"
    return None


def _same_duration_family(a: Quantity, b: Quantity) -> bool:
    """Refuse mixed duration units instead of adding hours and minutes directly."""
    fam_a = _duration_family(a.unit)
    fam_b = _duration_family(b.unit)
    return fam_a is not None and fam_a == fam_b


def _pick_add_cue(problem_text: str, *, prefer: tuple[str, ...]) -> str | None:
    tokens = _tokens(problem_text)
    for cue in prefer:
        if cue in tokens:
            return cue
    return None


def build_duration_segment_total(problem_text: str) -> GroundedDerivation | None:
    """Construct the ungated duration-segment-total chain, or ``None``.

    Shape (0015-class):
      leg1 + (leg1 × comparative_scalar) + leg3?

    Fold:
      start leg1 → × comparative → + leg1 → + leg3?
    """
    question_clause = _question_clause(problem_text)
    if not _asks_total_time(question_clause):
        return None
    if _has_hazard_surface(problem_text, question_clause):
        return None

    comparatives = extract_comparative_scalars(problem_text)
    if len(comparatives) != 1:
        return None
    comparative = comparatives[0]
    if comparative.scalar <= 0:
        return None

    body_clauses = [
        clause
        for clause in segment_clauses(problem_text)
        if clause != question_clause and comparative.cue in _tokens(clause)
    ]
    if len(body_clauses) != 1:
        return None

    durations = _duration_quantities(body_clauses[0])
    if len(durations) != 2:
        return None

    leg1, leg3 = durations
    if not _same_duration_family(leg1, leg3):
        return None

    add_back_cue = _pick_add_cue(problem_text, prefer=("subway", "bus", "ride", "and"))
    if add_back_cue is None:
        return None
    tail_cue = _pick_add_cue(problem_text, prefer=("then", "and"))
    if tail_cue is None:
        return None

    steps: list[Step] = [
        comparative_step(comparative),
        Step(
            op="add",
            operand=Quantity(value=leg1.value, unit=leg1.unit, source_token=leg1.source_token),
            cue=add_back_cue,
        ),
        Step(
            op="add",
            operand=Quantity(value=leg3.value, unit=leg3.unit, source_token=leg3.source_token),
            cue=tail_cue,
        ),
    ]

    return GroundedDerivation(start=leg1, steps=tuple(steps))


def compose_duration_segment_total(problem_text: str) -> Resolution | None:
    """Gate the typed duration chain through self-verification."""
    derivation = build_duration_segment_total(problem_text)
    if derivation is None:
        return None
    return select_self_verified([derivation], problem_text, target_units=())


def resolve_promotable_duration_segment_total(problem_text: str) -> Resolution | None:
    """Serving promotion bridge (Gate A2g)."""
    return compose_duration_segment_total(problem_text)
