"""Live-booking cross-sectional funding-dispersion paper track (executable construction).
Books the REAL delta-neutral return (funding + spot-perp basis) forward each day, so the paper
record experiences the basis tail the backtest had to guard out. Mirrors the equity-factor
live tracks' ForwardStrategy contract: advance(last_date, extra) -> (rows, extra)."""
from __future__ import annotations

import datetime as dt
from collections.abc import Callable
from typing import Any

from fxhnt.domain.cross_sectional_funding import construction_weights
from fxhnt.domain.strategies.equity_factor import robust_z


def _today() -> str:
    return dt.date.today().isoformat()


class XsFundingForward:
    def __init__(self, live: Any, *, quantile: float = 0.2, cost_bps: float = 8.0,
                 borrowable_qvol: float = 2e7, clock: Callable[[], str] = _today) -> None:
        self._live = live
        self._q = quantile
        self._cost = cost_bps / 1e4
        self._bq = borrowable_qvol
        self._clock = clock

    def advance(self, last_date: str | None, extra: dict[str, Any]) -> tuple[list[tuple[str, float]], dict[str, Any]]:
        # `forward_engine.run_track` injects extra["_today"] (its UTC `today`) for observed tracks — consume
        # it when present so the booked date matches the engine's clock, not this process's local timezone.
        # Fall back to self._clock() only for the standalone/test path (no engine, no injected _today).
        today = extra.get("_today") or self._clock()
        prev_pos: dict[str, float] = extra.get("positions", {})
        prev_perp: dict[str, float] = extra.get("perp", {})
        prev_spot: dict[str, float] = extra.get("spot", {})
        snap = self._live.snapshot(prev_pos)          # {sym: (tf, today_funding, perp, spot, qvol)}
        syms = sorted(snap)

        # 1) new executable weights from the trailing-funding cross-section
        tf = [snap[s][0] for s in syms]
        scores = {syms[i]: z for i, z in enumerate(robust_z(tf))}
        borrowable = {s for s in syms if snap[s][4] >= self._bq}
        new_w = construction_weights(scores, "executable", quantile=self._q, borrowable=borrowable)

        # 2) book the PRIOR book's realized delta-neutral return (funding + basis) minus rebalance cost
        rows: list[tuple[str, float]] = []
        if prev_pos:
            realized = 0.0
            for s, w in prev_pos.items():
                if s in snap and s in prev_perp and s in prev_spot and prev_perp[s] > 0 and prev_spot[s] > 0:
                    _, tf_today, perp, spot, _ = snap[s]
                    spot_ret = spot / prev_spot[s] - 1.0
                    perp_ret = perp / prev_perp[s] - 1.0
                    realized += w * ((spot_ret - perp_ret) + tf_today)
            turnover = sum(abs(new_w.get(s, 0.0) - prev_pos.get(s, 0.0)) for s in set(new_w) | set(prev_pos))
            realized -= turnover * self._cost / 2.0
            rows.append((today, realized))

        # 3) carry the new book forward
        extra = {"positions": new_w,
                 "perp": {s: snap[s][2] for s in new_w},
                 "spot": {s: snap[s][3] for s in new_w},
                 "last_date": today}
        return rows, extra

    def current_weights(self, as_of: str) -> dict[str, float]:
        """Signed delta-neutral per-symbol target weights from the LATEST funding cross-section,
        reusing the SAME `robust_z` + `construction_weights(..., "executable", ...)` build as
        `advance()` step 1 (live snapshot is the as-of state). NOTE: `as_of` is non-binding here — the
        funding book is built from the LIVE snapshot, not a panel indexed by date, so the argument is
        accepted for interface parity but does not select a historical cross-section. Returns {} when
        the live snapshot is empty / yields no book."""
        snap = self._live.snapshot({})
        syms = sorted(snap)
        if not syms:
            return {}
        tf = [snap[s][0] for s in syms]
        scores = {syms[i]: z for i, z in enumerate(robust_z(tf))}
        borrowable = {s for s in syms if snap[s][4] >= self._bq}
        return construction_weights(scores, "executable", quantile=self._q, borrowable=borrowable)
