"""Walk-forward cross-sectional funding-dispersion backtest over the crypto_pit panel.
Daily rebalance, PIT universe, signed delta-neutral carry minus a liquidity-scaled cost.
Produces a daily net-return series per construction mode."""
from __future__ import annotations

from dataclasses import dataclass
from typing import Any

import numpy as np

from fxhnt.domain.backtest import compute_stats
from fxhnt.domain.cross_sectional_funding import (
    Panel, SpotPanel, construction_weights, eligible_asof, funding_score,
)
from fxhnt.domain.gauntlet.core import evaluate, per_period_sharpe

_PERIODS_PER_YEAR = 365   # crypto trades daily, all year

_MODES = ("long_tilt", "market_neutral", "executable")


@dataclass(frozen=True)
class FundingRunResult:
    returns_by_mode: dict[str, np.ndarray]
    n_names_avg: float
    panel_days: int        # full panel span (incl. days skipped for <4-coin cross-section)
    n_rebalances: int      # active rebalances actually booked (= len of each mode's series)
    dates: list[int]       # epoch-day each booked return is REALIZED on (`nxt`); shared across modes


class FundingBacktestRunner:
    def __init__(self, panel: Panel, *, spot: SpotPanel | None = None,
                 min_qvol: float = 1e6, min_history: int = 30,
                 lookback_days: int = 7, quantile: float = 0.2, cost_bps: float = 8.0,
                 slip_coef: float = 0.0005, slip_cap_bps: float = 50.0,
                 liq_ref: float = 1e6, borrowable_qvol: float = 2e7) -> None:
        """The `spot` panel should be pre-cleaned via `build_spot_panel` (perp-window +
        |basis| guarded); the runner defensively guards sp[d]>0 and sp[nxt]>0 but does not
        re-clean or assume a sanitized panel."""
        self._p = panel
        self._spot = spot
        self._minq, self._minh, self._lb, self._q = min_qvol, min_history, lookback_days, quantile
        self._cost = cost_bps / 1e4
        self._slip_coef, self._slip_cap = slip_coef, slip_cap_bps / 1e4
        self._liq_ref, self._borrow_q = liq_ref, borrowable_qvol

    def _coin_cost(self, qvol: float) -> float:
        """Round-trip cost: taker fee both legs + liquidity-scaled slippage (thin=more)."""
        slip = min(self._slip_cap, self._slip_coef * (self._liq_ref / qvol)) if qvol > 0 else self._slip_cap
        return self._cost + slip

    def _last_qvol(self, sym: str, day: int) -> float:
        """Most recent qvol on/before `day` for `sym` (held coins that dropped out still have history)."""
        series = self._p.get(sym, {})
        days = [x for x in series if x <= day]
        return series[max(days)][1] if days else 0.0

    def run(self) -> FundingRunResult:
        all_days = sorted({d for s in self._p.values() for d in s})
        series: dict[str, list[float]] = {m: [] for m in _MODES}
        prev_w: dict[str, dict[str, float]] = {m: {} for m in _MODES}
        dates: list[int] = []
        names_seen = 0
        n_rebals = 0
        for i in range(len(all_days) - 1):
            d, nxt = all_days[i], all_days[i + 1]
            elig = eligible_asof(self._p, d, min_qvol=self._minq, min_history=self._minh)
            if len(elig) < 4:                       # need a cross-section to rank
                continue
            scores = funding_score(self._p, elig, d, lookback_days=self._lb)
            borrowable = {s for s in elig if self._p[s].get(d, (0.0, 0.0, 0.0))[1] >= self._borrow_q}
            unit_ret: dict[str, float] = {}
            for s in elig:
                f = self._p[s][nxt][2] if nxt in self._p[s] else 0.0
                sp = self._spot.get(s) if self._spot is not None else None
                if (sp is not None and d in sp and nxt in sp and sp[d] > 0 and sp[nxt] > 0
                        and d in self._p[s] and nxt in self._p[s]
                        and self._p[s][d][0] > 0):
                    spot_ret = sp[nxt] / sp[d] - 1.0
                    perp_ret = self._p[s][nxt][0] / self._p[s][d][0] - 1.0
                    unit_ret[s] = (spot_ret - perp_ret) + f      # true delta-neutral cash-and-carry
                else:
                    unit_ret[s] = f                               # funding-only fallback
            held = set().union(*(prev_w[m] for m in _MODES)) if any(prev_w[m] for m in _MODES) else set()
            cost = {s: self._coin_cost(self._last_qvol(s, d)) for s in set(elig) | held}
            names_seen += len(elig)
            n_rebals += 1
            dates.append(int(nxt))           # return is realized on the next day
            for m in _MODES:
                w = construction_weights(scores, m, quantile=self._q, borrowable=borrowable)
                series[m].append(_book(w, prev_w[m], unit_ret, cost))
                prev_w[m] = w
        return FundingRunResult(
            returns_by_mode={m: np.asarray(series[m], dtype=float) for m in _MODES},
            n_names_avg=(names_seen / n_rebals) if n_rebals else 0.0,
            panel_days=len(all_days),
            n_rebalances=n_rebals,
            dates=dates,
        )


def _book(w: dict[str, float], prev_w: dict[str, float],
          unit_ret: dict[str, float], cost: dict[str, float]) -> float:
    """Carry the NEW book `w` earns over the next day (sum w_i * unit_ret_i, where unit_ret is the
    per-coin delta-neutral return: spot-perp basis + funding, or funding-only fallback) minus the
    round-trip rebalance cost charged on turnover between the prior book and the new one."""
    realized = sum(wt * unit_ret.get(s, 0.0) for s, wt in w.items())
    turnover = sum(abs(w.get(s, 0.0) - prev_w.get(s, 0.0)) * cost.get(s, 0.0)
                   for s in set(w) | set(prev_w)) / 2.0
    return realized - turnover


def _json_floats(d: dict[str, Any]) -> dict[str, Any]:
    """Coerce model_dump scalars to JSON-native: numpy → python float, NaN/inf → 0.0."""
    out: dict[str, Any] = {}
    for k, v in d.items():
        if isinstance(v, (np.floating, float)):
            fv = float(v)
            out[k] = fv if np.isfinite(fv) else 0.0
        elif isinstance(v, (np.integer,)):
            out[k] = int(v)
        else:
            out[k] = v
    return out


def evaluate_funding_matrix(panel: Panel, *, spot: SpotPanel | None = None,
                            floors: list[float], lookback_days: int = 7,
                            quantile: float = 0.2, cost_bps: float = 8.0, slip_coef: float = 0.0005,
                            oos_fraction: float = 0.40, dsr_min: float = 0.95,
                            oos_min_sharpe: float = 0.0, max_is_oos_decay: float = 0.50) -> dict[str, Any]:
    """Run the runner at each liquidity floor, gauntlet every (mode, floor) cell.
    n_trials = total cells tested (multiple-testing correction).

    Measures FUNDING-CARRY ONLY: each cell's return series is the signed delta-neutral
    funding carry (sum of w*funding minus rebalance cost). Spot-perp BASIS volatility and
    counterparty risk are NOT modelled, so stats.sharpe/cagr/maxDD describe a GROSS carry
    edge, not a net-of-basis deployable P&L (realistic Sharpe is ~2.5x lower with worse
    drawdowns). See the report's "caveats" key."""
    runs = {f: FundingBacktestRunner(panel, spot=spot, min_qvol=f, lookback_days=lookback_days,
                                     quantile=quantile, cost_bps=cost_bps, slip_coef=slip_coef).run()
            for f in floors}
    series_by_cell = {(m, f): runs[f].returns_by_mode[m] for f in floors for m in _MODES}
    n_trials = len(series_by_cell)
    is_sharpes = [per_period_sharpe(r[:int((1.0 - oos_fraction) * len(r))])
                  for r in series_by_cell.values() if len(r) > 3]
    sr_variance = float(np.var(is_sharpes)) if len(is_sharpes) > 1 else 0.0
    funding_only_caveat = (
        ("stats.sharpe/cagr/maxDD model the TRUE delta-neutral return (spot-perp BASIS P&L + funding "
         "minus cost) for coins with a spot leg; coins without spot fall back to funding-only. "
         "Counterparty risk is still NOT modelled. The gross-carry-only caveat no longer applies to basis-covered coins.")
        if spot is not None else
        ("stats.sharpe/cagr/maxDD measure FUNDING CARRY ONLY "
         "(sum of w*funding minus cost); spot-perp BASIS volatility and "
         "counterparty risk are NOT modelled. Realistic deployable Sharpe "
         "is ~2.5x lower with materially worse drawdowns. Treat as a GROSS "
         "carry edge, not a net-of-basis P&L."))
    out: dict[str, Any] = {"cells": {}, "n_trials": n_trials,
                           "basis_modeled": spot is not None,
                           "caveats": {
                               "funding_only": funding_only_caveat,
                               "sharpe_annualization": ("stats.sharpe uses 365/yr (crypto); verdict is_sharpe/"
                                                        "oos_sharpe use the gauntlet's 252/yr default. PASS/FAIL is "
                                                        "internally consistent; do not compare the two Sharpes numerically."),
                           },
                           "settings": {"floors": floors, "lookback_days": lookback_days,
                                        "quantile": quantile, "cost_bps": cost_bps,
                                        "slip_coef": slip_coef, "oos_fraction": oos_fraction}}
    for (m, f), r in series_by_cell.items():
        stats = compute_stats(r, periods_per_year=_PERIODS_PER_YEAR)
        key = f"{m}@{f:.0e}"
        if len(r) < 3:
            out["cells"][key] = {"mode": m, "floor": float(f), "stats": _json_floats(stats.model_dump()),
                                 "verdict": {"passed": False, "dsr": 0.0, "is_sharpe": 0.0,
                                             "oos_sharpe": 0.0, "n_trials": n_trials,
                                             "reasons": ["insufficient data"], "pvalue": 1.0}}
            continue
        split = int((1.0 - oos_fraction) * len(r))
        v = evaluate(r[:split], r[split:], n_trials=n_trials, sr_variance=sr_variance,
                     dsr_min=dsr_min, oos_min_sharpe=oos_min_sharpe, max_is_oos_decay=max_is_oos_decay,
                     has_economic_rationale=True)   # funding carry is a documented structural premium
        out["cells"][key] = {"mode": m, "floor": float(f),
                             "stats": _json_floats(stats.model_dump()),
                             "verdict": _json_floats(v.model_dump())}
    return out
