"""Forward-validation (factory layer 4) — the un-gameable gate before live capital.

A discovered specialist is a HYPOTHESIS. Before it can deploy, it must earn a real-time forward track:
only bars that appear AFTER promotion count, and only on days its regime is live. This can't be
multiple-tested away — it is genuine out-of-sample-in-time. A slot budget caps how many can forward-track
at once (the natural throttle on the whole search). Each cycle:
  mark   — accrue new in-regime forward returns for FORWARD specialists
  review — DEPLOY those whose forward edge held; RETIRE those whose edge failed forward
  promote — fill freed slots with the best DISCOVERED candidates
"""
from __future__ import annotations

import datetime as dt

from pydantic import BaseModel

import numpy as np

from fxhnt.config import Settings
from fxhnt.domain.factory import StrategyStatus
from fxhnt.domain.models import Market
from fxhnt.domain.regime import CusumDetector, VolTrendClassifier
from fxhnt.domain.strategies import get_strategy
from fxhnt.ports.data import DataProvider
from fxhnt.ports.repository import AnalyticalStore, StrategyStore

_MIN_HISTORY = 300


class ForwardReport(BaseModel):
    marked: int = 0
    deployed: int = 0
    retired: int = 0
    promoted: int = 0
    forward_tracking: int = 0


class ForwardValidationService:
    def __init__(self, data: DataProvider, analytical: AnalyticalStore, store: StrategyStore, settings: Settings,
                 *, classifier: VolTrendClassifier | None = None, slots: int = 5, min_forward_days: int = 15,
                 edge_decay_k: float = 0.5, edge_decay_h: float = 5.0, now: dt.datetime | None = None) -> None:
        self._data = data
        self._an = analytical
        self._store = store
        self._s = settings
        self._classifier = classifier or VolTrendClassifier()
        self._slots = slots
        self._min_days = min_forward_days
        self._edge_decay_k = edge_decay_k
        self._edge_decay_h = edge_decay_h
        self._now = now or dt.datetime.now(dt.timezone.utc)

    def _prices(self, market: Market):  # type: ignore[no-untyped-def]
        cached = self._an.load_prices(market)
        if cached is not None and len(cached) >= _MIN_HISTORY:
            return cached
        prices = self._data.fetch(market)
        self._an.save_prices(prices)
        return prices

    def run(self) -> ForwardReport:
        marked = self._mark()
        deployed, retired = self._review()
        promoted = self._promote()
        tracking = len(self._store.list(status=StrategyStatus.FORWARD))
        return ForwardReport(marked=marked, deployed=deployed, retired=retired, promoted=promoted,
                             forward_tracking=tracking)

    def _mark(self) -> int:
        marked = 0
        for rec in self._store.list(status=StrategyStatus.FORWARD):
            regimes = set(rec.specialist_regimes())
            changed = False
            for sym in rec.markets:
                prices = self._prices(Market(symbol=sym, asset_class=rec.asset_class))
                dates, rets = prices.dates, prices.returns()
                labels = self._classifier.classify(prices)
                pos = get_strategy(rec.spec.kind).positions(prices, rec.spec.params)
                anchor = rec.forward_last_date or rec.forward_started
                start = next((i for i, d in enumerate(dates) if d > anchor), len(dates)) if anchor else 1
                for i in range(max(start, 1), len(dates)):
                    if labels[i] in regimes:                     # only earns in its regime
                        r = float(pos[i - 1]) * float(rets[i])
                        rec.forward_days += 1
                        rec.forward_sum += r
                        rec.forward_sumsq += r * r
                        rec.forward_returns.append(r)            # the sequence, for CUSUM edge-decay
                        marked += 1
                if dates:
                    rec.forward_last_date = dates[-1]
                    changed = True
            if changed:
                rec.updated_at = self._now
                self._store.upsert(rec)
        return marked

    def _edge_decayed(self, returns: list[float]) -> bool:
        """CUSUM edge-decay: a structural NEGATIVE break in the forward return sequence means the edge
        died (returns drifting below zero). Catches it with minimal delay — the missing layer between
        Kelly (long-run sizing) and the circuit breaker (tail kill)."""
        if len(returns) < 10:
            return False                                          # warmup
        std = float(np.std(returns))
        det = CusumDetector(0.0, std if std > 1e-12 else 1.0, self._edge_decay_k, self._edge_decay_h)
        return any((b := det.update(r)) is not None and b.direction == "negative" for r in returns)

    def _review(self) -> tuple[int, int]:
        # Promotion to DEPLOYED is the factory_loop's diversification-gated decision; _review only retires.
        deployed = retired = 0
        for rec in self._store.list(status=StrategyStatus.FORWARD):
            if self._edge_decayed(rec.forward_returns):           # CUSUM: early structural-decay retire
                self._store.set_status(rec.strategy_id, StrategyStatus.RETIRED)
                retired += 1
                continue
            if rec.forward_days < self._min_days:
                continue
            if rec.forward_mean() <= 0.0:                        # edge failed forward -> retire, no capital risked
                self._store.set_status(rec.strategy_id, StrategyStatus.RETIRED)
                retired += 1
        return deployed, retired

    def _promote(self) -> int:
        active = (len(self._store.list(status=StrategyStatus.FORWARD))
                  + len(self._store.list(status=StrategyStatus.DEPLOYED)))
        free = max(0, self._slots - active)
        if free <= 0:
            return 0
        discovered = self._store.list(status=StrategyStatus.DISCOVERED)
        discovered.sort(key=lambda r: max((f.dsr for f in r.regime_fits if f.passed), default=0.0), reverse=True)
        promoted = 0
        for rec in discovered[:free]:
            anchor = ""
            if rec.markets:
                prices = self._prices(Market(symbol=rec.markets[0], asset_class=rec.asset_class))
                anchor = prices.dates[-1] if prices.dates else ""
            rec.forward_started = anchor          # forward begins now — only later bars count
            rec.forward_last_date = anchor
            rec.status = StrategyStatus.FORWARD
            rec.updated_at = self._now
            self._store.upsert(rec)
            promoted += 1
        return promoted
