"""Live FORWARD PAPER TRACK for the 4-edge Bybit book — the OUT-OF-SAMPLE record.

From T0 forward, record the Bybit book's daily paper P&L on the `bybit_features` warehouse. This mirrors the
existing live paper-track machinery (`forward_tracker.ForwardTracker` + the `STRATEGY_REGISTRY` /
`forward_nav` cockpit path) — NOT a new persistence layer.

WHAT IS RECORDED: the NAIVE EQUAL-WEIGHT 4-sleeve Bybit book (tstrend / unlock / xsfunding / positioning) —
the honest deployable number. The live risk overlay over-levers on Bybit (separate re-tune, NOT this track),
so the forward curve here is `bybit_book_eval.risk_managed_book_from_store(...)["naive_equal_weight"]`'s curve,
re-derived day-by-day from the SAME per-sleeve return series (`sleeve_returns_from_store`).

WHY ForwardTracker (historically): it froze inception at T0 and booked ONLY days strictly after `last_date`
(idempotent re-runs, per-date NAV updated in place via the days-list state). Since Task 8c the track runs
through `forward_engine.run_track` (DB anchor + recompute) instead — `forward_nav`/`forward_summary` in the
cockpit DB are the SSOT the gate ETL (`forward_ingest.ForwardIngestService`) reads directly. The gate falls
out of the registry entry's `gate_spec` (gate_type="reconciliation", min_forward_days=14): WAIT ~2 weeks,
then PASS iff the forward reconciles with the backtest + clean execution — the blind 60-day calendar wait is
gone. So this module only supplies the strategy adapter + the naive eq-wt curve; T0, idempotency, the gate,
and cockpit visibility are all the existing, tested code path.

READ-ONLY against the warehouse: `sleeve_returns_from_store` never writes; the only persistence is the
engine's DB anchor + the cockpit forward_nav upsert (the same as every other live track).
"""
from __future__ import annotations

import datetime as _dt
from typing import Any, Protocol

from fxhnt.application.bybit_book_eval import _DEFAULT_BYBIT_SLEEVES, sleeve_returns_from_store

_EPOCH = _dt.date(1970, 1, 1)

# TAIL-BUDGETED static leverage shape (relative per-sleeve leverage), scaled to the book-gross cap. After the
# `effective[s] = shape[s]·MAX_BOOK_GROSS/Σshape` transform these give carry ≈40% of gross (0.60), non-carry
# inverse-vol (unlock 0.44, positioning 0.26, trend 0.20). Carry is the low-vol workhorse but its risk is a
# rare DISLOCATION, not variance; capping it at 40% bounds a -20% carry day to ≈-8% of the book (vs -10.5% at
# the Sharpe-max ~53% weight) while keeping Sharpe ~2.7 (>> naive 2.26). See the 2026-07-07 tail-budget spec.
_LEVERED_SLEEVE_SHAPE: dict[str, float] = {
    "crypto_tstrend": 1.0, "unlock": 2.2, "xsfunding": 3.0, "positioning": 1.3}
MAX_BOOK_GROSS = 1.5

# Positioning capturability haircuts (see docs/superpowers/specs/2026-07-07-positioning-capacity-aware-returns).
# K = the tail-concentration cap's z-multiple: a single coin's daily contribution is clipped at K·σ_book (the
# book's robust MAD daily scale) so no one microcap-squeeze day dominates. A documented 4σ robustness ceiling
# — the adaptivity is in σ_book, not K. The capacity capital is threaded from settings.paper_capital at the
# wiring layer (gains-only; barely binds at fund scale, caps thin names as capital grows).
POSITIONING_TAIL_CAP_K = 4.0
# Ex-ante per-coin gross-weight cap for the positioning sleeve — no single coin's |weight| may exceed this
# fraction of the book's unit gross. Validated 2026-07-20 over the full 1295-day history (Sharpe 2.60→2.69,
# maxDD −32.3%→−29.9%, per-year Sharpe non-decreasing every year); caps LABUSDT-class single-coin implosions
# (a 10.3% long leg that did −59%/−79.5% on 2026-07-06/07) without over-throttling (0.04 falsified → 2.43).
POSITIONING_COIN_GROSS_CAP = 0.07


class _FeatureStore(Protocol):
    """The panel reads the edge engines need (TimescaleFeatureStore satisfies this). READ-ONLY."""

    def crypto_close_panel(self, *, suffix: str = "USDT") -> dict[str, dict[int, float]]: ...
    def crypto_funding_panel(self, *, suffix: str = "USDT") -> dict[str, dict[int, float]]: ...
    def crypto_spot_panel(self, *, suffix: str = "USDT") -> dict[str, dict[int, float]]: ...
    def read_panel(self, symbols: list[str], feature: str) -> dict[str, dict[int, float]]: ...


def naive_eqwt_daily_returns(returns_by_sleeve: dict[str, dict[int, float]],
                             sleeves: list[str], *, cost_bps: float = 0.0) -> list[tuple[int, float]]:
    """The NAIVE EQUAL-WEIGHT book's per-day return series `[(epoch_day, ret), ...]` over the sleeves that
    have data, sorted by day. This is the per-day curve underlying `bybit_book_eval._equal_weight_metrics`
    (which returns only the SUMMARY) — extracted so the forward tracker can book one row per day. Static
    1/n weight over the full present set; an absent-today sleeve contributes 0 (its weight idles, not
    re-normalised), matching `_equal_weight_metrics` exactly. `cost_bps` charges turnover the same way."""
    present = [s for s in sleeves if returns_by_sleeve.get(s)]
    if not present:
        return []
    n = len(present)
    w = 1.0 / n
    all_days = sorted({d for s in present for d in returns_by_sleeve[s]})
    daily: list[tuple[int, float]] = []
    prev_w = {s: 0.0 for s in present}
    for d in all_days:
        today = [s for s in present if d in returns_by_sleeve[s]]
        if not today:
            continue
        gross = sum(w * returns_by_sleeve[s][d] for s in today)
        if cost_bps:
            cur_w = {s: (w if d in returns_by_sleeve[s] else 0.0) for s in present}
            turnover = sum(abs(cur_w[s] - prev_w[s]) for s in present)
            gross -= turnover * cost_bps / 1e4
            prev_w = cur_w
        daily.append((d, gross))
    return daily


def levered_eqwt_daily_returns(returns_by_sleeve: dict[str, dict[int, float]], sleeves: list[str],
                              shape: dict[str, float], *, max_gross: float = MAX_BOOK_GROSS,
                              cost_bps: float = 0.0) -> list[tuple[int, float]]:
    """The book's per-day return series `[(epoch_day, ret), ...]` under a STATIC per-sleeve leverage `shape`
    (relative leverage), scaled so the summed absolute weight (book gross) never exceeds `max_gross`. Mirrors
    `naive_eqwt_daily_returns` exactly (static weight over the present set, absent-today sleeve idles at 0,
    same turnover charge) — it IS the naive book when `shape` is uniform and `max_gross >= 1`."""
    present = [s for s in sleeves if returns_by_sleeve.get(s)]
    if not present:
        return []
    n = len(present)
    w = {s: shape.get(s, 1.0) / n for s in present}
    gross = sum(abs(v) for v in w.values())
    if gross > max_gross and gross > 0:
        scale = max_gross / gross
        w = {s: v * scale for s, v in w.items()}
    all_days = sorted({d for s in present for d in returns_by_sleeve[s]})
    daily: list[tuple[int, float]] = []
    prev_w = {s: 0.0 for s in present}
    for d in all_days:
        today = [s for s in present if d in returns_by_sleeve[s]]
        if not today:
            continue
        g = sum(w[s] * returns_by_sleeve[s][d] for s in today)
        if cost_bps:
            cur_w = {s: (w[s] if d in returns_by_sleeve[s] else 0.0) for s in present}
            turnover = sum(abs(cur_w[s] - prev_w[s]) for s in present)
            g -= turnover * cost_bps / 1e4
            prev_w = cur_w
        daily.append((d, g))
    return daily


class BybitFourEdgeStrategy:
    """`ForwardStrategy` adapter: the recomputable naive eq-wt 4-edge Bybit book over `bybit_features`.

    `advance(last_date, extra)` returns the FULL `(iso_date, ret)` series the naive book knows now; the
    `ForwardTracker` books only the rows strictly after `last_date` (so it is idempotent + only ever extends
    forward). External inputs (the unlock DefiLlama calendar, an optional stablecoin spot panel) are INJECTED
    so this stays pure + network-free in tests; on the cluster the asset wires the live loaders. A sleeve with
    no data simply contributes no returns (the book is whatever subset has data — the honest curve)."""

    def __init__(self, store: _FeatureStore, *, universe: set[str] | None = None,
                 sleeves: list[str] | None = None, cost_bps: float = 5.5,
                 unlock_events: list[dict[str, Any]] | None = None,
                 stablecoin_spot_panel: dict[str, dict[int, float]] | None = None,
                 leverage_shape: dict[str, float] | None = None,
                 max_gross: float = MAX_BOOK_GROSS,
                 positioning_capacity_capital: float | None = None,
                 positioning_tail_cap_k: float | None = None,
                 positioning_coin_gross_cap: float | None = None,
                 sleeve_returns: dict[str, dict[int, float]] | None = None) -> None:
        self._store = store
        self._universe = universe
        self._sleeves = list(sleeves) if sleeves is not None else list(_DEFAULT_BYBIT_SLEEVES)
        self._cost_bps = cost_bps
        self._unlock_events = unlock_events
        self._stablecoin_spot_panel = stablecoin_spot_panel
        self._leverage_shape = leverage_shape
        self._max_gross = max_gross
        # positioning-only capturability haircuts (gains-only capacity + tail-concentration cap); None = off.
        self._positioning_capacity_capital = positioning_capacity_capital
        self._positioning_tail_cap_k = positioning_tail_cap_k
        self._positioning_coin_gross_cap = positioning_coin_gross_cap
        # PRECOMPUTED per-sleeve series (compute-once): when supplied the store reads are SKIPPED and these are
        # used directly — the persist Job computes the 4 sleeves once and reuses them for both references.
        self._sleeve_returns = sleeve_returns

    def advance(self, last_date: str | None,
                extra: dict[str, Any]) -> tuple[list[tuple[str, float]], dict[str, Any]]:
        returns_by_sleeve: dict[str, dict[int, float]] = {}
        if self._sleeve_returns is not None:
            # reuse the precomputed series; apply the SAME >= 2-day filter as the store-read path.
            returns_by_sleeve = {s: v for s, v in self._sleeve_returns.items()
                                 if s in self._sleeves and len(v) >= 2}
        else:
            for sleeve in self._sleeves:
                series = sleeve_returns_from_store(
                    self._store, sleeve, universe=self._universe, cost_bps=self._cost_bps,
                    unlock_events=self._unlock_events, stablecoin_spot_panel=self._stablecoin_spot_panel,
                    capacity_capital=self._positioning_capacity_capital,
                    tail_cap_k=self._positioning_tail_cap_k,
                    coin_gross_cap=self._positioning_coin_gross_cap)
                if len(series) >= 2:   # need >= 2 days to be a real sleeve in the book
                    returns_by_sleeve[sleeve] = series

        active = [s for s in self._sleeves if s in returns_by_sleeve]
        if self._leverage_shape is None:
            daily = naive_eqwt_daily_returns(returns_by_sleeve, active, cost_bps=self._cost_bps)
        else:
            daily = levered_eqwt_daily_returns(returns_by_sleeve, active, self._leverage_shape,
                                               max_gross=self._max_gross, cost_bps=self._cost_bps)
        rows = [((_EPOCH + _dt.timedelta(days=d)).isoformat(), float(r)) for d, r in daily]
        return rows, extra


def bybit_report_sid_kwargs(strategy_id: str) -> dict:
    """The per-sid IDENTITY kwargs for the report-kind bybit book strategies passed to
    `bybit_4edge_backtest_summary` — the SINGLE source shared by the per-sid `report_backtest_summary`
    provider and the batch `_persist_bybit_book` writer, so a per-sid param can never drift between them.
    (Common kwargs — cost_bps, universe, and the positioning haircuts vs precomputed sleeve_returns — stay
    with each caller: they differ by build mode, batch vs per-sid.) `xsfunding`/`unlock` (Phase 0b: their own
    Bybit single-sleeve tracks, like positioning) are handled ONLY by `_persist_bybit_book` (via
    `_BYBIT_BOOK_SIDS`), not by `report_backtest_summary` — see that module's `_REPORT_SIDS`."""
    if strategy_id == "bybit_4edge":
        return {}
    if strategy_id == "bybit_4edge_levered":
        return {"leverage_shape": _LEVERED_SLEEVE_SHAPE, "strategy_id": "bybit_4edge_levered"}
    if strategy_id == "positioning":
        return {"sleeves": ["positioning"], "strategy_id": "positioning"}
    if strategy_id == "xsfunding":
        return {"sleeves": ["xsfunding"], "strategy_id": "xsfunding"}
    if strategy_id == "unlock":
        return {"sleeves": ["unlock"], "strategy_id": "unlock"}
    raise ValueError(f"bybit_report_sid_kwargs: {strategy_id!r} is not a report-kind bybit sid")


def bybit_4edge_backtest_summary(store: _FeatureStore, *, universe: set[str] | None = None,
                                 sleeves: list[str] | None = None, cost_bps: float = 5.5,
                                 unlock_events: list[dict[str, Any]] | None = None,
                                 stablecoin_spot_panel: dict[str, dict[int, float]] | None = None,
                                 leverage_shape: dict[str, float] | None = None,
                                 max_gross: float = MAX_BOOK_GROSS,
                                 strategy_id: str = "bybit_4edge",
                                 positioning_capacity_capital: float | None = None,
                                 positioning_tail_cap_k: float | None = None,
                                 positioning_coin_gross_cap: float | None = None,
                                 sleeve_returns: dict[str, dict[int, float]] | None = None):  # type: ignore[no-untyped-def]
    """Backtest reference for the bybit_4edge RECONCILIATION gate: the full-history metrics of the SAME naive
    eq-wt funding-net 4-edge book the forward track records (`BybitFourEdgeStrategy`). Persisted under
    strategy_id 'bybit_4edge' so the cockpit ref provider hands the gate an `expected_window_return` — without
    it the reconciliation band is silently skipped and the gate degrades to a bare min-days + clean-exec timer
    (a false-PASS risk on a real-capital gate). cagr/vol/sharpe/maxDD from the full series; the gauntlet
    verdict (dsr/is/oos/pvalue) from a 60/40 IS/OOS split, mirroring the equity-factor backtest_summary rows.

    `positioning_capacity_capital`/`positioning_tail_cap_k` apply the positioning capturability haircuts;
    `positioning_coin_gross_cap` applies the per-coin EX-ANTE weight cap (shapes the position, not a return
    haircut) — all three only take effect when this builds from the store, i.e. `sleeve_returns` is None; a
    precomputed `sleeve_returns` must already have them baked in — the persist Job computes the positioning
    series with them)."""
    rows, _ = BybitFourEdgeStrategy(
        store, universe=universe, sleeves=sleeves, cost_bps=cost_bps,
        unlock_events=unlock_events, stablecoin_spot_panel=stablecoin_spot_panel,
        leverage_shape=leverage_shape, max_gross=max_gross,
        positioning_capacity_capital=positioning_capacity_capital,
        positioning_tail_cap_k=positioning_tail_cap_k,
        positioning_coin_gross_cap=positioning_coin_gross_cap,
        sleeve_returns=sleeve_returns).advance(None, {})
    return backtest_summary_from_rows(strategy_id, rows)


def backtest_summary_from_rows(strategy_id: str, rows: list[tuple[str, float]]):  # type: ignore[no-untyped-def]
    """Build a `BacktestSummary` (full-history metrics) from a forward strategy's INCEPTION series
    `[(iso_date, ret), ...]` — i.e. the exact rows the track's own `ForwardStrategy.advance(None, {})` returns.
    Because it consumes the SAME series (at the SAME basis) the track's forward books, the reconciliation gate
    reconciles apples-to-apples. Shared by `bybit_4edge_backtest_summary` (the book) and the per-track ref
    persist for the individual sleeves (crypto_tstrend / unlock / stablecoin_rotation), whose forward
    constructions differ from the raw sleeve series — so their ref MUST come from their own strategy, not from
    `sleeve_returns_from_store`. cagr/vol/sharpe/maxDD from the full series; gauntlet verdict from a 60/40 split."""
    import numpy as np

    from fxhnt.application.forward_models import BacktestSummary
    from fxhnt.domain.backtest import compute_stats
    from fxhnt.domain.gauntlet.core import evaluate

    if len(rows) < 2:
        return BacktestSummary(strategy_id=strategy_id, as_of="", cagr=0.0, ann_vol=0.0, sharpe=0.0,
                               max_drawdown=0.0, passed=False, dsr=0.0, is_sharpe=0.0, oos_sharpe=0.0,
                               pvalue=1.0)
    rets = np.array([r for _, r in rows], dtype=float)
    stats = compute_stats(rets, periods_per_year=365)   # crypto trades every calendar day
    split = int(0.6 * len(rets))
    v = evaluate(rets[:split], rets[split:], n_trials=1, sr_variance=0.0, has_economic_rationale=True)
    return BacktestSummary(
        strategy_id=strategy_id, as_of=rows[-1][0],
        cagr=float(stats.cagr), ann_vol=float(stats.ann_vol), sharpe=float(stats.sharpe),
        max_drawdown=float(stats.max_drawdown), passed=bool(v.passed), dsr=float(v.dsr),
        is_sharpe=float(v.is_sharpe), oos_sharpe=float(v.oos_sharpe), pvalue=float(v.pvalue))
