"""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 os
from collections.abc import Callable
from typing import Any

from fxhnt.application.forward_tracker import _today_iso
from fxhnt.domain.strategies import (
    cross_venue,
    funding_carry,
    growth_discipline,
    multistrat,
    sixtyforty,
)
from fxhnt.ports.market_data import (
    BinanceFundingClient,
    CrossVenueFundingClient,
    DailyBarClient,
)

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


class SixtyFortyStrategy:
    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


class MultiStratStrategy:
    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 multistrat.INSTRUMENTS}
        return multistrat.book_series(closes), extra


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


class FundingCarryStrategy:
    """Live-booking delta-neutral funding-harvest strategy (faithful port of crypto_funding_paper.py).

    Each day it fetches the universe's funding + volume, requalifies (hysteresis), books today's realized
    carry on the PRIOR positions net of turnover cost, then rebalances to a fresh equal-weight book stored
    in `extra["positions"]`. On the FIRST run the prior book is empty so realized/cost are 0 and the tracker
    freezes (books nothing) — it just seeds initial positions, exactly like cmd_run on a fresh state."""

    def __init__(self, client: BinanceFundingClient, clock: Callable[[], str] = _today_iso) -> None:
        self._client = client
        self._clock = clock

    def advance(self, last_date: str | None, extra: dict[str, Any]) -> tuple[list[tuple[str, float]], dict[str, Any]]:
        prev: dict[str, float] = extra.get("positions", {})
        liq, tf30, last24 = self._client.scan(set(prev))

        q = funding_carry.qualify(liq, tf30, prev)
        n = len(q)
        newpos = {c: 1.0 / n for c in q} if n else {}
        net = funding_carry.book_return(prev, last24, newpos)

        return [(self._clock(), net)], {"positions": newpos}


class MomentumVrpStrategy:
    """Live-booking surfer PoC: crypto cross-sectional residual-momentum book + tail-managed VRP sleeve
    (faithful port of foxhunt scripts/surfer/surfer_poc.py `paper`). Each run fetches the live Binance perp
    panel, marks the prior two-sleeve book to today's prices + funding, scales the VRP sleeve to the
    momentum-vol risk budget (calibrated once from the cached crypto_pit history), books the combined return
    for today, then rebalances on the weekly boundary — all carried in `extra`. On the FIRST run the prior
    book is empty so no mark happens (comb_ret 0, today == frozen inception) and the tracker freezes, just
    seeding positions/prices/sleeve-equities. `today` comes from the panel's latest day (`int(days[-1])`),
    not the wall clock, exactly like the original."""

    _EXTRA_KEYS = ("inception", "last_mark_day", "positions", "prices", "equity", "log",
                   "vrp_equity", "combined_equity", "vrp_mu", "vrp_vu")

    def __init__(self, cfg: dict[str, Any] | None = None) -> None:
        # import here so torch/vendored surfer is only required when this strategy is actually used.
        from fxhnt.vendor.surfer import surfer_poc

        self._poc = surfer_poc
        self._cfg = cfg if cfg is not None else surfer_poc.CFG

    def _seed_state(self, extra: dict[str, Any]) -> dict[str, Any]:
        """Reconstruct surfer_poc's working state dict from the tracker's opaque `extra` (or a fresh state)."""
        st: dict[str, Any] = dict(inception=None, last_mark_day=None, positions={}, prices={},
                                  equity=1.0, log=[])
        for k in self._EXTRA_KEYS:
            if k in extra:
                st[k] = extra[k]
        return st

    @staticmethod
    def _iso(epoch_day: int) -> str:
        import datetime as _dt

        return (_dt.datetime(1970, 1, 1, tzinfo=_dt.timezone.utc)
                + _dt.timedelta(days=int(epoch_day))).strftime("%Y-%m-%d")

    def advance(self, last_date: str | None, extra: dict[str, Any]) -> tuple[list[tuple[str, float]], dict[str, Any]]:
        st = self._seed_state(extra)
        comb_ret, today_epoch, new_st, _info = self._poc.paper_step(self._cfg, st)
        iso_today = self._iso(today_epoch)
        updated = {k: new_st[k] for k in self._EXTRA_KEYS if k in new_st}
        return [(iso_today, float(comb_ret))], updated


class CrossVenueStrategy:
    """Live-booking price-neutral cross-venue funding-arb strategy (faithful port of
    cross_venue_funding.py). Each day it fetches per-venue funding + volume, computes per-coin spreads
    (max-min daily funding across liquid, non-distress venues), requalifies via ENTRY/EXIT hysteresis +
    TOPK, books today's realized carry on the PRIOR positions net of turnover cost, then rebalances to a
    fresh equal-weight book stored in `extra["positions"]`. On the FIRST run the prior book is empty so
    realized/cost are 0 and the tracker freezes (books nothing) — it just seeds initial positions, exactly
    like cmd_run on a fresh state."""

    def __init__(self, client: CrossVenueFundingClient, clock: Callable[[], str] = _today_iso) -> None:
        self._client = client
        self._clock = clock

    def advance(self, last_date: str | None, extra: dict[str, Any]) -> tuple[list[tuple[str, float]], dict[str, Any]]:
        prev: dict[str, float] = extra.get("positions", {})
        funding, volume = self._client.snapshot()
        rows = cross_venue.compute_spreads(funding, volume)
        cur = {r["coin"]: r["spread"] for r in rows}

        held = cross_venue.select(cur, prev)
        n = len(held)
        newpos = {c: 1.0 / n for c in held} if n else {}
        net = cross_venue.book_return(prev, cur, newpos)

        return [(self._clock(), net)], {"positions": newpos}
