"""The ETL seam: walk the strategy registry, read each track's DB record (the engine's `forward_summary` +
`forward_nav` rows — see `forward_engine.run_track`), compute the gate, and write the verdict back. The
*_state.json path is retired: the DB anchor + record are the SSOT. Best-effort per tracker — a track with
no DB summary yet is simply skipped, never aborting the batch (a single not-yet-written track must not
blind the whole cockpit).

GATE SELECTION: a tracker whose gate_spec is `gate_type="reconciliation"` uses the reconciliation gate
(WAIT ~2wk, then PASS iff the forward reconciles with the backtest + clean execution) — the blind 60-day
calendar wait is gone. It needs a BacktestReference for the strategy; that comes from an injected
`backtest_ref` provider (the cockpit's stored backtest CAGR by default), so this module stays pure and
network-free in tests. No reference (or a non-reconciliation gate_spec) → the legacy absolute gate."""
from __future__ import annotations

import datetime as dt
import logging
from typing import Any, Protocol

from fxhnt.adapters.persistence.forward_nav import ForwardNavRepo
from fxhnt.application.forward_models import ForwardNavRow, ForwardSummary, GateVerdict
from fxhnt.application.gate import evaluate_gate
from fxhnt.application.gate_policy import POLICY, POLICY_VERSION, policy_hash
from fxhnt.application.gate_policy_resolve import resolve_policy
from fxhnt.application.promotion_corr import corr_to_book, deploy_book_returns, passes_backstop
from fxhnt.application.reconciliation_gate import BacktestReference, evaluate_reconciliation_gate
from fxhnt.registry import STRATEGY_REGISTRY

_log = logging.getLogger(__name__)


class BacktestRefProvider(Protocol):
    """Supplies the backtest reference a reconciliation gate reconciles against. Returns None when the
    strategy has no stored backtest (the gate then degrades to the min-window + clean-exec checks only)."""

    def reference(self, strategy_id: str, forward_days: int) -> BacktestReference | None: ...


class CockpitBacktestRefProvider:
    """Default provider: pro-rate the cockpit's stored backtest CAGR to the forward window. No daily series is
    available from the summary row, so only the cumulative-band sub-check runs (correlation is skipped). Pure
    over the already-loaded summaries — no extra I/O on the request path."""

    # A forward track whose reconciliation reference is ANOTHER track's backtest (the executed multistrat
    # track reconciles against the MODELED multistrat's basis-matched reference — spec C1 §4: execution decay
    # shows as the executed track diverging from the modeled book's expectation).
    _REF_ALIAS = {"multistrat_exec": "multistrat", "bybit_4edge_exec": "bybit_4edge"}

    def __init__(self, repo: ForwardNavRepo) -> None:
        self._cagr = {b.strategy_id: float(b.cagr) for b in repo.all_backtest_summaries()}

    def reference(self, strategy_id: str, forward_days: int) -> BacktestReference | None:
        cagr = self._cagr.get(self._REF_ALIAS.get(strategy_id, strategy_id))
        if cagr is None:
            return None
        # expected cumulative fractional return over `forward_days` calendar days at the backtest CAGR.
        expected = (1.0 + cagr) ** (forward_days / 365.0) - 1.0
        return BacktestReference(expected_window_return=expected, daily_returns=None)


def _gate_verdict(summary: ForwardSummary, rows: list[ForwardNavRow], gate_spec: dict[str, Any],
                  backtest_ref: BacktestRefProvider | None, sid: str) -> GateVerdict:
    policy = resolve_policy(POLICY, gate_spec)
    if gate_spec.get("gate_type") == "reconciliation":
        ref = backtest_ref.reference(sid, max(summary.days, policy.min_forward_days)) if backtest_ref else None
        return evaluate_reconciliation_gate(summary, rows, ref, policy)
    return evaluate_gate(summary, policy)


class ForwardIngestService:
    def __init__(self, repo: ForwardNavRepo, backtest_ref: BacktestRefProvider | None = None) -> None:
        self._repo = repo
        # default to the cockpit's stored backtest CAGRs; injectable so tests stay pure + network-free.
        self._backtest_ref = backtest_ref or CockpitBacktestRefProvider(repo)

    def ingest(self, at: dt.datetime) -> int:
        """Compute + persist the gate verdict for every track whose record the engine has written to the DB.
        Reads the summary + nav rows the engine produced (forward_engine.run_track), evaluates the gate, and
        writes back the verdict. The *_state.json path is retired — the DB anchor + record are the SSOT."""
        count = 0
        summaries = {s.strategy_id: s for s in self._repo.all_summaries()}
        for sid, entry in STRATEGY_REGISTRY.items():
            row = summaries.get(sid)
            if row is None:
                continue  # the engine has not written this track yet (fresh/absent)
            try:
                summary = ForwardSummary(strategy_id=row.strategy_id, as_of=row.as_of, days=row.days,
                                         nav=row.nav, total_return=row.total_return, sharpe=row.sharpe,
                                         maxdd=row.maxdd)
                rows = self._repo.nav_history(sid)
                verdict = _gate_verdict(summary, rows, entry["gate_spec"], self._backtest_ref, sid)
                self._repo.upsert_summary(summary, verdict.status, verdict.reason, at,
                                          policy_version=POLICY_VERSION,
                                          policy_hash=policy_hash(POLICY, POLICY_VERSION))
                count += 1
            except Exception as e:  # noqa: BLE001 — one bad tracker must not abort the batch (freeze the gate)
                _log.warning("skipping tracker %s: %s", sid, e)
                continue
        return count


def ingest_forward_state(dsn: str) -> None:
    """Evaluate + persist the forward gate for every track the engine has recorded in the DB (SSOT).

    Reusable entry-point called by both the CLI command and the Dagster cockpit_forward asset.
    Idempotent: migrate() is a no-op if tables already exist.
    """
    repo = ForwardNavRepo(dsn)
    repo.migrate()
    n = ForwardIngestService(repo).ingest(at=dt.datetime.now(dt.UTC).replace(tzinfo=None))
    _log.info("ingest_forward_state: evaluated %d tracker(s) from the DB record", n)


def evaluate_promotions(dsn: str) -> list[str]:
    """ONE-WAY research→deploy promotion. For each registry strategy flagged `promote_on_pass` whose forward
    gate is now PASS and which is not already promoted, persist the promotion — gated by an AUTOMATIC
    correlation backstop: the candidate's daily-return correlation to the current deploy book (registry
    tier=="deploy" tracks + already-promoted tracks, minus the candidate itself) must be below
    `POLICY.promotion_max_corr`. An empty deploy book (first edge) trivially passes; too little overlap makes
    the correlation indeterminate and WITHHOLDS promotion (the safe, capital-adjacent error — never auto-promote
    on missing/short data). Runs AFTER `ingest_forward_state` (so the gate status just written is fresh).
    LIGHT: reads the summaries' gate_status + nav history + the registry flag, writes only genuinely-new
    promotions — no heavy recompute. The promotion is persistent (never reverts if the gate later dips), so a
    track becomes fund-tier deliberately once, not flickering with the daily gate. Returns the newly-promoted
    ids."""
    repo = ForwardNavRepo(dsn)
    repo.migrate()
    gate = {r.strategy_id: r.gate_status for r in repo.all_summaries()}
    already = repo.promoted_strategy_ids()
    # the current deploy book = registry deploy-tier tracks + already-promoted tracks (effective deploy set),
    # each as its {date: ret} series from A's record.
    deploy_sids = {sid for sid, m in STRATEGY_REGISTRY.items() if m.get("tier") == "deploy"} | already

    _series_cache: dict[str, dict[str, float]] = {}

    def _series(sid: str) -> dict[str, float]:
        if sid not in _series_cache:
            _series_cache[sid] = {r.date: r.ret for r in repo.nav_history(sid)}
        return _series_cache[sid]

    deploy_series = {s: _series(s) for s in deploy_sids}     # each deploy member's history read ONCE

    newly: list[str] = []
    for sid, meta in STRATEGY_REGISTRY.items():
        if not (meta.get("promote_on_pass") and sid not in already and gate.get(sid) == "PASS"):
            continue
        book_sids = deploy_sids - {sid}                      # never correlate a candidate against itself
        book = deploy_book_returns({s: deploy_series[s] for s in book_sids}) if book_sids else {}
        corr = corr_to_book(_series(sid), book, POLICY.promotion_min_overlap_days) if book else None
        if not passes_backstop(corr, book_empty=not book_sids, max_corr=POLICY.promotion_max_corr):
            _log.info("evaluate_promotions: %s gate=PASS but corr backstop failed (corr=%s, max=%.2f) "
                      "→ promotion withheld", sid, f"{corr:.2f}" if corr is not None else "n/a",
                      POLICY.promotion_max_corr)
            continue
        repo.promote(sid, dt.datetime.now(dt.UTC).replace(tzinfo=None))
        newly.append(sid)
        _log.info("evaluate_promotions: %s cleared gate + corr backstop → PROMOTED research→deploy", sid)
    return newly
