"""Fleet orchestrator (factory layer 3) — runs the standing research force.

A Territory is a hunter's mandate: a strategy family over a market universe with a parameter grid. The
orchestrator searches each territory, evaluates every candidate REGIME-CONDITIONALLY (layer 1), and writes
the regime-specialists it finds into the lifecycle store (layer 2) with global trial accounting. The
gauntlet here is the CHEAP pre-filter; forward-validation (layer 4) is the real one.

Hunters run sequentially today (the local model + GPU serialize anyway); the interface is built so they
can run concurrently and be agent-proposed later — each hunter is one specialist 'agent'.
"""
from __future__ import annotations

import datetime as dt

from pydantic import BaseModel, Field

from fxhnt.config import Settings
from fxhnt.domain.backtest import run_backtest
from fxhnt.domain.factory import RegimeFit, StrategyRecord, StrategyStatus, candidate_key, strategy_id
from fxhnt.domain.models import AssetClass, Market, StrategySpec
from fxhnt.domain.regime import VolTrendClassifier, evaluate_by_regime
from fxhnt.ports.data import DataProvider
from fxhnt.ports.repository import AnalyticalStore, StrategyStore

_MIN_HISTORY = 300


class Territory(BaseModel):
    name: str                      # the hunter, e.g. "trend-hunter"
    kind: str                      # strategy kind
    markets: list[str]
    asset_class: AssetClass = AssetClass.FUTURE
    param_grid: list[dict] = Field(default_factory=lambda: [{"window": 200.0}])


class FleetReport(BaseModel):
    territories: int
    candidates_tested: int
    specialists_found: int
    by_regime: dict[str, int] = Field(default_factory=dict)
    global_trials: int = 0


class FleetOrchestrator:
    def __init__(self, data: DataProvider, analytical: AnalyticalStore, store: StrategyStore,
                 settings: Settings, *, classifier: VolTrendClassifier | None = None,
                 now: dt.datetime | None = None) -> None:
        self._data = data
        self._an = analytical
        self._store = store
        self._s = settings
        self._classifier = classifier or VolTrendClassifier()
        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 hunt(self, territories: list[Territory]) -> FleetReport:
        g = self._s.gauntlet
        tested = 0
        found = 0
        by_regime: dict[str, int] = {}

        for terr in territories:
            candidates = [(sym, params) for sym in terr.markets for params in terr.param_grid]
            local_eff = max(1, len(candidates))  # within-territory multiple-testing pre-filter
            for sym, params in candidates:
                cand_key = candidate_key(sym.upper(), terr.kind, params)
                if self._store.is_seen(cand_key):
                    continue  # tested in a prior cycle — don't re-count the global trial or re-do the work
                tested += 1
                self._store.mark_seen(cand_key)
                market = Market(symbol=sym.upper(), asset_class=terr.asset_class)
                spec = StrategySpec(kind=terr.kind, params=dict(params))
                # Thread the HONEST GLOBAL trial count into the gauntlet bar so the deflated-Sharpe
                # threshold scales with the full cumulative search, not just this territory's candidates.
                # pre-commit: store.total_trials() reflects prior cycles; tested is the new unseen
                # candidates evaluated SO FAR in THIS cycle (added to store AFTER the loop).
                n_trials = self._store.total_trials() + local_eff
                try:
                    prices = self._prices(market)
                    bt = run_backtest(prices, spec, self._s.cost_bps_per_turnover)
                    labels = self._classifier.classify(prices)
                    verdicts = evaluate_by_regime(
                        bt.returns, labels, oos_fraction=g.oos_fraction, n_trials=n_trials,
                        dsr_min=g.dsr_min, oos_min_sharpe=g.oos_min_sharpe, max_is_oos_decay=g.max_is_oos_decay,
                    )
                except Exception:
                    continue
                fits = [RegimeFit(regime=r, dsr=v.dsr, is_sharpe=v.is_sharpe, oos_sharpe=v.oos_sharpe, passed=v.passed)
                        for r, v in verdicts.items()]
                if not any(f.passed for f in fits):
                    continue
                found += 1
                for f in fits:
                    if f.passed:
                        by_regime[f.regime] = by_regime.get(f.regime, 0) + 1
                self._store.upsert(StrategyRecord(
                    strategy_id=strategy_id([sym.upper()], terr.asset_class, spec), markets=[sym.upper()],
                    asset_class=terr.asset_class, spec=spec, regime_fits=fits, status=StrategyStatus.DISCOVERED,
                    discovered_at=self._now, updated_at=self._now,
                    n_trials_at_discovery=self._store.total_trials() + tested, source=terr.name,
                ))

        self._store.add_trials(tested)  # honest global accounting
        return FleetReport(territories=len(territories), candidates_tested=tested, specialists_found=found,
                           by_regime=by_regime, global_trials=self._store.total_trials())
