"""Bybit LIVE paper book — per-symbol positions, marked to live Bybit prices (the fund's sole crypto venue).

Bybit is the venue we will actually trade, so it must be a REAL live book (per-symbol positions + trades +
mark-to-market equity), not only the returns/forward-NAV track. This module turns the 4 Bybit edges'
per-symbol target weights (surfaced faithfully by `bybit_paper_weights`, reconciled to the stored sleeve
returns) into target positions on the latest Bybit close panel, diffs them against the prior held book to
produce trades, and persists them through the SHARED `persist_paper_book` seam into `PaperRepo` (the bybit
book is the repo's sole venue). The cockpit then renders it via `PaperBookService.view`.

Naive EQUAL-WEIGHT combination (25% each) — the DEPLOY book (the A/B proved naive eq-wt is best OOS for the
Bybit book; the live overlay is NOT applied here, matching the bybit forward track's design).

Per-symbol weight: `combined[sleeve][symbol] = (1/n_sleeves)·sleeve_weight[symbol]`. Target qty =
`weight·capital / price` (the shared `target_positions`). Cost: a flat Bybit per-turnover rate (5.5bp),
the same haircut the forward track + stored `bybit_sleeve_ret` charge.

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.
"""
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
from fxhnt.application.bybit_paper_weights import sleeve_weights_and_contributions
from fxhnt.application.paper_book import persist_paper_book

# The Bybit live book's per-turnover trading cost (matches the forward track / the stored bybit_sleeve_ret
# 5.5bp). Flat across all 4 sleeves (the Bybit-perp realistic taker+spread cost).
_BYBIT_COST_RATE = 0.00055


def latest_prices(store: Any, *, universe: set[str] | None = None) -> dict[str, float]:
    """`{symbol: latest_close}` from the Bybit `close` panel — the live mark (the last day per symbol). Reuses
    the universe-restricted read so only the liquid symbols load. READ-ONLY."""
    from fxhnt.application.bybit_book_eval import _bounded_store

    panel = _bounded_store(store, universe).crypto_close_panel()
    out: dict[str, float] = {}
    for sym, series in panel.items():
        if series:
            out[sym] = float(series[max(series)])
    return out


def combined_symbol_weights(store: Any, *, universe: set[str] | None = None,
                            unlock_events: list[dict[str, Any]] | None = None,
                            sleeves: tuple[str, ...] = _DEFAULT_BYBIT_SLEEVES,
                            as_of_day: int | None = None,
                            positioning_coin_gross_cap: float | None = None,
                            ) -> tuple[dict[str, dict[str, float]], int | None]:
    """`({sleeve: {symbol: combined_weight}}, as_of_day)` for the LATEST day of each sleeve, combined NAIVE
    EQUAL-WEIGHT (1/n per sleeve). `combined_weight = (1/n)·sleeve_weight` where `sleeve_weight` is the
    per-symbol weight the edge's engine used (surfaced faithfully by `sleeve_weights_and_contributions`).

    Each sleeve's weights are taken on ITS OWN latest available day (the engines emit different last days);
    the returned `as_of_day` is the max latest day across the sleeves (the book's run_date). A sleeve with no
    weights (no data / uncomputable, e.g. unlock with no calendar) is simply absent. `as_of_day` (epoch day)
    can be pinned for a deterministic backfill day. `positioning_coin_gross_cap` (positioning sleeve ONLY) is
    forwarded straight into `sleeve_weights_and_contributions` — default None preserves the byte-identical
    uncapped behaviour. Pure given the store; READ-ONLY."""
    n = len(sleeves)
    if n == 0:
        return {}, None
    combined: dict[str, dict[str, float]] = {}
    latest_day: int | None = None
    for sleeve in sleeves:
        by_day = sleeve_weights_and_contributions(store, sleeve, universe=universe,
                                                  unlock_events=unlock_events,
                                                  positioning_coin_gross_cap=positioning_coin_gross_cap)
        days = [d for d in by_day if as_of_day is None or d <= as_of_day]
        if not days:
            continue
        d = max(days)
        latest_day = d if latest_day is None else max(latest_day, d)
        # weight is the first element of each (weight, return) pair; scale by the naive eq-wt sleeve share.
        combined[sleeve] = {sym: (1.0 / n) * w for sym, (w, _r) in by_day[d].items() if w != 0.0}
    return {s: w for s, w in combined.items() if w}, latest_day


def latest_raw_sleeve_weights(store: Any, *, universe: set[str] | None = None,
                              unlock_events: list[dict[str, Any]] | None = None,
                              sleeves: tuple[str, ...] = _DEFAULT_BYBIT_SLEEVES,
                              as_of_day: int | None = None,
                              positioning_coin_gross_cap: float | None = None) -> dict[str, float]:
    """`{symbol: raw_target_weight}` for the Bybit 4-edge book at its LATEST as_of, BEFORE any risk overlay —
    the source the real-paper / testnet execution leg places orders against.

    This is the per-symbol SUM of the naive-eq-wt `combined_symbol_weights` (each sleeve already carries its
    1/n share), i.e. the book's RAW pre-overlay per-symbol target. It is NOT the post-overlay sim-marked
    positions (which would bake in the sim book's own equity/leverage/killswitch state) — the vol-target /
    gross-cap / drawdown-killswitch overlay is applied SEPARATELY downstream (re-applied on the execution
    venue's own NAV/drawdown, see `paper_book.paper_risk_overlay` / `IncrementalRiskOverlay.overlay`).

    Pure + READ-ONLY against the warehouse `store` (writes nothing); memory-bounded on the liquid `universe`.
    Net-zero symbols (a long sleeve cancelled by a short sleeve) are dropped. `as_of_day` (epoch day) pins a
    deterministic day; default = each sleeve's own latest. The companion as_of day is available via
    `combined_symbol_weights` when the caller needs it (e.g. an idempotency marker). `positioning_coin_gross_cap`
    forwards into `combined_symbol_weights` — default None preserves the byte-identical uncapped behaviour."""
    combined, _day = combined_symbol_weights(
        store, universe=universe, unlock_events=unlock_events, sleeves=sleeves, as_of_day=as_of_day,
        positioning_coin_gross_cap=positioning_coin_gross_cap)
    out: dict[str, float] = {}
    for by_symbol in combined.values():
        for sym, w in by_symbol.items():
            out[sym] = out.get(sym, 0.0) + w
    return {sym: w for sym, w in out.items() if w != 0.0}


def combined_symbol_weights_and_returns(
    store: Any, *, universe: set[str] | None = None,
    unlock_events: list[dict[str, Any]] | None = None,
    sleeves: tuple[str, ...] = _DEFAULT_BYBIT_SLEEVES,
    as_of_day: int | None = None,
    positioning_coin_gross_cap: float | None = None,
) -> tuple[dict[str, dict[str, float]], dict[str, dict[str, float]], int | None]:
    """`(combined_weights, returns, as_of_day)` — like `combined_symbol_weights` but ALSO surfaces each
    sleeve's TRUE per-symbol return on its OWN latest (bundled) day: `returns[sleeve][symbol]` is the `r` in
    the `(weight, return)` pair (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 forward stepper
    books the TRUE edge return (no naked perp price-MTM, no dropped funding/hedge). `positioning_coin_gross_cap`
    (positioning sleeve ONLY) forwards into `sleeve_weights_and_contributions` so the LIVE paper-book positions
    honor the same per-coin cap the reconciled return uses — default None preserves the byte-identical
    uncapped behaviour. Pure; READ-ONLY."""
    n = len(sleeves)
    if n == 0:
        return {}, {}, None
    combined: dict[str, dict[str, float]] = {}
    returns: dict[str, dict[str, float]] = {}
    latest_day: int | None = None
    for sleeve in sleeves:
        by_day = sleeve_weights_and_contributions(store, sleeve, universe=universe,
                                                  unlock_events=unlock_events,
                                                  positioning_coin_gross_cap=positioning_coin_gross_cap)
        days = [d for d in by_day if as_of_day is None or d <= as_of_day]
        if not days:
            continue
        d = max(days)
        latest_day = d if latest_day is None else max(latest_day, d)
        row = by_day[d]
        combined[sleeve] = {sym: (1.0 / n) * w for sym, (w, _r) in row.items() if w != 0.0}
        returns[sleeve] = {sym: r for sym, (_w, r) in row.items()}
    combined = {s: w for s, w in combined.items() if w}
    returns = {s: returns[s] for s in combined}     # keep only sleeves with surviving weights
    return combined, returns, latest_day


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


def derive_and_persist_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,
                                        enabled: bool = True,
                                        cost_rate: float = _BYBIT_COST_RATE,
                                        as_of_day: int | None = None,
                                        real_spread_by_coin: dict[str, float] | None = None,
                                        positioning_coin_gross_cap: float | None = None) -> int:
    """Derive the Bybit live paper book for its latest day and persist it through the shared
    `persist_paper_book` seam into `repo` (bybit is `PaperRepo`'s sole venue). Returns the number of
    open positions persisted (0 when disabled / no data).

    Pipeline:
      1. combined per-symbol target weights (naive eq-wt 25% each) + each sleeve's TRUE per-symbol return for
         the latest day (`combined_symbol_weights_and_returns`);
      2. latest Bybit close per symbol = the live mark (for sizing + turnover cost);
      3. `persist_paper_book` (RETURN-DEFINED mode) books each sleeve's TRUE return (carry incl. funding,
         unlock incl. hedge, trend/positioning = price) contemporaneously on the latest day's weights, sizes
         target positions (`weight·equity/price`), diffs vs the prior bybit book → trades, charges the 5.5bp
         per-turnover cost, and compounds into equity.

    HISTORY: this stepper USED to mark every sleeve to raw perp price (no funding/carry/hedge args) — dropping
    the carry sleeve's funding (the edge) and the unlock hedge — understating the book ~2.6x. It now books the
    TRUE edge returns and reconciles to the reconstruction / cockpit Backtest.

    COST: when `real_spread_by_coin` is supplied, the latest day is charged the per-COIN MEASURED cost (the
    SAME taker_fee + 0.5·real_spread(coin) the cockpit Backtest charges), so the forward `/paper` step ties to
    the Backtest NET; else the flat Bybit 5.5bp turnover haircut (the pre-real-spread path).

    `positioning_coin_gross_cap` (positioning sleeve ONLY) forwards into `combined_symbol_weights_and_returns`
    so the LIVE persisted positions honor the same per-coin cap the reconciled return uses — a single
    over-short coin can no longer take an outsized real position. Default None preserves the byte-identical
    uncapped behaviour.

    READ-ONLY against the warehouse `store`; only the paper store is written. `enabled=False` is a no-op."""
    if not enabled:
        return 0
    combined, ret_map, day = combined_symbol_weights_and_returns(
        store, universe=universe, unlock_events=unlock_events, as_of_day=as_of_day,
        positioning_coin_gross_cap=positioning_coin_gross_cap)
    if not combined or day is None:
        return 0
    run_date = _epoch_day_to_iso(day)
    prices = latest_prices(store, universe=universe)
    sleeve_weights = {sleeve: 1.0 for sleeve in combined}   # the per-symbol weights already carry the 1/n
    if real_spread_by_coin is not None:
        # Per-COIN MEASURED cost for the latest day (real L1 spread) — ties the forward step to the Backtest
        # NET. Local import avoids a circular import (bybit_paper_backfill imports this module).
        from fxhnt.application.bybit_paper_backfill import cost_frac_by_day
        cf = cost_frac_by_day(store, universe=universe, unlock_events=unlock_events,
                              real_spread_by_coin=real_spread_by_coin)
        return persist_paper_book(
            repo, capital=capital, run_date=run_date, sleeve_weights=sleeve_weights,
            symbol_weights_by_sleeve=combined, prices=prices, at=at, enabled=enabled,
            leverage=1.0, return_by_sleeve_symbol=ret_map, cost_frac=cf.get(day, 0.0))
    # Cost rate keyed over ALL sleeves (not just the present ones): a sleeve EXITING the book still has
    # turnover in `prior` that must be charged the Bybit flat rate, never `trading_cost`'s 15bp default.
    cost_rates = {sleeve: cost_rate for sleeve in _DEFAULT_BYBIT_SLEEVES}
    return persist_paper_book(
        repo, capital=capital, run_date=run_date, sleeve_weights=sleeve_weights,
        symbol_weights_by_sleeve=combined, prices=prices, at=at, enabled=enabled,
        leverage=1.0, cost_rates=cost_rates, return_by_sleeve_symbol=ret_map)
