"""Automated backtest-to-deploy pipeline: research, validate, deploy, monitor."""

from __future__ import annotations

import logging
import time
from collections import deque
from dataclasses import dataclass, field
from typing import Any, Callable

logger = logging.getLogger("horizon.fund.autopilot")


@dataclass(frozen=True)
class DeployCriteria:
    """Minimum criteria for deploying a strategy."""
    min_sharpe: float = 1.0
    min_trades: int = 30
    max_drawdown_pct: float = 20.0
    min_profit_factor: float = 1.2
    max_robustness_p: float = 0.05
    require_walk_forward: bool = True
    min_wf_positive_pct: float = 0.6

    # Overfitting gates (used by BiasGuard)
    max_pbo: float = 0.50  # Max probability of backtest overfitting
    min_deflated_sharpe: float = 1.0  # Deflated Sharpe after multiple testing correction
    min_oos_sharpe: float = 0.5  # Min out-of-sample Sharpe ratio


@dataclass
class DeployCandidate:
    """A strategy candidate evaluated for deployment."""
    market_id: str
    exchange: str
    strategy_type: str
    pipeline: list[Callable] | None = None
    params: dict[str, Any] = field(default_factory=dict)
    edge_estimate: float = 0.0
    confidence: float = 0.0

    # Backtest results
    backtest_sharpe: float = 0.0
    backtest_trades: int = 0
    backtest_drawdown_pct: float = 0.0
    backtest_profit_factor: float = 0.0
    robustness_p_value: float = 1.0
    walk_forward_positive_pct: float = 0.0

    # Overfitting metrics (from CPCV / multiple testing correction)
    pbo: float | None = None  # Probability of backtest overfitting
    deflated_sharpe: float | None = None  # Sharpe adjusted for multiple trials
    oos_sharpe: float | None = None  # Out-of-sample Sharpe
    n_trials: int = 1  # Number of strategy variations tested

    # Hypothesis tracking
    hypothesis_id: str | None = None

    # Decision
    passes_criteria: bool = False
    rejection_reasons: list[str] = field(default_factory=list)
    bias_alerts: list[str] = field(default_factory=list)
    deployed: bool = False
    deployed_at: float = 0.0


class Autopilot:
    """Automated strategy validation and deployment pipeline.

    Takes opportunities from the ResearchPipeline, backtests them
    using horizon.backtest(), validates with robustness tests, and
    deploys to the fund via FundManager if criteria are met.

    Includes overfitting prevention via BiasGuard: PBO check,
    deflated Sharpe gate, OOS validation, confirmation bias detection,
    and small-sample rejection.

    All decisions are logged for audit.
    """

    def __init__(
        self,
        criteria: DeployCriteria | None = None,
        paper_first: bool = True,
        max_deploys_per_cycle: int = 3,
        bias_guard: Any | None = None,
        decay_tracker: Any | None = None,
    ) -> None:
        self._criteria = criteria or DeployCriteria()
        self._paper_first = paper_first
        self._max_deploys = max_deploys_per_cycle
        self._candidates: deque[DeployCandidate] = deque(maxlen=10_000)
        self._deployed: deque[DeployCandidate] = deque(maxlen=10_000)
        self._bias_guard = bias_guard
        self._decay_tracker = decay_tracker

    def evaluate(
        self,
        candidate: DeployCandidate,
        backtest_fn: Callable | None = None,
        robustness_fn: Callable | None = None,
    ) -> DeployCandidate:
        """Evaluate a candidate for deployment.

        Args:
            candidate: The candidate to evaluate.
            backtest_fn: Optional function(candidate) -> BacktestResult.
            robustness_fn: Optional function(backtest_result) -> RobustnessReport.

        Returns:
            The candidate with evaluation fields populated.
        """
        reasons: list[str] = []

        # Run backtest if function provided
        if backtest_fn is not None:
            try:
                result = backtest_fn(candidate)
                metrics = result.metrics if hasattr(result, "metrics") else None

                if metrics is not None:
                    candidate.backtest_sharpe = getattr(metrics, "sharpe", 0.0)
                    candidate.backtest_trades = getattr(metrics, "total_trades", 0)
                    candidate.backtest_drawdown_pct = abs(getattr(metrics, "max_drawdown_pct", 0.0))
                    candidate.backtest_profit_factor = getattr(metrics, "profit_factor", 0.0)

                # Run robustness if backtest passed basic checks
                if robustness_fn is not None and candidate.backtest_trades >= self._criteria.min_trades:
                    try:
                        rob = robustness_fn(result)
                        if hasattr(rob, "permutation") and rob.permutation is not None:
                            candidate.robustness_p_value = rob.permutation.p_value
                    except Exception as e:
                        logger.debug("Robustness test failed: %s", e)
                        candidate.robustness_p_value = 1.0

            except Exception as e:
                logger.warning("Backtest failed for %s: %s", candidate.market_id, e)
                reasons.append(f"backtest_failed: {e}")
                candidate.rejection_reasons = reasons
                self._candidates.append(candidate)
                return candidate

        # Check criteria
        c = self._criteria
        if candidate.backtest_sharpe < c.min_sharpe:
            reasons.append(f"sharpe={candidate.backtest_sharpe:.2f} < {c.min_sharpe}")
        if candidate.backtest_trades < c.min_trades:
            reasons.append(f"trades={candidate.backtest_trades} < {c.min_trades}")
        if candidate.backtest_drawdown_pct > c.max_drawdown_pct:
            reasons.append(f"drawdown={candidate.backtest_drawdown_pct:.1f}% > {c.max_drawdown_pct}%")
        if candidate.backtest_profit_factor < c.min_profit_factor:
            reasons.append(f"profit_factor={candidate.backtest_profit_factor:.2f} < {c.min_profit_factor}")
        if candidate.robustness_p_value > c.max_robustness_p:
            reasons.append(f"p_value={candidate.robustness_p_value:.3f} > {c.max_robustness_p}")

        # BiasGuard overfitting checks
        bias_alerts: list[str] = []
        if self._bias_guard is not None:
            from horizon.fund._bias_guard import BiasGuard
            guard: BiasGuard = self._bias_guard
            alerts = guard.check_deploy(
                strategy_type=candidate.strategy_type,
                market_id=candidate.market_id,
                backtest_sharpe=candidate.backtest_sharpe,
                n_trades=candidate.backtest_trades,
                pbo=candidate.pbo,
                deflated_sharpe=candidate.deflated_sharpe,
                oos_sharpe=candidate.oos_sharpe,
                n_trials=candidate.n_trials,
            )
            for alert in alerts:
                bias_alerts.append(f"[{alert.severity}] {alert.bias_type}: {alert.message}")
                if alert.severity == "block":
                    reasons.append(f"bias_block: {alert.bias_type} - {alert.message}")
            candidate.bias_alerts = bias_alerts

        candidate.passes_criteria = len(reasons) == 0
        candidate.rejection_reasons = reasons
        self._candidates.append(candidate)

        if candidate.passes_criteria:
            logger.info(
                "Candidate PASSES: %s sharpe=%.2f trades=%d dd=%.1f%%",
                candidate.market_id, candidate.backtest_sharpe,
                candidate.backtest_trades, candidate.backtest_drawdown_pct,
            )
        else:
            logger.info(
                "Candidate REJECTED: %s reasons=%s",
                candidate.market_id, reasons,
            )

        return candidate

    def deploy_candidates(
        self,
        fund_manager: Any,
        candidates: list[DeployCandidate] | None = None,
    ) -> list[DeployCandidate]:
        """Deploy passing candidates to the fund.

        Args:
            fund_manager: FundManager instance.
            candidates: Candidates to deploy (defaults to all passing).

        Returns:
            List of successfully deployed candidates.
        """
        from horizon.fund._types import StrategyConfig

        if candidates is None:
            candidates = [c for c in self._candidates if c.passes_criteria and not c.deployed]

        deployed: list[DeployCandidate] = []
        for candidate in candidates[: self._max_deploys]:
            if candidate.pipeline is None:
                logger.warning("Candidate %s has no pipeline, skipping", candidate.market_id)
                continue

            mode = "paper" if self._paper_first else "live"
            config = StrategyConfig(
                name=f"auto_{candidate.strategy_type}_{candidate.market_id[:20]}",
                pipeline=candidate.pipeline,
                markets=[candidate.market_id],
                mode=mode,
                params=candidate.params,
            )

            try:
                fund_manager.add_strategy(config)
                candidate.deployed = True
                candidate.deployed_at = time.time()
                deployed.append(candidate)
                self._deployed.append(candidate)
                logger.info("Auto-deployed: %s on %s (%s mode)", config.name, candidate.market_id, mode)
            except Exception as e:
                logger.error("Auto-deploy failed for %s: %s", candidate.market_id, e)

        return deployed

    def passing_candidates(self) -> list[DeployCandidate]:
        """Return candidates that passed all criteria."""
        return [c for c in self._candidates if c.passes_criteria]

    def rejected_candidates(self) -> list[DeployCandidate]:
        """Return rejected candidates with reasons."""
        return [c for c in self._candidates if not c.passes_criteria]

    def deployed_strategies(self) -> list[DeployCandidate]:
        """Return all deployed candidates."""
        return list(self._deployed)

    def all_candidates(self) -> list[DeployCandidate]:
        return list(self._candidates)

    def check_deployed(
        self,
        strategy_statuses: list[Any],
    ) -> list[str]:
        """Check deployed strategies and recommend retirements.

        Args:
            strategy_statuses: List of StrategyStatus objects from controller.

        Returns:
            List of strategy names recommended for retirement.
        """
        to_retire: list[str] = []
        now = time.time()

        deployed_names = {
            f"auto_{c.strategy_type}_{c.market_id[:20]}"
            for c in self._deployed
        }

        for status in strategy_statuses:
            if status.name not in deployed_names:
                continue
            # Check uptime (at least 14 days before retiring)
            if status.uptime_secs < 14 * 86400:
                continue
            # Retire if negative PnL after 14+ days
            if status.pnl < 0:
                to_retire.append(status.name)
                logger.info(
                    "Auto-retire candidate: '%s' pnl=%.2f after %.1f days",
                    status.name, status.pnl, status.uptime_secs / 86400,
                )
            # Retire if drawdown exceeds strategy limit
            elif status.drawdown_pct > self._criteria.max_drawdown_pct:
                to_retire.append(status.name)
                logger.info(
                    "Auto-retire candidate: '%s' drawdown=%.1f%% exceeds %.1f%%",
                    status.name, status.drawdown_pct, self._criteria.max_drawdown_pct,
                )
            # Retire if alpha decay tracker says edge is gone
            elif self._decay_tracker is not None:
                try:
                    if self._decay_tracker.should_retire(status.name):
                        to_retire.append(status.name)
                        logger.info(
                            "Auto-retire candidate: '%s' edge decayed below threshold",
                            status.name,
                        )
                except Exception:
                    pass

        return to_retire

    def clear(self) -> None:
        """Clear candidate history (not deployed history)."""
        self._candidates.clear()
