"""Single source of the Bybit 4-edge book's scheduled-data persist logic.

`_persist_bybit_book` precomputes the deploy sleeves' full daily return series ONCE (liquid
`bybit_features` universe) and, from that SAME series, writes BOTH the `bybit_sleeve_ret` cockpit-sim
precompute AND the `_BYBIT_BOOK_SIDS` report-kind reconciliation-gate refs. It lives here (a neutral
application module) — NOT in the CLI composition root — so its three callers can share it without any of
them dragging in the other's import graph:

  * `fxhnt bybit-persist-sleeve-ret` / `fxhnt backtest-refs --all` (the CLI commands);
  * the nightly `bybit_book_precompute` Dagster asset (runs in an own-memory per-run K8s Job).

Keeping it out of `cli.py` matters: `cli.py` is the composition root (`from __future__ import annotations`
+ heavy top-level typer/broker/databento imports) AND `_persist_bybit_book` imports back INTO
`fxhnt.adapters.orchestration.assets` — so an asset importing it from `cli.py` would both pull the whole
CLI chain into the import-light daemon graph and route through that awkward cli↔assets coupling. This
module has neither problem.
"""
from __future__ import annotations

# The report-kind sids covering the bybit 4-edge book (+ its standalone single-sleeve tracks) — handled as a
# compute-once batch by `_persist_bybit_book`, NOT by the generic per-sid `backtest_ref` loop in
# `backtest_refs_cmd`. `xsfunding`/`unlock` joined 2026-07 (Phase 0b venue consolidation: their own Bybit
# forward tracks, like `positioning`) — all three sleeves are already computed as part of
# `_DEFAULT_BYBIT_SLEEVES` below, so this is a routing change only, no new compute.
_BYBIT_BOOK_SIDS = ("bybit_4edge", "bybit_4edge_levered", "positioning", "xsfunding", "unlock")


def _persist_bybit_book(settings, *, min_dollar_vol: float = 10_000_000.0,
                         cost_bps: float | None = None) -> tuple[int, str]:
    """Compute-once writer of the bybit 4-edge book's scheduled data. Precomputes the 4 sleeve return
    series ONCE (liquid `bybit_features` universe), then (a) persists them to `bybit_sleeve_ret` (the
    cockpit sim's precompute) and (b) upserts the `_BYBIT_BOOK_SIDS` report-kind reconciliation-gate refs
    (bybit_4edge / bybit_4edge_levered / positioning / xsfunding / unlock) from the SAME series. Single source of the
    bybit-book persist logic, shared by the on-demand `bybit-persist-sleeve-ret` command, the scheduled
    `backtest-refs --all`, and the nightly `bybit_book_precompute` asset (own-memory per-run K8s Job).

    Why an application helper, not the nightly daemon asset body: this is the heavy 4-sleeve compute. Wired
    INLINE into the nightly `bybit_4edge_nav` asset it OOM-killed the 4Gi dagster daemon (exit 137) — and
    because OOM is uncatchable the try/except guard could not save the forward-nav step either, breaking the
    forward track. So this writer runs in its OWN memory: the `bybit_book_precompute` asset is tagged for a
    high-memory per-run K8s Job (K8sRunLauncher), and it is ALSO reachable via `backtest-refs --all` /
    `bybit-persist-sleeve-ret` for an out-of-band run.

    MEMORY-BOUNDED: only the `--min-dollar-vol` liquid subset is READ (never the full ~794-coin table).
    IDEMPOTENT: (run_date, sleeve) primary-key overwrite — re-running on the same data rewrites the same rows.
    unlock needs the DefiLlama calendar (DB-first, JSON fallback); absent → the unlock sleeve is simply
    omitted, never fabricated.

    cost_bps defaults to `settings.bybit_cost_bps` (None sentinel) — the SAME single-sourced cost the
    forward tracks charge, so this ref can never silently diverge from what the tracks actually pay."""
    import datetime as dt
    import logging

    from fxhnt.adapters.orchestration.assets import _data_dir, persist_bybit_sleeve_returns
    from fxhnt.adapters.persistence.paper_repo import PaperRepo
    from fxhnt.adapters.warehouse.timescale_feature_store import TimescaleFeatureStore
    from fxhnt.application.bybit_liquidity import liquid_universe
    from fxhnt.application.unlock_calendar_loader import load_unlock_events, operational_unlock_repo

    log = logging.getLogger(__name__)
    cost_bps = settings.bybit_cost_bps if cost_bps is None else cost_bps
    store = TimescaleFeatureStore(settings.operational_dsn, table="bybit_features")
    try:
        universe = liquid_universe(store, min_dollar_vol=min_dollar_vol) or None
        try:
            unlock_events = load_unlock_events(operational_unlock_repo(settings),
                                               f"{_data_dir()}/unlock_calendar.json")
        except Exception as exc:  # noqa: BLE001 — absent calendar → unlock sleeve omitted, not a crash
            log.warning("_persist_bybit_book: could not load unlock calendar: %s", exc)
            unlock_events = []

        # COMPUTE-ONCE: the 4 deploy sleeves' full backtest series are the SAME input the row-persist and BOTH
        # backtest references (naive + levered) need. Compute them ONE time here and reuse for all three, instead
        # of recomputing the heavy multi-symbol walk-forward 3x (the old cost). The positioning capturability
        # haircuts (capacity + tail-cap) are baked into the positioning series HERE so every downstream
        # reference (bybit_4edge, levered, standalone positioning) reconciles against the capturable book.
        from fxhnt.application.bybit_book_eval import _DEFAULT_BYBIT_SLEEVES, sleeve_returns_from_store
        from fxhnt.application.bybit_forward_track import POSITIONING_COIN_GROSS_CAP, POSITIONING_TAIL_CAP_K
        sleeve_rets = {
            s: sleeve_returns_from_store(store, s, universe=universe, cost_bps=cost_bps,
                                         unlock_events=unlock_events,
                                         capacity_capital=settings.paper_capital,
                                         tail_cap_k=POSITIONING_TAIL_CAP_K,
                                         coin_gross_cap=POSITIONING_COIN_GROSS_CAP)
            for s in _DEFAULT_BYBIT_SLEEVES}

        repo = PaperRepo(settings.operational_dsn)
        repo.migrate()
        n = persist_bybit_sleeve_returns(
            repo, store, at=dt.datetime.now(dt.UTC), universe=universe,
            cost_bps=cost_bps, unlock_events=unlock_events, sleeve_returns=sleeve_rets)

        # Persist the `_BYBIT_BOOK_SIDS` report-kind reconciliation-gate refs (bybit_4edge naive-eqwt / levered
        # / standalone positioning / standalone xsfunding / standalone unlock) from the SAME precomputed sleeve
        # series — so the reconciliation gate has an `expected_window_return` to reconcile the forward against.
        # Without it the gate's divergence band is silently skipped and it degrades to a bare min-days +
        # clean-exec timer — a false-PASS risk on the real-capital book. `bybit_report_sid_kwargs` is the SINGLE
        # source of the per-sid identity kwargs; for bybit_4edge/bybit_4edge_levered/positioning it is ALSO
        # shared with `report_backtest_summary` (backtest_refs_report.py) so the two can never drift apart —
        # xsfunding/unlock are NOT in that module's `_REPORT_SIDS` (their refs are only ever computed here,
        # gated out of the generic per-sid loop below by `sid in _BYBIT_BOOK_SIDS`).
        # positioning's/xsfunding's/unlock's OWN forward tracks use BybitFourEdgeStrategy(sleeves=[<sleeve>]) —
        # the SAME single-sleeve construction (no vol overlay), so their reconciliation-gate ref is that book's
        # summary keyed as their own sid. crypto_tstrend / stablecoin have DIFFERENT forward constructions than
        # their raw sleeve series, so their refs are persisted from their own strategies in
        # `_persist_track_backtest_ref` (their nav assets), NOT here.
        from fxhnt.adapters.persistence.forward_nav import ForwardNavRepo
        from fxhnt.application.bybit_forward_track import bybit_4edge_backtest_summary, bybit_report_sid_kwargs
        fwd_repo = ForwardNavRepo(settings.operational_dsn)
        fwd_repo.migrate()
        for sid in _BYBIT_BOOK_SIDS:
            bt = bybit_4edge_backtest_summary(
                store, universe=universe, cost_bps=cost_bps, unlock_events=unlock_events,
                sleeve_returns=sleeve_rets, **bybit_report_sid_kwargs(sid))
            fwd_repo.upsert_backtest_summary(bt, at=dt.datetime.now(dt.UTC).replace(tzinfo=None))
            log.info("_persist_bybit_book: %s backtest ref cagr=%.1f%% sharpe=%.2f -> backtest_summary",
                     sid, bt.cagr * 100, bt.sharpe)
    finally:
        store.close()

    scope = f"{len(universe)} liquid symbols" if universe else "full universe (no liquid subset)"
    return n, scope
