"""Research use-case — orchestrates the vertical slice: data → backtest → IS/OOS gauntlet → persist.

Depends only on PORTS (DataProvider, OperationalRepository, AnalyticalStore), injected by the caller
(the CLI composition root). Holds no infrastructure knowledge — fully unit-testable with fakes.
"""
from __future__ import annotations

import datetime as dt

from fxhnt.config import Settings
from fxhnt.domain.backtest import run_backtest
from fxhnt.domain.gauntlet import evaluate
from fxhnt.domain.models import Market, ResearchRun, StrategySpec, run_id
from fxhnt.domain.strategies import get_strategy
from fxhnt.ports.data import DataProvider
from fxhnt.ports.repository import AnalyticalStore, OperationalRepository

_MIN_HISTORY = 300


class ResearchService:
    def __init__(self, data: DataProvider, operational: OperationalRepository,
                 analytical: AnalyticalStore, settings: Settings) -> None:
        self._data = data
        self._op = operational
        self._an = analytical
        self._s = settings

    def evaluate_candidate(self, market: Market, spec: StrategySpec, *,
                           n_trials: int = 1, sr_variance: float = 0.0, persist: bool = True) -> ResearchRun:
        # 1. data — cache-first in the analytical store, fall back to the provider
        prices = self._an.load_prices(market)
        if prices is None or len(prices) < _MIN_HISTORY:
            prices = self._data.fetch(market)
            self._an.save_prices(prices)

        # 2. backtest (net of costs)
        bt = run_backtest(prices, spec, cost_bps_per_turnover=self._s.cost_bps_per_turnover)

        # 3. in-sample / out-of-sample split
        r = bt.returns
        split = int((1.0 - self._s.gauntlet.oos_fraction) * len(r))

        # 4. the gauntlet (n_trials/sr_variance carry the FULL search size — set by the discovery layer)
        strategy = get_strategy(spec.kind)
        verdict = evaluate(
            r[:split], r[split:], n_trials=n_trials, sr_variance=sr_variance,
            dsr_min=self._s.gauntlet.dsr_min, oos_min_sharpe=self._s.gauntlet.oos_min_sharpe,
            max_is_oos_decay=self._s.gauntlet.max_is_oos_decay,
            has_economic_rationale=bool(getattr(strategy, "rationale", "")),
        )

        run = ResearchRun(
            run_id=run_id(market, spec), market=market, spec=spec, stats=bt.stats,
            verdict=verdict, n_trials=n_trials, created_at=dt.datetime.now(dt.timezone.utc),
        )
        if persist:
            self._op.save_run(run)
            self._an.save_returns(run.run_id, prices.dates, r)
        return run
