"""The paper-forward ForwardStrategy services: each composes a market-data port + a pure domain module
and yields candidate (date, ret) rows for the generic ForwardTracker. Recomputable strategies return the
full series each run (the tracker books only the new tail); live-booking strategies book today + carry
positions in `extra`."""
from __future__ import annotations

import math
import os
from typing import Any

from fxhnt.domain.strategies import (
    growth_discipline,
    multistrat,
    sixtyforty,
)
from fxhnt.ports.market_data import DailyBarClient

_SIXTYFORTY_WEIGHTS = [("SPY", 0.6), ("IEF", 0.4)]


class SixtyFortyStrategy:
    """Pure 60/40 (SPY/IEF) BENCHMARK reference line — NOT an edge, no capital rides it (research tier,
    loose absolute gate). DOCUMENTED spec §4 PIT exemption: its registry track is `record_mode="recompute"`
    but `bars.adj_closes` reads LIVE Tiingo total-return adjClose, which is retroactively revised on
    corporate actions (not a frozen point-in-time snapshot) — a violation of the general recompute-track PIT
    rule. This is deliberate: full PIT-snapshotting is out of scope for a benchmark reference, and adjClose
    revisions are immaterial to the 60/40 line. See the `sixtyforty` entry in `registry.py` for the same
    note on the definition side."""
    def __init__(self, bars: DailyBarClient) -> None:
        self._bars = bars

    def advance(self, last_date: str | None, extra: dict[str, Any]) -> tuple[list[tuple[str, float]], dict[str, Any]]:
        closes = {s: self._bars.adj_closes(s) for s, _ in _SIXTYFORTY_WEIGHTS}
        return sixtyforty.daily_returns(closes, _SIXTYFORTY_WEIGHTS), extra

    def current_weights(self, as_of: str) -> dict[str, float]:
        """Fixed 60/40 target weights — the SAME `_SIXTYFORTY_WEIGHTS` the daily-return path rebalances
        to. Constant (date-independent)."""
        return {s: w for s, w in _SIXTYFORTY_WEIGHTS}


class MultiStratStrategy:
    def __init__(self, bars: DailyBarClient, instruments: list[str] | None = None,
                 *, drop_incomplete_today: bool = True) -> None:
        self._bars = bars
        # default = the original 6-stream port; the fxhnt fund track passes multistrat.FUND_INSTRUMENTS
        # (5 pure-equity streams, BTC-USD dropped — see FUND_INSTRUMENTS for why).
        self._instruments = instruments if instruments is not None else multistrat.INSTRUMENTS
        # The nightly `multistrat_nav` asset (23:30 UTC, after the US close froze today's FINAL close) passes
        # False so today is BOOKED — dropping it left the forward track permanently one trading day stale.
        # The exec path (14:35 UTC, market open) keeps the default True. See multistrat._build.
        self._drop_incomplete_today = drop_incomplete_today

    def advance(self, last_date: str | None, extra: dict[str, Any]) -> tuple[list[tuple[str, float]], dict[str, Any]]:
        closes = {s: self._bars.adj_closes(s) for s in self._instruments}
        return multistrat.book_series(
            closes, self._instruments, drop_incomplete_today=self._drop_incomplete_today), extra


class LeveredShadowStrategy:
    """OBSERVE-ONLY levered shadow of a base recompute-replay strategy (e.g. `MultiStratStrategy`): the SAME
    daily return series, LEVERED to the fund's vol target (capped at `max_leverage`) and charged the financing
    drag on the borrowed `(L-1)` portion. Return-defined — no positions, NEVER executed. It previews running
    the risk-parity book levered, which an unlevered 1x risk-parity book (bond-heavy, sitting well under its
    vol target) leaves on the table.

    `L = min(max_leverage, target_vol / realized_ann_vol)`, floored at 1.0 (a shadow only levers UP, never
    de-levers below the base). Recomputed from the FULL series each run — a static per-run lever (like the
    Bybit levered shadow's static sleeve shape), recalibrating to the book's own vol as history grows. The
    daily financing drag `= (financing_bps/1e4)/252 · (L-1)` is the HONEST cost of the borrowed leg, so the
    shadow can never overstate the lift: leverage is not free (the moat is cheap financing, not zero)."""
    def __init__(self, base: Any, *, target_vol: float, financing_bps: float, max_leverage: float) -> None:
        self._base = base
        self._target_vol = target_vol
        self._financing_bps = financing_bps
        self._max_leverage = max_leverage

    def advance(self, last_date: str | None, extra: dict[str, Any]) -> tuple[list[tuple[str, float]], dict[str, Any]]:
        rows, new_extra = self._base.advance(last_date, extra)
        if len(rows) < 2:
            return rows, new_extra
        rets = [r for _, r in rows]
        mean = sum(rets) / len(rets)
        ann_vol = math.sqrt(sum((r - mean) ** 2 for r in rets) / (len(rets) - 1)) * math.sqrt(252)
        lev = min(self._max_leverage, self._target_vol / ann_vol) if ann_vol > 0 else 1.0
        lev = max(1.0, lev)   # observe-only shadow LEVERS UP only — never de-levers below the base book
        daily_financing = (self._financing_bps / 1e4) / 252.0 * (lev - 1.0)
        return [(d, lev * r - daily_financing) for d, r in rows], new_extra


# The financing-rate band shown alongside the levered shadow — grounded in IBKR's real USD margin schedule
# (2026-07: Fed funds effective 3.63% + a tiered spread that shrinks with balance): ~5.83% IBKR Pro tier-1
# (<$100k), ~5.3% at $100k-1M, 6.83% IBKR Lite; the low end (~1-3%) is institutional/portfolio-margin. The
# verdict hinges on this rate, so it is shown as a BAND, not a single assumed number.
FINANCING_SENSITIVITY_BPS: tuple[float, ...] = (100.0, 300.0, 530.0, 583.0, 683.0)


def leverage_financing_sensitivity(base_daily_returns: list[float], *, target_vol: float,
                                   max_leverage: float, rates_bps: tuple[float, ...] = FINANCING_SENSITIVITY_BPS,
                                   current_bps: float | None = None) -> dict[str, Any]:
    """The levered shadow's outcome ACROSS a range of financing rates — the honest sensitivity band, since the
    verdict (the levered Sharpe) hinges on the financing rate, which is an IBKR benchmark+tier ASSUMPTION, not
    a fixed fact. One leverage `L` (set by base vol -> target vol) applies to every rate; only the financing
    drag differs. Returns per rate `{rate_pct, ann_return_pct, ann_vol_pct, sharpe, is_current}` plus the
    break-even rate (= the base book's own ann return — above it, leverage SUBTRACTS return) and the base 1x
    metrics for side-by-side comparison. `{}`-ish (empty rows) for a too-short series."""
    rets = list(base_daily_returns)
    if len(rets) < 2:
        return {"rows": [], "leverage": 1.0, "break_even_pct": 0.0, "base_ann_return_pct": 0.0,
                "base_ann_vol_pct": 0.0, "base_sharpe": 0.0}
    n = len(rets)
    mean = sum(rets) / n
    ann_vol = math.sqrt(sum((r - mean) ** 2 for r in rets) / (n - 1)) * math.sqrt(252)
    base_ann = mean * 252
    lev = max(1.0, min(max_leverage, target_vol / ann_vol) if ann_vol > 0 else 1.0)
    lev_vol = lev * ann_vol
    rows = []
    for f_bps in sorted(set(rates_bps) | ({current_bps} if current_bps is not None else set())):
        f = f_bps / 1e4
        lev_ann = lev * base_ann - (lev - 1.0) * f
        rows.append({"rate_pct": f_bps / 100.0, "ann_return_pct": lev_ann * 100,
                     "ann_vol_pct": lev_vol * 100, "sharpe": (lev_ann / lev_vol) if lev_vol else 0.0,
                     "adds": lev_ann > base_ann,
                     "is_current": current_bps is not None and abs(f_bps - current_bps) < 1e-9})
    return {"rows": rows, "leverage": lev, "break_even_pct": base_ann * 100,
            "base_ann_return_pct": base_ann * 100, "base_ann_vol_pct": ann_vol * 100,
            "base_sharpe": (base_ann / ann_vol) if ann_vol else 0.0}


class GrowthDisciplineStrategy:
    def __init__(self, bars: DailyBarClient) -> None:
        self._bars = bars

    def advance(self, last_date: str | None, extra: dict[str, Any]) -> tuple[list[tuple[str, float]], dict[str, Any]]:
        w = float(os.environ.get("GD_EQUITY_WEIGHT", "0.70"))
        qqq = self._bars.adj_closes("QQQ")
        closes = {s: self._bars.adj_closes(s) for s in multistrat.INSTRUMENTS}
        return growth_discipline.daily_returns(qqq, closes, w), extra
