"""Sub-project B3 — funding revocation on sustained decay. Pure, recompute-from-record: replay B1's
reconciliation gate as-of each forward record-day and step a hysteretic funding state machine from
inception. No persisted mutable state (mirrors A/B2a). Only DIVERGENCE de-funds; only PASS re-funds;
IMMATURE/EXECUTION_GAP are neutral. An edge's decay run advances only after its first PASS."""
from __future__ import annotations

from collections.abc import Callable, Sequence
from dataclasses import dataclass
from typing import Protocol

from fxhnt.application.forward_models import CAT_DIVERGENCE, CAT_PASS, ForwardSummary
from fxhnt.application.gate_policy_resolve import ResolvedPolicy
from fxhnt.application.reconciliation_gate import BacktestReference, evaluate_reconciliation_gate


class _Row(Protocol):
    date: str
    ret: float
    nav: float


@dataclass(frozen=True, slots=True)
class FundingStatus:
    state: str                    # "funded" | "de-funded" — the capital verdict
    label: str                    # "funded" | "decaying" | "de-funded" — display
    decay_run: int
    recovery_run: int
    first_pass_date: str | None


def compute_funding(sid: str, rows: Sequence[_Row],
                    ref_for_days: Callable[[int], BacktestReference | None],
                    resolved_policy: ResolvedPolicy,
                    defund_window: int, refund_window: int) -> FundingStatus:
    """Replay the funding state machine over `rows` (ascending). At each day, recompute the reconciliation
    gate as-of that day (as-of summary from the truncated record; backtest reference pro-rated to the day's
    forward length) and step the machine. Deterministic; no persisted state."""
    state = "funded"
    decay_run = recovery_run = 0
    has_passed = False
    first_pass_date: str | None = None

    for i in range(len(rows)):
        upto = rows[:i + 1]
        last = upto[-1]
        days = i + 1
        summary = ForwardSummary(strategy_id=sid, as_of=last.date, days=days, nav=last.nav,
                                 total_return=last.nav - 1.0, sharpe=0.0, maxdd=0.0)
        ref = ref_for_days(max(days, resolved_policy.min_forward_days))
        # The gate sees the FULL record (so the divergence band + correlation match production); only the
        # gap/continuity check uses a recent tail window (floored at 2 rows) so a RESOLVED historical gap ages
        # out instead of masking divergence forever. The cumulative divergence signal comes from `summary`.
        window = max(resolved_policy.min_forward_days + 1, 2)
        gap_rows = list(upto[-window:])
        category = evaluate_reconciliation_gate(summary, list(upto), ref, resolved_policy,
                                                gap_rows=gap_rows).category

        if category == CAT_PASS and not has_passed:
            has_passed = True
            first_pass_date = last.date

        if state == "funded":
            decay_run = decay_run + 1 if (category == CAT_DIVERGENCE and has_passed) else 0
            if decay_run >= defund_window:
                state, decay_run, recovery_run = "de-funded", 0, 0
        else:  # de-funded
            recovery_run = recovery_run + 1 if category == CAT_PASS else 0
            if recovery_run >= refund_window:
                state, decay_run, recovery_run = "funded", 0, 0

    label = "de-funded" if state == "de-funded" else ("decaying" if decay_run > 0 else "funded")
    return FundingStatus(state, label, decay_run, recovery_run, first_pass_date)
