"""Strategy-agnostic forward-validation tracker — the only ground truth before capital.

`ForwardTracker` freezes any `ForwardStrategy` port at INCEPTION and thereafter books only realized
returns of days STRICTLY AFTER it — a genuine out-of-sample-in-time paper track (no capital risked).
The combined book is one (recomputable) strategy on it; in-sample backtest Sharpe means nothing until
the forward record confirms it.
"""
from __future__ import annotations

import datetime as _dt
import json
import os
from dataclasses import dataclass
from typing import Any, Protocol

import numpy as np

from fxhnt.application.combined_book import CombinedBook
from fxhnt.domain.gauntlet import per_period_sharpe

_ANN = np.sqrt(365.0)


@dataclass
class ForwardStatus:
    inception: str
    last_date: str
    forward_days: int
    forward_return_pct: float
    forward_sharpe: float
    forward_max_drawdown: float
    booked_today: int


class ForwardStrategy(Protocol):
    def advance(self, last_date: str | None, extra: dict[str, Any]) -> tuple[list[tuple[str, float]], dict[str, Any]]:
        """Return candidate (ISO-date, fractional-return) rows the strategy knows now, plus its updated
        carry-state. The tracker books only rows strictly after last_date and persists `extra` opaquely."""
        ...


def _today_iso() -> str:
    return _dt.datetime.now(_dt.timezone.utc).strftime("%Y-%m-%d")


class ForwardTracker:
    """Strategy-agnostic forward-validation tracker. Freezes inception on first run (books nothing — no
    backfill), then books only days strictly after last_date. Owns the days-list state file."""

    def __init__(self, strategy: ForwardStrategy, state_path: str) -> None:
        self._strategy = strategy
        self._path = state_path

    def _load(self) -> dict | None:
        if os.path.exists(self._path):
            with open(self._path) as f:
                return json.load(f)
        return None

    def _save(self, state: dict) -> None:
        os.makedirs(os.path.dirname(self._path) or ".", exist_ok=True)
        with open(self._path, "w") as f:
            json.dump(state, f, indent=2)

    def step(self) -> ForwardStatus:
        loaded = self._load()
        prev_last = None if loaded is None else (loaded["last_date"] or None)
        prev_extra = {} if loaded is None else loaded.get("extra", {})
        rows, new_extra = self._strategy.advance(prev_last, prev_extra)
        rows = sorted(rows, key=lambda x: x[0])

        booked = 0
        if loaded is None:
            latest = rows[-1][0] if rows else _today_iso()
            state: dict[str, Any] = {"inception": latest, "last_date": latest, "nav": 1.0, "days": [], "extra": new_extra}
        else:
            state = loaded
            for d, r in rows:
                if d > state["last_date"]:
                    state["nav"] *= (1.0 + r)
                    state["days"].append({"date": d, "ret": r, "nav": state["nav"]})
                    state["last_date"] = d
                    booked += 1
            state["extra"] = new_extra
        self._save(state)

        rets = np.array([row["ret"] for row in state["days"]])
        nav = np.array([row["nav"] for row in state["days"]])
        sharpe = float(per_period_sharpe(rets) * _ANN) if len(rets) >= 3 else 0.0
        mdd = float((nav / np.maximum.accumulate(nav) - 1.0).min()) if len(nav) else 0.0
        return ForwardStatus(
            inception=state["inception"], last_date=state["last_date"], forward_days=len(state["days"]),
            forward_return_pct=100.0 * (state["nav"] - 1.0), forward_sharpe=sharpe,
            forward_max_drawdown=mdd, booked_today=booked)


class CombinedBookStrategy:
    """Recomputable-series ForwardStrategy over a CombinedBook."""
    def __init__(self, book: CombinedBook) -> None:
        self._book = book

    def advance(self, last_date: str | None, extra: dict[str, Any]) -> tuple[list[tuple[str, float]], dict[str, Any]]:
        _, dates, combo = self._book.build()
        rows = [(d, float(r)) for d, r in zip(dates, combo) if np.isfinite(r)]
        return rows, extra


def CombinedBookForwardTracker(book: CombinedBook, state_path: str) -> ForwardTracker:  # noqa: N802
    """Back-compat factory: the combined book is just a recomputable ForwardStrategy."""
    return ForwardTracker(CombinedBookStrategy(book), state_path)
