"""Gate A2k — fraction-decrease delta (decrease-to fraction of base).

Experience Flywheel Sprint 8 / composition-validation **cv-0007** (train_sample
**0005**): forecast clause states a quantity will ``decrease to N/M of`` its
current value; a follow-on clause states the current amount; the question asks
the **decrease by** delta (not the final value).

    decrease_by = base × (1 − N/M)

Narrow organ — not a generic affine equation parser, not ``N/M more than`` affine
surfaces (0010-class), not percent-decrease.  Promotion requires:

- exactly one ``decrease to N/M of`` forecast (no ``more than`` fraction affine);
- question binds ``decrease`` + ``by`` (delta target, not final-value question);
- exactly one explicit current/base quantity with a unit;
- hazard refusal (percent, goal language, multiple decrease-to fractions).

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.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, SelfVerification, select_self_verified
from generate.math_roundtrip import _tokens

_DECREASE_TO_FRACTION_RE: Final[re.Pattern[str]] = re.compile(
    r"decrease\s+to\s+(\d+)\s*/\s*(\d+)\s+of",
    re.IGNORECASE,
)
_EXTRA_FRACTION_RE: Final[re.Pattern[str]] = re.compile(r"\d+\s*/\s*\d+")
_GOAL_INTENT: Final[frozenset[str]] = frozenset(
    {"want", "wants", "wanted", "need", "needs", "hoping", "hopes", "plans", "aims", "goal"}
)
_FINAL_VALUE_CUES: Final[frozenset[str]] = frozenset({"be", "will", "what"})


def _asks_decrease_delta(question_clause: str) -> bool:
    tokens = _tokens(question_clause)
    return "decrease" in tokens and "by" in tokens


def _asks_final_value(question_clause: str) -> bool:
    tokens = _tokens(question_clause)
    if "by" in tokens:
        return False
    if "decrease" not in tokens:
        return False
    return bool(_FINAL_VALUE_CUES & tokens) or "temperature" in tokens


def _target_fraction(problem_text: str) -> tuple[int, int, str] | None:
    matches = list(_DECREASE_TO_FRACTION_RE.finditer(problem_text))
    if len(matches) != 1:
        return None
    num_s, den_s = matches[0].group(1), matches[0].group(2)
    try:
        num = int(num_s)
        den = int(den_s)
    except ValueError:
        return None
    if num <= 0 or den <= 0 or num >= den:
        return None
    return num, den, f"{num_s}/{den_s}"


def _has_hazard_surface(problem_text: str, question_clause: str) -> bool:
    text_tokens = _tokens(problem_text)
    question_tokens = _tokens(question_clause)
    if "more" in text_tokens and "than" in text_tokens:
        return True
    if "%" in problem_text or "percent" in text_tokens or "percentage" in text_tokens:
        return True
    if text_tokens & _GOAL_INTENT:
        return True
    if _asks_final_value(question_clause):
        return True
    fractions = _EXTRA_FRACTION_RE.findall(problem_text)
    target = _target_fraction(problem_text)
    if target is None:
        return True
    _, _, token = target
    extra = [f for f in fractions if f.replace(" ", "") != token.replace(" ", "")]
    return len(extra) > 0


def _current_base_quantity(problem_text: str, question_clause: str) -> Quantity | None:
    for clause in segment_clauses(problem_text):
        if _DECREASE_TO_FRACTION_RE.search(clause):
            continue
        tokens = _tokens(clause)
        if "current" not in tokens and "now" not in tokens:
            continue
        quantities = [q for q in extract_quantities(clause) if q.unit]
        if len(quantities) == 1:
            return quantities[0]
    for clause in segment_clauses(problem_text):
        if clause == question_clause:
            continue
        if _DECREASE_TO_FRACTION_RE.search(clause):
            continue
        quantities = [q for q in extract_quantities(clause) if q.unit]
        if len(quantities) == 1:
            return quantities[0]
    return None


def build_fraction_decrease(problem_text: str) -> GroundedDerivation | None:
    """Construct ``base × (1 − N/M)`` decrease delta, or ``None``."""
    question_clause = _question_clause(problem_text)
    if not _asks_decrease_delta(question_clause):
        return None
    if _has_hazard_surface(problem_text, question_clause):
        return None

    fraction = _target_fraction(problem_text)
    base = _current_base_quantity(problem_text, question_clause)
    if fraction is None or base is None:
        return None

    num, den, source_token = fraction
    delta_factor = 1.0 - (num / den)
    if delta_factor <= 0:
        return None

    return GroundedDerivation(
        start=base,
        steps=(
            Step(
                op="multiply",
                operand=Quantity(
                    value=delta_factor,
                    unit="",
                    source_token=source_token,
                ),
                cue="decrease",
            ),
        ),
    )


def _self_verifies_fraction_decrease(
    derivation: GroundedDerivation, problem_text: str
) -> SelfVerification:
    from generate.derivation.verify import _base_reasons

    tokens = _tokens(problem_text)
    reasons = list(_base_reasons(derivation, tokens))

    fraction = _target_fraction(problem_text)
    if fraction is None:
        reasons.append("missing decrease-to fraction forecast")
    else:
        _, _, token = fraction
        if token not in problem_text.replace(" ", ""):
            reasons.append(f"fraction token {token!r} not grounded in text")

    base = _current_base_quantity(problem_text, _question_clause(problem_text))
    if base is None:
        reasons.append("missing explicit current/base quantity")

    return SelfVerification(verified=not reasons, reasons=tuple(reasons))


def compose_fraction_decrease(problem_text: str) -> Resolution | None:
    """Gate the typed fraction-decrease chain through self-verification."""
    derivation = build_fraction_decrease(problem_text)
    if derivation is None:
        return None
    if not _self_verifies_fraction_decrease(derivation, problem_text).verified:
        return None
    return Resolution(
        answer=derivation.answer,
        answer_unit=derivation.answer_unit,
        derivation=derivation,
    )


def resolve_promotable_fraction_decrease(problem_text: str) -> Resolution | None:
    """Serving promotion bridge (Gate A2k)."""
    return compose_fraction_decrease(problem_text)
