"""Daily-rebalanced SHORT-rich stablecoin peg-reversion backtest over a spot-close panel.

panel = {symbol: {epoch_day: close}}. Each day d->nxt: short the rich names (short_rich_weights),
book realized daily return minus turnover cost. Flat on days with no rich name."""
from __future__ import annotations

import bisect
from dataclasses import dataclass

import numpy as np

from fxhnt.application._date_axis import as_of_day as _as_of_day
from fxhnt.domain.stablecoin_reversion import short_rich_weights


@dataclass(frozen=True)
class StableRunResult:
    returns: np.ndarray
    dates: list[int]
    n_active_avg: float


class StableReversionRunner:
    def __init__(self, panel: dict[str, dict[int, float]], *, thresh_bp: float = 50.0,
                 cost_bps: float = 4.0, periods_per_year: int = 365) -> None:
        self._p = panel
        self._th = thresh_bp / 1e4
        self._cost = cost_bps / 1e4
        self._ppy = periods_per_year
        # Date-independent sorted day axis, memoized on first use. The panel is immutable after
        # construction and the runner is built fresh per backfill, so a single memoization is safe.
        self._all_days_cache: list[int] | None = None

    def _all_days(self) -> list[int]:
        """Sorted distinct panel days (date-independent) — memoized so `current_weights` finds the
        latest day <= as_of via bisect instead of re-scanning+sorting the whole panel every call."""
        if self._all_days_cache is None:
            self._all_days_cache = sorted({d for s in self._p.values() for d in s})
        return self._all_days_cache

    def run(self) -> StableRunResult:
        all_days = self._all_days()
        rets: list[float] = []
        dates: list[int] = []
        prev: dict[str, float] = {}
        active = 0
        n = 0
        for i in range(len(all_days) - 1):
            d, nxt = all_days[i], all_days[i + 1]
            dev = {s: series[d] - 1.0 for s, series in self._p.items()
                   if d in series and series[d] > 0}
            w = short_rich_weights(dev, self._th)
            realized = 0.0
            k_real = 0
            for s, wt in w.items():
                series = self._p[s]
                if nxt in series and series[d] > 0 and series[nxt] > 0:
                    realized += wt * (series[nxt] / series[d] - 1.0)
                    k_real += 1
            turnover = sum(abs(w.get(s, 0.0) - prev.get(s, 0.0)) for s in set(w) | set(prev))
            rets.append(realized - turnover * self._cost / 2.0)
            dates.append(int(nxt))
            active += k_real
            n += 1
            prev = w
        return StableRunResult(np.asarray(rets, dtype=float), dates,
                               (active / n) if n else 0.0)

    def current_weights(self, as_of: str) -> dict[str, float]:
        """Signed per-symbol target weights for the LATEST day <= `as_of`, reusing the SAME
        `dev`-building + `short_rich_weights` logic as `run()`'s loop. Returns {} when no day on or
        before `as_of` has data (or no name is rich)."""
        day_lim = _as_of_day(as_of)
        all_days = self._all_days()
        # latest panel day <= day_lim via bisect over the memoized sorted day axis (was a full-panel
        # scan+sort every call). bisect_right - 1 == index of the greatest day <= day_lim.
        idx = bisect.bisect_right(all_days, day_lim) - 1
        if idx < 0:
            return {}
        d = all_days[idx]
        dev = {s: series[d] - 1.0 for s, series in self._p.items()
               if d in series and series[d] > 0}
        return short_rich_weights(dev, self._th)
