"""Bybit LIVE paper-book BACKFILL — walk the full Bybit history and persist the live paper book daily, so
`/paper/replay` shows a multi-year record for the deploy venue.

(The legacy combined-book equivalent, `paper_backfill.backfill_paper_book` driven by the `paper-backfill`
CLI, was RETIRED — Phase 0b Task 7d, a dead legacy-venue-era artifact redundant with this Bybit book.) This
module instead: reuses the SAME seams the LIVE Bybit derive (`bybit_paper_book.derive_and_persist_bybit_paper_book`)
uses, but for EVERY historical day rather than only the latest:

  * `bybit_paper_weights.sleeve_weights_and_contributions` — per-sleeve, per-day, per-symbol `(weight,
    return)` pairs, reconciled to each edge's GROSS return engine (the HARD reconciliation invariant). The
    runner emits ALL days in one call, so the backfill extracts the whole history per sleeve ONCE (no
    per-date engine re-run).
  * naive EQUAL-WEIGHT 4-edge combination (`combined_weight = (1/n)·sleeve_weight`) — the DEPLOY book, the
    same combination the live derive uses (the A/B proved naive eq-wt is best OOS for the Bybit book; the
    live overlay is intentionally NOT applied here, matching the live book + forward track's design).
  * `paper_book.persist_paper_book` — sizes target positions (`weight·equity/price` at THAT day's close),
    diffs vs the PRIOR day's bybit book → trades, charges the 5.5bp per-turnover cost (the SAME haircut the
    live book + forward track charge, so the backfill + forward record form ONE continuous book), and
    compounds the book MTM into nav. Per-`run_date` idempotent replace → re-running is safe.

Dates are processed ASCENDING so the compounding nav equity (`nav_equity_before` + prior-book MTM) builds
date-by-date, into `PaperRepo` (bybit is its sole venue). READ-ONLY against the warehouse store; only the
paper store is written. Memory-bounded: the weight extraction reuses the `_UniverseRestrictedStore` seam, so
only the liquid `universe` symbols ever load.

CONTINUITY: the LAST backfilled day == what the single-day live derive
(`derive_and_persist_bybit_paper_book`) produces — both take the latest day's `combined_symbol_weights` and
push it through `persist_paper_book`. The backfill simply fills every PRIOR day with the same machinery.
"""
from __future__ import annotations

import datetime as dt
from typing import Any

from fxhnt.adapters.persistence.paper_repo import PaperRepo
from fxhnt.application.bybit_book_eval import _DEFAULT_BYBIT_SLEEVES, _bounded_store
from fxhnt.application.bybit_liquidity_sweep import _TAKER_FEE_BPS, per_coin_book_returns
from fxhnt.application.bybit_maker_recost import recost_net_series
from fxhnt.application.bybit_paper_book import _BYBIT_COST_RATE, _epoch_day_to_iso
from fxhnt.application.bybit_paper_weights import sleeve_weights_and_contributions
from fxhnt.application.paper_book import persist_paper_book


def _extract_per_sleeve(
    store: Any, *, universe: set[str] | None,
    unlock_events: list[dict[str, Any]] | None,
    sleeves: tuple[str, ...],
    positioning_coin_gross_cap: float | None = None,
) -> dict[str, dict[int, dict[str, tuple[float, float]]]]:
    """`{sleeve: {epoch_day: {symbol: (weight, TRUE return)}}}` — one reconciled engine run per sleeve
    (`sleeve_weights_and_contributions`, Σ w·r == the edge's GROSS return). The single source the combined
    WEIGHTS and the per-symbol RETURNS are both derived from, so the live book and the reconstruction
    consume identical pairs. A sleeve with no weights is absent. `positioning_coin_gross_cap` (positioning
    sleeve ONLY) forwards into `sleeve_weights_and_contributions` — default None preserves the byte-identical
    uncapped behaviour. READ-ONLY; memory-bounded by `universe`."""
    out: dict[str, dict[int, dict[str, tuple[float, float]]]] = {}
    for sleeve in sleeves:
        per_day = sleeve_weights_and_contributions(store, sleeve, universe=universe,
                                                   unlock_events=unlock_events,
                                                   positioning_coin_gross_cap=positioning_coin_gross_cap)
        if per_day:
            out[sleeve] = per_day
    return out


def _combined_weights(per_sleeve: dict[str, dict[int, dict[str, tuple[float, float]]]], n: int
                      ) -> dict[int, dict[str, dict[str, float]]]:
    """`{epoch_day: {sleeve: {symbol: (1/n)·weight}}}` — naive eq-wt combination; zero-weight symbols
    dropped (a zero target adds nothing); a day/sleeve with no surviving weight is absent."""
    by_day: dict[int, dict[str, dict[str, float]]] = {}
    for sleeve, per_day in per_sleeve.items():
        for day, row in per_day.items():
            scaled = {sym: (1.0 / n) * w for sym, (w, _r) in row.items() if w != 0.0}
            if scaled:
                by_day.setdefault(day, {})[sleeve] = scaled
    return by_day


def _returns_by_day(per_sleeve: dict[str, dict[int, dict[str, tuple[float, float]]]]
                    ) -> dict[int, dict[str, dict[str, float]]]:
    """`{epoch_day: {sleeve: {symbol: TRUE return}}}` — each edge's per-symbol realized return (the `r` in
    the `(w, r)` pairs: carry = basis + FUNDING, unlock = short_leg_pnl incl. hedge + squeeze, trend /
    positioning = price). Fed to `persist_paper_book`'s return-defined book so the live book books the TRUE
    edge return, not naked perp price-MTM (which dropped funding + the hedge legs)."""
    by_day: dict[int, dict[str, dict[str, float]]] = {}
    for sleeve, per_day in per_sleeve.items():
        for day, row in per_day.items():
            by_day.setdefault(day, {})[sleeve] = {sym: r for sym, (_w, r) in row.items()}
    return by_day


def combined_symbol_weights_by_day(
    store: Any, *, universe: set[str] | None = None,
    unlock_events: list[dict[str, Any]] | None = None,
    sleeves: tuple[str, ...] = _DEFAULT_BYBIT_SLEEVES,
) -> dict[int, dict[str, dict[str, float]]]:
    """`{epoch_day: {sleeve: {symbol: combined_weight}}}` over the FULL history of every sleeve, combined
    NAIVE EQUAL-WEIGHT (`combined_weight = (1/n)·sleeve_weight`).

    This is the historical counterpart of `bybit_paper_book.combined_symbol_weights` (which returns only the
    latest day): each sleeve's per-day per-symbol `(weight, return)` pairs are pulled ONCE via
    `sleeve_weights_and_contributions` (the reconciled extraction — Σ w·r == the edge's GROSS return), then
    every day on which a sleeve has weights contributes that sleeve's `(1/n)`-scaled weights to that day.

    A sleeve with no weights (no data / uncomputable, e.g. unlock with no calendar) is simply absent on every
    day. A day on which NO sleeve has weights is absent from the result. Zero-weight symbols are dropped (a
    zero target adds nothing to the book). Pure given the store; READ-ONLY; memory-bounded (the runners read
    only the `universe` subset)."""
    n = len(sleeves)
    if n == 0:
        return {}
    per_sleeve = _extract_per_sleeve(store, universe=universe, unlock_events=unlock_events, sleeves=sleeves)
    return _combined_weights(per_sleeve, n)


def closes_by_day(store: Any, *, universe: set[str] | None = None) -> dict[int, dict[str, float]]:
    """`{epoch_day: {symbol: close}}` from the Bybit `close` panel — that day's mark for every symbol. Reuses
    the universe-restricted read so only the liquid symbols load. READ-ONLY. (The live derive marks only the
    latest close via `latest_prices`; the backfill needs each day's close to size + MTM that day.)"""
    panel = _bounded_store(store, universe).crypto_close_panel()
    out: dict[int, dict[str, float]] = {}
    for sym, series in panel.items():
        for day, close in series.items():
            out.setdefault(int(day), {})[sym] = float(close)
    return out


def cost_frac_by_day(
    store: Any, *, universe: set[str] | None = None,
    unlock_events: list[dict[str, Any]] | None = None,
    sleeves: tuple[str, ...] = _DEFAULT_BYBIT_SLEEVES,
    real_spread_by_coin: dict[str, float] | None = None,
    taker_fee_bps: float = _TAKER_FEE_BPS,
) -> dict[int, float]:
    """`{epoch_day: cost_frac}` — the per-COIN MEASURED cost as a FRACTION of equity for each day, EXACTLY
    the cost `_bybit_measured_net` charges: `cost_frac[d] = gross[d] − net[d]` where `gross`/`net` come from
    `per_coin_book_returns` (the cockpit Backtest basis) re-costed with the REAL L1 spread via
    `recost_net_series` (taker_fee + 0.5·real_spread(coin), CS fallback for an un-quoted coin — identical to
    the Backtest). Subtracting this from the live book's (already-reconciled) GROSS makes the live `/paper` NET
    == the Backtest NET BY CONSTRUCTION. READ-ONLY; memory-bounded by `universe`."""
    pc = per_coin_book_returns(store, universe=universe, sleeves=list(sleeves), capital=1.0,
                               taker_fee_bps=taker_fee_bps, unlock_events=unlock_events or [],
                               return_detail=True)
    if not pc.get("gross_series"):
        return {}
    gross = pc["gross_series"]
    rc = recost_net_series(pc, fill_fraction=0.0, adverse_sel=0.0, taker_fee_bps=taker_fee_bps,
                           real_spread_by_coin=real_spread_by_coin)
    net = rc["net_series"]
    return {d: gross[d] - net.get(d, gross[d]) for d in gross}


def backfill_bybit_paper_book(
    repo: PaperRepo, store: Any, *, capital: float, at: dt.datetime,
    universe: set[str] | None = None,
    unlock_events: list[dict[str, Any]] | None = None,
    cost_rate: float = _BYBIT_COST_RATE,
    sleeves: tuple[str, ...] = _DEFAULT_BYBIT_SLEEVES,
    since_day: int | None = None,
    real_spread_by_coin: dict[str, float] | None = None,
    positioning_coin_gross_cap: float | None = None,
) -> int:
    """Walk every historical Bybit day on which the 4-edge weights exist and persist the live eq-wt book for
    each day. Returns the number of dates with a persisted nav row.

    `repo` is a `PaperRepo` (bybit is its sole venue). For each day ascending:
      1. the naive eq-wt 4-edge per-symbol target weights for THAT day (from
         `combined_symbol_weights_by_day`);
      2. that day's Bybit close per symbol = the mark;
      3. `persist_paper_book` (RETURN-DEFINED mode) books each sleeve's TRUE per-symbol return for THAT day
         (carry = basis + FUNDING, unlock = short_leg_pnl incl. hedge + squeeze, trend/positioning = price)
         contemporaneously on that day's weights — so the daily book return == the reconstruction's
         `(1/n)·Σ sleeve_true_gross` — sizes target positions (`weight·equity/price`) for the cockpit view,
         charges the 5.5bp per-turnover cost, and compounds into equity (idempotent per run_date).

    Ascending order is REQUIRED so the compounding nav (`nav_equity_before` + today's booked return) is
    causal. Per-`run_date` idempotent replace makes re-running safe (each date's positions/trades/nav are
    rewritten, never duplicated). `since_day` (epoch day) restricts to days STRICTLY after it — the resumable
    / incremental fill (the days before are left as already-persisted history).

    COST: when `real_spread_by_coin` is supplied, each day is charged the per-COIN MEASURED cost
    (`cost_frac_by_day` — taker_fee + 0.5·real_spread(coin), the SAME cost `_bybit_measured_net` / the cockpit
    Backtest charge), so the live `/paper` NET == the Backtest NET BY CONSTRUCTION. When it is None the legacy
    flat per-sleeve `cost_rate` (Bybit 5.5bp) turnover haircut is charged instead (the pre-real-spread path,
    and the cost_rate=0 GROSS-reconciliation path).

    HISTORY: the live book USED to mark every sleeve to raw perp price (no funding/carry/hedge args), which
    DROPPED the carry sleeve's funding (the literal edge) and the unlock hedge and lagged every sleeve a day
    — understating the book ~2.6x (the ~+294% bug vs the honest reconstruction). It now books the TRUE edge
    returns and reconciles to the reconstruction (guarded by test_bybit_book_reconciliation).

    `positioning_coin_gross_cap` (positioning sleeve ONLY) forwards into `_extract_per_sleeve` /
    `sleeve_weights_and_contributions` so the backfilled positions honor the same per-coin cap the reconciled
    return uses — default None preserves the byte-identical uncapped behaviour.

    READ-ONLY against the warehouse `store`; only the paper store is written. The shared `_BYBIT_COST_RATE`
    keeps the backfill and the live forward track on the SAME cost basis → one continuous book."""
    n = len(sleeves)
    if n == 0:
        return 0
    per_sleeve = _extract_per_sleeve(store, universe=universe, unlock_events=unlock_events, sleeves=sleeves,
                                     positioning_coin_gross_cap=positioning_coin_gross_cap)
    weights_by_day = _combined_weights(per_sleeve, n)
    if not weights_by_day:
        return 0
    returns_by_day = _returns_by_day(per_sleeve)
    closes = closes_by_day(store, universe=universe)
    # Per-COIN MEASURED cost per day (real L1 spread) when supplied — makes the live NET tie to the Backtest.
    cost_fracs = (cost_frac_by_day(store, universe=universe, unlock_events=unlock_events, sleeves=sleeves,
                                   real_spread_by_coin=real_spread_by_coin)
                  if real_spread_by_coin is not None else {})
    days = sorted(d for d in weights_by_day if since_day is None or d > since_day)
    persisted = 0
    for day in days:
        combined = weights_by_day[day]
        prices = closes.get(day, {})
        run_date = _epoch_day_to_iso(day)
        # The per-symbol weights already carry the 1/n share, so each sleeve's sleeve_weight is 1.0.
        sleeve_weights = {sleeve: 1.0 for sleeve in combined}
        ret_map = returns_by_day.get(day, {})
        # Real per-coin measured cost (→ /paper NET == Backtest NET) when available; else the flat per-sleeve
        # haircut. Flat `cost_rates` is keyed over ALL `sleeves` (not just the present ones): a sleeve EXITING
        # the book (e.g. the unlock window closing) still has turnover in `prior`, which must be charged the
        # Bybit flat rate — else `trading_cost`'s 15bp DEFAULT would be applied to the exit, an off-rate spike.
        if real_spread_by_coin is not None:
            persist_paper_book(
                repo, capital=capital, run_date=run_date, sleeve_weights=sleeve_weights,
                symbol_weights_by_sleeve=combined, prices=prices, at=at, enabled=True,
                leverage=1.0, return_by_sleeve_symbol=ret_map, cost_frac=cost_fracs.get(day, 0.0))
        else:
            cost_rates = {sleeve: cost_rate for sleeve in sleeves}
            persist_paper_book(
                repo, capital=capital, run_date=run_date, sleeve_weights=sleeve_weights,
                symbol_weights_by_sleeve=combined, prices=prices, at=at, enabled=True,
                leverage=1.0, cost_rates=cost_rates, return_by_sleeve_symbol=ret_map)
        persisted += 1
    return persisted
