"""Phase C — re-validate the cross-sectional CARRY edge on BYBIT data (the venue we'll actually trade).

This runs the SAME carry pipeline used for the legacy combined-book venue, but reads the Bybit-namespaced `bybit_features`
TimescaleDB table (funding + perp close + spot close, ingested by Phases A/B). It reuses, unchanged:

  * `XsFundingReplay` (warehouse_funding_panel(close, funding) → eligible/funding_score/construction_weights)
    for the per-day cross-sectional carry weights — the live xsfunding construction;
  * `carry_return_by_symbol` / `basis_change_by_symbol` for the per-symbol FULL daily carry return
    `carry_retᵢ = (spot_retᵢ − perp_retᵢ) + fundingᵢ` (funding accrual PLUS the realized delta-neutral
    basis), the single source of truth the paper book also books;
  * `sleeve_daily_carry`'s convention `Σ wᵢ·carry_retᵢ / Σ|wᵢ|` for the book's daily carry return.

The only I/O is the feature store passed in (Bybit or the legacy venue), so `carry_metrics_from_store` is PURE given
the store. This is a READ-ONLY report — no writes. It produces real numbers only once the Phase-A/B Bybit
ingests have populated `bybit_features`; before that the Bybit side is empty → verdict 'insufficient data'.
"""
from __future__ import annotations

import datetime as dt
import math
import statistics as st
from typing import Any, Protocol

from fxhnt.application.funding_panel import basis_change_by_symbol, warehouse_funding_panel
from fxhnt.application.paper_book import carry_return_by_symbol
from fxhnt.application.xsfunding_replay import XsFundingReplay

_PERIODS_PER_YEAR = 365


class _FeatureStore(Protocol):
    """Just the panel reads the carry pipeline needs (TimescaleFeatureStore satisfies this)."""

    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]]: ...


# --- TRUSTWORTHY-knob defaults (documented; mirror the legacy venue's carry treatment) ------------------
# Per-coin liquidity-scaled cost model: cost_bpsᵢ = clip(base + slip_coef·(ref_dollar_vol/median_dvᵢ),
# base, cap). A liquid coin (median_dv >= ref) pays ~base; a thinner coin pays more, capped. ref is the
# turnover scale at which the slippage add-on equals slip_coef bp.
_DEFAULT_SLIP_COEF = 2.0            # bp of extra slippage at the reference depth
_DEFAULT_REF_DOLLAR_VOL = 10_000_000.0   # the liquid floor — a coin at ref pays base+slip_coef
_DEFAULT_COST_CAP_BPS = 50.0        # never charge more than this per coin (illiquid-alt ceiling)


def _epoch_day_to_iso(day: int) -> str:
    return (dt.date(1970, 1, 1) + dt.timedelta(days=int(day))).isoformat()


def worst_basis_proxy(perp_high: float, perp_low: float,
                      spot_high: float, spot_low: float) -> float:
    """DEPRECATED — NOT used by the carry accounting any more (kept only as a pure helper / for the old
    unit tests). It was the daily HIGH/LOW adverse-basis magnitude `abs(min(0, spot_low/perp_high − 1))`,
    formerly SUBTRACTED additively EVERY day — which compounded the FULL daily range into a −100% wipeout
    (Sharpe −20.85), because the full daily range is NOT a valid `worst_basis` (it is a few % every day, not
    ≈ the close basis on normal days). The squeeze-tail is now charged via the REAL 1m `worst_basis` OVERRIDE
    (see `carry_metrics_from_store` / `BybitKlines.intraday_worst_basis`), mirroring the legacy venue exactly. Pure;
    no I/O. Non-positive prices → 0."""
    if perp_high <= 0.0 or spot_low <= 0.0:
        return 0.0
    wb = spot_low / perp_high - 1.0
    return abs(min(0.0, wb))


def per_coin_cost_bps(median_dollar_vol: float, *, base_bps: float,
                      slip_coef: float = _DEFAULT_SLIP_COEF,
                      ref_dollar_vol: float = _DEFAULT_REF_DOLLAR_VOL,
                      cap_bps: float = _DEFAULT_COST_CAP_BPS) -> float:
    """Liquidity-scaled per-coin taker cost (bps): `clip(base + slip_coef·(ref/median_dv), base, cap)`.

    A liquid coin (median_dv >= ref) → the add-on slip_coef·(ref/median_dv) <= slip_coef, ~base+slip_coef
    floored toward base; a thin coin (median_dv << ref) → the add-on blows up, capped at `cap_bps`. The
    flat-5.5bp naive model is the median_dv→∞ limit. median_dv <= 0 (no liquidity) → the cap (max charge).
    Pure."""
    if median_dollar_vol <= 0.0:
        return cap_bps
    cost = base_bps + slip_coef * (ref_dollar_vol / median_dollar_vol)
    return max(base_bps, min(cap_bps, cost))


def _curve_metrics(daily: list[tuple[int, float]], *, days_meta: dict[str, Any]) -> dict[str, Any]:
    """Standard CAGR / Sharpe / maxDD / total-return over a list of (epoch_day, net_ret), ascending.

    Mirrors `paper_sim._metrics`: Sharpe = mean/pstdev × √365; CAGR annualised over the date-span
    year-fraction; maxDD off the compounded equity curve from 1.0. `daily` empty → all-zero metrics."""
    if not daily:
        return {"cagr": 0.0, "sharpe": 0.0, "max_dd": 0.0, "total_return": 0.0,
                "ann_return": 0.0, "days": 0, **days_meta}
    days = [d for d, _ in daily]
    rets = [r for _, r in daily]

    equity = 1.0
    peak = 1.0
    max_dd = 0.0
    for r in rets:
        equity *= (1.0 + r)
        peak = max(peak, equity)
        if peak > 0:
            max_dd = min(max_dd, equity / peak - 1.0)
    total_return = equity - 1.0

    span = (days[-1] - days[0]) if len(days) >= 2 else 0
    years = span / _PERIODS_PER_YEAR if span > 0 else 0.0
    cagr = ((equity ** (1.0 / years) - 1.0) if years > 0 and equity > 0 else 0.0)

    if len(rets) >= 2:
        sd = st.pstdev(rets)
        sharpe = (st.mean(rets) / sd * math.sqrt(_PERIODS_PER_YEAR)) if sd > 0 else 0.0
    else:
        sharpe = 0.0
    ann_return = st.mean(rets) * _PERIODS_PER_YEAR if rets else 0.0

    return {"cagr": cagr, "sharpe": sharpe, "max_dd": max_dd, "total_return": total_return,
            "ann_return": ann_return, "days": len(rets), **days_meta}


def carry_metrics_from_store(store: _FeatureStore, *, cost_bps: float = 0.0,
                             start_day: int | None = None,
                             universe: set[str] | None = None,
                             charge_squeeze_tail: bool = False,
                             per_coin_cost: bool = False,
                             base_cost_bps: float = 5.5,
                             liquidity_lookback_days: int = 90) -> dict[str, Any]:
    """Build the carry curve from `store` and return {cagr, sharpe, max_dd, total_return, days, ann_return,
    symbols, first_day, last_day}.

    Pipeline (identical to the legacy-venue carry standalone, just a different store):
      1. close/funding/spot panels ← the store (the only I/O; `universe` restricts the symbol set);
      2. `warehouse_funding_panel(close, funding)` → the Panel `XsFundingReplay` consumes;
      3. for each trading day d (union of close-panel days), `XsFundingReplay.current_weights(d)` gives the
         market-neutral cross-sectional carry weights wᵢ (causal: uses only data <= d);
      4. the per-symbol FULL carry return `carry_retᵢ = (spot_retᵢ − perp_retᵢ) + fundingᵢ` via
         `carry_return_by_symbol(funding_d, basis_change_by_symbol(spot, perp, d))`;
      5. the book's day return = `Σ wᵢ·carry_retᵢ / Σ|wᵢ|` (the `sleeve_daily_carry` convention), realised
         on day d from the weights formed at d (the weight is a unit-gross book held into d's carry/basis);
      6. a per-turnover cost haircut: `Σᵢ|wᵢ_d − wᵢ_prev|/Σ|wᵢ_d| · cost_bps/1e4`, subtracted from the day
         return (turnover of the unit-gross weight vector vs the prior held weights).

    TRUSTWORTHY knobs (de-artifact the naive 13.92 Sharpe):
      * `universe` (set | None): restrict the carry symbols to this set (e.g. `liquid_universe(store)`); None
        = all symbols (the naive over-diversified universe). This is the DOMINANT fix.
      * `charge_squeeze_tail` (default False): OVERRIDE each held name's daily-close basis with the REAL
        intraday 1m `worst_basis` (capped at −1.0) when one is stored for that symbol/day — EXACTLY the
        legacy venue's `carry_return_by_symbol(worst_basis_by_symbol=...)` semantics. This charges the intraday
        squeeze the daily-close basis is blind to, WITHOUT adding anything on normal days (a worst_basis ≈
        the close basis barely moves the curve; only a genuine squeeze day bites). Needs the `worst_basis`
        feature (written by `ingest_bybit_worst_basis` from 1m perp+spot klines); a symbol/day with no
        `worst_basis` FALLS BACK to its daily-close basis. NOTE: this REPLACES the old broken additive
        high/low-`worst_basis_proxy` charge, which subtracted the full daily range every day → −100%.
      * `per_coin_cost` (default False): replace the flat `cost_bps` with a liquidity-scaled per-coin taker
        cost `per_coin_cost_bps(median_dvᵢ, base=base_cost_bps)` weighted by each name's turnover share —
        liquid coins ≈ base, thinner coins charged more (capped). When per_coin_cost is on, `base_cost_bps`
        is the floor; when off, the flat `cost_bps` is used.

    `cost_bps=0` and all knobs off → the gross/naive curve (backward-compatible). `start_day` (epoch-day,
    inclusive) floors the window. Returns sane zero metrics (days=0, symbols=0) on an empty store."""
    close = store.crypto_close_panel()
    funding = store.crypto_funding_panel()
    spot = store.crypto_spot_panel()
    if universe is not None:
        close = {s: v for s, v in close.items() if s in universe}
        funding = {s: v for s, v in funding.items() if s in universe}
        spot = {s: v for s, v in spot.items() if s in universe}

    symbols = sorted(set(close) | set(funding) | set(spot))
    panel = warehouse_funding_panel(close, funding)
    replay = XsFundingReplay(panel)

    # Real intraday worst_basis panel for the squeeze-tail OVERRIDE (only read when requested). {sym: {day: wb}}.
    worst_basis: dict[str, dict[int, float]] = {}
    if charge_squeeze_tail and symbols:
        worst_basis = store.read_panel(symbols, "worst_basis")

    # Per-coin median turnover (over the trailing window ending at the last day) for the per-coin cost model.
    median_dv: dict[str, float] = {}
    if per_coin_cost and symbols:
        turnover = store.read_panel(symbols, "turnover")
        all_t_days = [d for days in turnover.values() for d in days]
        if all_t_days:
            t_hi = max(all_t_days)
            t_lo = t_hi - liquidity_lookback_days + 1
            for sym in symbols:
                vals = [v for d, v in turnover.get(sym, {}).items() if t_lo <= d <= t_hi]
                if vals:
                    median_dv[sym] = st.median(vals)

    all_days = sorted({d for days in close.values() for d in days})
    if start_day is not None:
        all_days = [d for d in all_days if d >= start_day]

    daily: list[tuple[int, float]] = []
    prev_w: dict[str, float] = {}
    for d in all_days:
        weights = replay.current_weights(_epoch_day_to_iso(d))
        gross = sum(abs(w) for w in weights.values())
        if gross <= 0.0:
            prev_w = {}
            continue
        funding_d = {s: days[d] for s, days in funding.items() if d in days}
        basis_d = basis_change_by_symbol(spot, close, d)
        # Squeeze-tail OVERRIDE: replace this day's daily-close basis with the stored intraday worst_basis
        # (capped at −1.0 inside carry_return_by_symbol) for the symbols/days that have one — mirroring
        # the legacy venue's carry_return_by_symbol(worst_basis_by_symbol=...). Symbols/days without a worst_basis
        # keep the daily-close basis (the documented fallback). NOT an additive charge.
        worst_basis_d = (
            {s: wb[d] for s, wb in worst_basis.items() if d in wb} if charge_squeeze_tail else None)
        carry_ret = carry_return_by_symbol(funding_d, basis_d, worst_basis_d)
        book_ret = sum(w * carry_ret.get(s, 0.0) for s, w in weights.items()) / gross

        # Cost: flat per-turnover, OR liquidity-scaled per-coin (each name's turnover share at its own bps).
        if per_coin_cost:
            cost = 0.0
            for s in set(weights) | set(prev_w):
                traded = abs(weights.get(s, 0.0) - prev_w.get(s, 0.0))
                if traded == 0.0:
                    continue
                bps = per_coin_cost_bps(median_dv.get(s, 0.0), base_bps=base_cost_bps)
                cost += traded * (bps / 1e4)
            book_ret -= cost / gross
        elif cost_bps:
            turnover_w = sum(abs(weights.get(s, 0.0) - prev_w.get(s, 0.0))
                             for s in set(weights) | set(prev_w))
            book_ret -= (turnover_w / gross) * (cost_bps / 1e4)
        prev_w = weights
        daily.append((d, book_ret))

    meta = {
        "symbols": len(symbols),
        "first_day": _epoch_day_to_iso(daily[0][0]) if daily else None,
        "last_day": _epoch_day_to_iso(daily[-1][0]) if daily else None,
    }
    return _curve_metrics(daily, days_meta=meta)


def carry_weights_and_contributions(store: _FeatureStore, *, universe: set[str] | None = None,
                                    start_day: int | None = None,
                                    ) -> dict[int, dict[str, tuple[float, float]]]:
    """`{day: {symbol: (weight, carry_return)}}` for the xsfunding (carry) edge, reusing the EXACT pipeline
    `carry_metrics_from_store` uses: `XsFundingReplay.current_weights` (market-neutral, dollar-neutral
    quintile weights) + `carry_return_by_symbol` (`(spot_ret − perp_ret) + funding`). The weight is the
    UNIT-GROSS `wᵢ/Σ|w|` and the per-symbol return is the full daily carry return, so
    `Σ_symbol weight·carry_return == carry_metrics_from_store`'s GROSS day return `Σ wᵢ·carry_retᵢ/Σ|wᵢ|`
    (pre-cost; the per-turnover cost is applied identically by the live book's `trading_cost`). The carry
    return is the carry CONVENTION (not price-MTM) — the live bybit book books this sleeve as `return_mode
    == 'carry'`, exactly like the legacy venue's xsfunding sleeve. READ-ONLY; pure given the store.

    `universe` restricts the symbol set at the panel level (same as `carry_metrics_from_store`)."""
    close = store.crypto_close_panel()
    funding = store.crypto_funding_panel()
    spot = store.crypto_spot_panel()
    if universe is not None:
        close = {s: v for s, v in close.items() if s in universe}
        funding = {s: v for s, v in funding.items() if s in universe}
        spot = {s: v for s, v in spot.items() if s in universe}

    panel = warehouse_funding_panel(close, funding)
    replay = XsFundingReplay(panel)
    all_days = sorted({d for days in close.values() for d in days})
    if start_day is not None:
        all_days = [d for d in all_days if d >= start_day]

    out: dict[int, dict[str, tuple[float, float]]] = {}
    for d in all_days:
        weights = replay.current_weights(_epoch_day_to_iso(d))
        gross = sum(abs(w) for w in weights.values())
        if gross <= 0.0:
            continue
        funding_d = {s: days[d] for s, days in funding.items() if d in days}
        basis_d = basis_change_by_symbol(spot, close, d)
        carry_ret = carry_return_by_symbol(funding_d, basis_d, None)
        # Normalise the weights to unit gross (Σ|w|=1) — the `/gross` in carry_metrics_from_store's book_ret.
        out[int(d)] = {s: (w / gross, carry_ret.get(s, 0.0)) for s, w in weights.items()}
    return out


def revalidate(bybit_store: _FeatureStore, *, bybit_cost_bps: float = 5.5) -> dict[str, Any]:
    """Compute carry metrics on the Bybit store — the venue we trade (Phase 0b venue consolidation retired
    the legacy-venue-baseline comparison this report used to render; the venue-switch question it answered
    was already settled — Bybit IS the sole crypto venue).

    Returns {bybit: {...}, verdict: {...}}. The verdict: 'insufficient data' (holds=None) when the Bybit
    curve has < 2 days (the Phase A/B ingests not yet populated bybit_features); otherwise holds=True iff
    the net Bybit Sharpe is positive (the edge is alive), holds=False otherwise."""
    bybit = carry_metrics_from_store(bybit_store, cost_bps=bybit_cost_bps)

    if bybit["days"] < 2:
        verdict = {"holds": None, "reason": "insufficient data",
                   "detail": "Bybit carry curve has <2 days — run the Phase-A/B Bybit ingests first."}
    else:
        holds = bybit["sharpe"] > 0.0
        verdict = {
            "holds": holds,
            "reason": "bybit carry edge is positive" if holds else "bybit carry edge is NOT positive",
            "rule": "bybit.sharpe > 0",
            "bybit_sharpe": bybit["sharpe"],
        }
    return {"bybit": bybit, "verdict": verdict}


def trustworthy_revalidate(bybit_store: _FeatureStore, *,
                           bybit_cost_bps: float = 5.5,
                           start_day: int | None = None,
                           min_dollar_vol: float = 10_000_000.0,
                           liquidity_lookback_days: int = 90) -> dict[str, Any]:
    """The de-artifacting 2-row report: Bybit-naive (the 13.92 artifact — all symbols, daily-close basis,
    flat fee) vs Bybit-trustworthy (liquid universe + REAL 1m worst_basis squeeze-tail OVERRIDE + per-coin
    cost). Returns {bybit_naive, bybit_trustworthy, universe_size, verdict}.

    (Phase 0b venue consolidation: the legacy-venue-baseline comparison this report used to also render is
    retired — Bybit is the sole crypto venue.) The verdict compares the TRUSTWORTHY row against the NAIVE
    one: holds=True when the trustworthy treatment still clears a positive Sharpe (the discipline preserves
    a real edge, not an artifact); holds=None when the trustworthy curve has <2 days (insufficient data —
    run the Phase-A/B ingests first, plus `bybit-ingest-worst-basis` for the real 1m worst_basis the
    squeeze-tail override consumes)."""
    from fxhnt.application.bybit_liquidity import liquid_universe

    universe = liquid_universe(bybit_store, min_dollar_vol=min_dollar_vol,
                              lookback_days=liquidity_lookback_days)

    bybit_naive = carry_metrics_from_store(bybit_store, cost_bps=bybit_cost_bps, start_day=start_day)
    bybit_trustworthy = carry_metrics_from_store(
        bybit_store, start_day=start_day, universe=universe, charge_squeeze_tail=True,
        per_coin_cost=True, base_cost_bps=bybit_cost_bps, liquidity_lookback_days=liquidity_lookback_days)

    if bybit_trustworthy["days"] < 2:
        verdict = {"holds": None, "reason": "insufficient data",
                   "detail": "Trustworthy Bybit carry curve has <2 days — run the Phase-A/B ingests "
                             "(plus bybit-ingest-worst-basis for the real 1m squeeze-tail) first."}
    else:
        holds = bybit_trustworthy["sharpe"] > 0.0
        verdict = {
            "holds": holds,
            "reason": "trustworthy carry edge holds on Bybit" if holds
                      else "trustworthy carry edge does NOT hold on Bybit",
            "rule": "bybit_trustworthy.sharpe > 0",
            "bybit_trustworthy_sharpe": bybit_trustworthy["sharpe"],
        }
    return {"bybit_naive": bybit_naive, "bybit_trustworthy": bybit_trustworthy,
            "universe_size": len(universe), "verdict": verdict}
