"""Gate A2m — temporal tariff (overtime shift earnings + bundle overflow tariff).

Experience Flywheel Sprint 9 / microscope ``temporal_tariff:temporal_aggregation``
(train_sample **0001**, **0017**): two narrow tariff shapes, not a generic time
parser.

**Pattern A — overtime shift earnings (0001)**

    regular = threshold_hours × hourly_rate × days
    overtime_total = (hours_per_day − threshold_hours) × (rate + rate × ½) × days
    total = regular + overtime_total

**Pattern B — bundle overflow tariff (0017)**

    overflow_cost = (rental_days − bundle_days) × daily_rate
    total = bundle_price + overflow_cost

Promotion requires explicit positive anchors per pattern, question-target binding
(money/cost), and hazard refusal (profit, insurance, percent, multiple tariffs).

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

from __future__ import annotations

import re
from typing import Final

from collections import Counter

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 PRONOUNS, leading_subject_token
from generate.derivation.target import _question_clause
from generate.derivation.verify import Resolution, SelfVerification
from generate.math_roundtrip import _token_in, _tokens, _value_grounds

_FRACTION_RE: Final[re.Pattern[str]] = re.compile(r"\d+\s*/\s*\d+")
_HOURLY_RATE_RE: Final[re.Pattern[str]] = re.compile(
    r"\$\s*(\d+(?:\.\d+)?)\s*(?:an\s+hour|/hour|per\s+hour)",
    re.IGNORECASE,
)
_THRESHOLD_SHIFT_RE: Final[re.Pattern[str]] = re.compile(
    r"more\s+than\s+(\d+)\s+hours?\s+(?:a|an|each|per)\s+(?:day|shift|workday)",
    re.IGNORECASE,
)
_THRESHOLD_BAD_ANCHOR_RE: Final[re.Pattern[str]] = re.compile(
    r"more\s+than\s+\d+\s+hours?\s+(?:per|a|an|each|for)\s+(?:week|month|year|total)",
    re.IGNORECASE,
)
_RENT_VERBS: Final[frozenset[str]] = frozenset({"rent", "rents", "rented", "renting"})
_QUESTION_MONEY_SUBJECT_RE: Final[tuple[re.Pattern[str], ...]] = (
    re.compile(
        r"how\s+much(?:\s+money)?\s+does\s+(\w+)\s+(?:make|earn)",
        re.IGNORECASE,
    ),
    re.compile(r"how\s+much\s+will\s+it\s+cost\s+(\w+)", re.IGNORECASE),
    re.compile(r"what\s+does\s+it\s+cost\s+(\w+)", re.IGNORECASE),
    re.compile(r"how\s+much\s+does\s+(\w+)\s+pay", re.IGNORECASE),
    re.compile(r"what\s+is\s+the\s+total\s+cost(?:\s+for\s+(\w+))?", re.IGNORECASE),
)
_BUNDLE_TARIFF_RE: Final[re.Pattern[str]] = re.compile(
    r"\$\s*(\d+(?:\.\d+)?)\s+per\s+day\s+or\s+\$\s*(\d+(?:\.\d+)?)\s+for\s+(\d+)\s+days?",
    re.IGNORECASE,
)
_EARN_VERBS: Final[frozenset[str]] = frozenset(
    {"earn", "earned", "make", "made", "receive", "received", "get", "got"}
)
_COST_VERBS: Final[frozenset[str]] = frozenset({"cost", "costs", "charge", "charges", "pay", "paid"})
_TEXT_BLOCKERS: Final[frozenset[str]] = frozenset(
    {
        "insurance",
        "percent",
        "percentage",
        "profit",
        "ticket",
        "tickets",
        "vet",
        "amusement",
    }
)
_QUESTION_BLOCKERS: Final[frozenset[str]] = frozenset({"long", "take"})


def _asks_money_total(question_clause: str) -> bool:
    tokens = _tokens(question_clause)
    if "money" in tokens and bool(_EARN_VERBS & tokens):
        return True
    if "how" in tokens and "much" in tokens and bool((_COST_VERBS | _EARN_VERBS) & tokens):
        return True
    if "what" in tokens and bool({"cost", "costs", "charge", "charges", "price", "pay", "paid"} & tokens):
        return True
    return False


def _question_money_subject(question_clause: str) -> str | None:
    for pattern in _QUESTION_MONEY_SUBJECT_RE:
        match = pattern.search(question_clause)
        if match is None:
            continue
        subject = match.group(1)
        if subject is None:
            continue
        return subject.lower()
    return None


def _subjects_match(body_actor: str | None, asked_subject: str | None) -> bool:
    if body_actor is None or asked_subject is None:
        return False
    if asked_subject in PRONOUNS:
        return True
    return asked_subject == body_actor.lower()


def _overtime_worker(problem_text: str, question_clause: str) -> str | None:
    for clause in segment_clauses(problem_text):
        if clause == question_clause:
            continue
        if _HOURLY_RATE_RE.search(clause) or re.search(
            r"makes\s+\$\s*\d", clause, re.IGNORECASE
        ):
            subject = leading_subject_token(clause)
            if subject is not None:
                return subject.lower()
    for clause in segment_clauses(problem_text):
        if clause == question_clause:
            continue
        tokens = _tokens(clause)
        if "hours" not in tokens and "hour" not in tokens:
            continue
        if "every" not in tokens and "per" not in tokens and "each" not in tokens:
            continue
        subject = leading_subject_token(clause)
        if subject is not None and subject.lower() not in {"if", "when"}:
            return subject.lower()
    return None


def _bundle_renter(problem_text: str, question_clause: str) -> str | None:
    for clause in segment_clauses(problem_text):
        if clause == question_clause:
            continue
        tokens = _tokens(clause)
        if not (_RENT_VERBS & tokens):
            continue
        subject = leading_subject_token(clause)
        if subject is not None:
            return subject.lower()
    return None


def _question_target_matches_actor(
    problem_text: str, question_clause: str, *, pattern: str
) -> bool:
    asked = _question_money_subject(question_clause)
    if pattern == "overtime":
        body_actor = _overtime_worker(problem_text, question_clause)
    else:
        body_actor = _bundle_renter(problem_text, question_clause)
    if body_actor is None:
        return False
    if asked is None:
        tokens = _tokens(question_clause)
        return "what" in tokens and "total" in tokens and "cost" in tokens
    return _subjects_match(body_actor, asked)


def _body_text(problem_text: str) -> str:
    question_clause = _question_clause(problem_text)
    return problem_text.replace(question_clause, "").strip()


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


def _half_fraction_operand(problem_text: str) -> Quantity | None:
    match = re.search(r"1\s*/\s*2", problem_text)
    if match is None:
        return None
    return Quantity(value=0.5, unit="", source_token=match.group(0).replace(" ", ""))


def _hourly_rate(problem_text: str) -> Quantity | None:
    match = _HOURLY_RATE_RE.search(problem_text)
    if match is None:
        match = re.search(r"makes\s+\$\s*(\d+(?:\.\d+)?)", problem_text, re.IGNORECASE)
    if match is None:
        return None
    token = match.group(1)
    return Quantity(value=float(token), unit="dollars", source_token=token)


def _threshold_hours(problem_text: str) -> Quantity | None:
    if _THRESHOLD_BAD_ANCHOR_RE.search(problem_text):
        return None
    match = _THRESHOLD_SHIFT_RE.search(problem_text)
    if match is None:
        return None
    token = match.group(1)
    return Quantity(value=float(token), unit="hours", source_token=token)


def _hours_per_day(problem_text: str, question_clause: str) -> Quantity | None:
    threshold = _threshold_hours(problem_text)
    for clause in segment_clauses(problem_text):
        tokens = _tokens(clause)
        if "hours" not in tokens and "hour" not in tokens:
            continue
        if "more" in tokens and "than" in tokens:
            continue
        if "every" not in tokens and "per" not in tokens and "each" not in tokens:
            continue
        quantities = [
            q
            for q in extract_quantities(clause)
            if q.unit in {"hours", "hour"} or q.unit.rstrip("s") == "hour"
        ]
        if len(quantities) != 1:
            continue
        q = quantities[0]
        if threshold is not None and q.value <= threshold.value:
            continue
        return Quantity(value=q.value, unit="hours", source_token=q.source_token)
    return None


def _day_count(problem_text: str, question_clause: str) -> Quantity | None:
    for clause in segment_clauses(problem_text):
        tokens = _tokens(clause)
        if "days" not in tokens and "day" not in tokens:
            continue
        if clause == question_clause and "hours" not in tokens and "hour" not in tokens:
            continue
        quantities = [
            q
            for q in extract_quantities(clause)
            if q.unit in {"days", "day"} or q.unit.rstrip("s") == "day"
        ]
        if len(quantities) == 1:
            q = quantities[0]
            return Quantity(value=q.value, unit="days", source_token=q.source_token)
    return None


def _rental_days(problem_text: str, question_clause: str) -> Quantity | None:
    for clause in segment_clauses(problem_text):
        if clause == question_clause:
            continue
        tokens = _tokens(clause)
        if not (_RENT_VERBS & tokens):
            continue
        quantities = [
            q
            for q in extract_quantities(clause)
            if q.unit in {"days", "day"} or q.unit.rstrip("s") == "day"
        ]
        if len(quantities) == 1:
            q = quantities[0]
            return Quantity(value=q.value, unit="days", source_token=q.source_token)
    return None


def _bundle_tariff(problem_text: str) -> tuple[Quantity, Quantity, Quantity] | None:
    match = _BUNDLE_TARIFF_RE.search(problem_text)
    if match is None:
        return None
    daily_token, bundle_token, bundle_days_token = (
        match.group(1),
        match.group(2),
        match.group(3),
    )
    return (
        Quantity(value=float(daily_token), unit="dollars", source_token=daily_token),
        Quantity(value=float(bundle_token), unit="dollars", source_token=bundle_token),
        Quantity(value=float(bundle_days_token), unit="days", source_token=bundle_days_token),
    )


def build_overtime_shift_earnings(problem_text: str) -> GroundedDerivation | None:
    """Construct overtime shift earnings total, or ``None``."""
    question_clause = _question_clause(problem_text)
    if not _asks_money_total(question_clause):
        return None
    if not _question_target_matches_actor(problem_text, question_clause, pattern="overtime"):
        return None
    if _has_hazard_surface(problem_text, question_clause):
        return None
    if "overtime" not in _tokens(problem_text):
        return None

    rate = _hourly_rate(problem_text)
    threshold = _threshold_hours(problem_text)
    hours_per_day = _hours_per_day(problem_text, question_clause)
    days = _day_count(problem_text, question_clause)
    half = _half_fraction_operand(problem_text)
    if None in (rate, threshold, hours_per_day, days, half):
        return None
    if hours_per_day.value <= threshold.value:
        return None

    ot_hours_per_day = hours_per_day.value - threshold.value
    ot_rate = rate.value + (rate.value * half.value)
    ot_total = ot_hours_per_day * ot_rate * days.value

    return GroundedDerivation(
        start=threshold,
        steps=(
            Step(op="multiply", operand=rate, cue="hour"),
            Step(op="multiply", operand=days, cue="days"),
            Step(
                op="add",
                operand=Quantity(
                    value=ot_total,
                    unit="hours",
                    source_token=hours_per_day.source_token,
                ),
                cue="overtime",
            ),
        ),
    )


def build_bundle_overflow_tariff(problem_text: str) -> GroundedDerivation | None:
    """Construct bundle + overflow-day tariff total, or ``None``."""
    question_clause = _question_clause(problem_text)
    if not _asks_money_total(question_clause):
        return None
    if not _question_target_matches_actor(problem_text, question_clause, pattern="bundle"):
        return None
    if _has_hazard_surface(problem_text, question_clause):
        return None
    if "overtime" in _tokens(problem_text):
        return None

    tariff = _bundle_tariff(problem_text)
    rental_days = _rental_days(problem_text, question_clause)
    if tariff is None or rental_days is None:
        return None

    daily_rate, bundle_price, bundle_days = tariff
    if rental_days.value <= bundle_days.value:
        return None

    overflow_cost = (rental_days.value - bundle_days.value) * daily_rate.value

    return GroundedDerivation(
        start=bundle_price,
        steps=(
            Step(
                op="add",
                operand=Quantity(
                    value=overflow_cost,
                    unit="dollars",
                    source_token=rental_days.source_token,
                ),
                cue="per",
            ),
        ),
    )


def _self_verifies_temporal_tariff(
    derivation: GroundedDerivation, problem_text: str, *, pattern: str
) -> SelfVerification:
    reasons: list[str] = []
    tokens = _tokens(problem_text)
    question_clause = _question_clause(problem_text)

    operands = [derivation.start, *(s.operand for s in derivation.steps)]
    for q in operands:
        if not _value_grounds(q.source_token, tokens):
            reasons.append(f"operand {q.source_token!r} not grounded in text")

    for step in derivation.steps:
        if not _token_in(step.cue, tokens):
            reasons.append(f"operation cue {step.cue!r} not grounded in text")

    body = _body_text(problem_text)
    body_quantities = Counter(q.source_token for q in extract_quantities(body))
    used = Counter(
        [derivation.start.source_token]
        + [step.operand.source_token for step in derivation.steps]
    )
    if pattern == "overtime":
        half = _half_fraction_operand(problem_text)
        if half is not None:
            used[half.source_token] += 1
            if half.source_token == "1/2":
                used["2"] += 1
    elif pattern == "bundle":
        tariff = _bundle_tariff(problem_text)
        if tariff is not None:
            daily_rate, _, bundle_days = tariff
            used[daily_rate.source_token] += 1
            used[bundle_days.source_token] += 1
    unused = body_quantities - used
    if unused:
        reasons.append(f"incomplete: unused body quantities {sorted(unused.elements())}")

    if pattern == "overtime":
        if _hourly_rate(problem_text) is None:
            reasons.append("missing hourly rate anchor")
        if _threshold_hours(problem_text) is None:
            reasons.append("missing overtime threshold")
        if _hours_per_day(problem_text, question_clause) is None:
            reasons.append("missing hours-per-day anchor")
        if _day_count(problem_text, question_clause) is None:
            reasons.append("missing day-count anchor")
        if _half_fraction_operand(problem_text) is None:
            reasons.append("missing overtime half-fraction anchor")
    elif pattern == "bundle":
        if _bundle_tariff(problem_text) is None:
            reasons.append("missing bundle tariff anchor")
        if _rental_days(problem_text, question_clause) is None:
            reasons.append("missing rental-day question anchor")

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


def _compose_pattern(
    builder, problem_text: str, *, pattern: str
) -> Resolution | None:
    derivation = builder(problem_text)
    if derivation is None:
        return None
    if not _self_verifies_temporal_tariff(derivation, problem_text, pattern=pattern).verified:
        return None
    return Resolution(
        answer=derivation.answer,
        answer_unit=derivation.answer_unit,
        derivation=derivation,
    )


def compose_overtime_shift_earnings(problem_text: str) -> Resolution | None:
    return _compose_pattern(build_overtime_shift_earnings, problem_text, pattern="overtime")


def compose_bundle_overflow_tariff(problem_text: str) -> Resolution | None:
    return _compose_pattern(build_bundle_overflow_tariff, problem_text, pattern="bundle")


def compose_temporal_tariff(problem_text: str) -> Resolution | None:
    """Try overtime then bundle overflow; refuse on ambiguity."""
    overtime = compose_overtime_shift_earnings(problem_text)
    bundle = compose_bundle_overflow_tariff(problem_text)
    if overtime is not None and bundle is not None:
        return None
    return overtime or bundle


def resolve_promotable_temporal_tariff(problem_text: str) -> Resolution | None:
    """Serving promotion bridge (Gate A2m)."""
    return compose_temporal_tariff(problem_text)