"""Adaptive execution: auto-tune strategy parameters based on execution quality feedback."""

from __future__ import annotations

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

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


@dataclass(frozen=True)
class ParameterAdjustment:
    """A parameter adjustment decision."""
    strategy: str
    parameter: str
    old_value: float
    new_value: float
    reason: str
    timestamp: float


@dataclass
class AdaptiveConfig:
    """Configuration for adaptive parameter tuning."""
    enabled: bool = True
    min_fills_before_adjust: int = 20
    adjustment_interval_secs: float = 300.0
    max_spread_adjustment_pct: float = 20.0
    max_size_adjustment_pct: float = 30.0
    fill_rate_low_threshold: float = 0.5
    fill_rate_high_threshold: float = 0.9
    adverse_selection_threshold: float = 0.01
    slippage_threshold_bps: float = 20.0


class AdaptiveExecutionTuner:
    """Auto-tunes strategy execution parameters based on quality feedback.

    Reads execution quality metrics from ExecutionQualityTracker
    and adjusts strategy parameters via Engine runtime param updates.

    Tunable parameters:
    - spread_width: widened on high adverse selection, tightened on low fill rate
    - order_size: reduced on high slippage/impact, increased on good quality
    - aggression: timing aggressiveness for TWAP/execution algos

    Changes are applied via engine.set_param() for hot-reload.
    """

    def __init__(self, config: AdaptiveConfig | None = None) -> None:
        self._config = config or AdaptiveConfig()
        self._adjustments: deque[ParameterAdjustment] = deque(maxlen=10_000)
        self._last_adjust: dict[str, float] = {}
        self._lock = threading.Lock()
        self._current_params: dict[str, dict[str, float]] = {}

    def tune(
        self,
        strategy: str,
        metrics: dict[str, float],
        engine: Any = None,
    ) -> list[ParameterAdjustment]:
        """Evaluate metrics and apply parameter adjustments if needed.

        Args:
            strategy: Strategy name.
            metrics: Execution metrics from ExecutionQualityTracker.strategy_metrics().
            engine: Engine instance (for applying param changes).

        Returns:
            List of adjustments applied.
        """
        if not self._config.enabled:
            return []

        now = time.time()

        # Rate limit adjustments
        with self._lock:
            last = self._last_adjust.get(strategy, 0.0)
            if now - last < self._config.adjustment_interval_secs:
                return []

        total_fills = metrics.get("total_fills", 0)
        if total_fills < self._config.min_fills_before_adjust:
            return []

        fill_rate = metrics.get("fill_rate", 0.0)
        slippage = metrics.get("avg_slippage_bps", 0.0)
        adverse = metrics.get("adverse_selection", 0.0)
        impact = metrics.get("market_impact_bps", 0.0)

        adjustments: list[ParameterAdjustment] = []

        # Initialize current params for this strategy
        with self._lock:
            if strategy not in self._current_params:
                self._current_params[strategy] = {
                    "spread_multiplier": 1.0,
                    "size_multiplier": 1.0,
                }
            params = self._current_params[strategy]

        # Rule 1: High adverse selection -> widen spread
        if adverse > self._config.adverse_selection_threshold:
            old = params["spread_multiplier"]
            new = min(old * 1.10, 1.0 + self._config.max_spread_adjustment_pct / 100.0)
            if new != old:
                params["spread_multiplier"] = new
                adjustments.append(ParameterAdjustment(
                    strategy=strategy,
                    parameter="spread_multiplier",
                    old_value=old,
                    new_value=new,
                    reason=f"adverse_selection={adverse:.4f} > {self._config.adverse_selection_threshold}",
                    timestamp=now,
                ))

        # Rule 2: Low fill rate -> tighten spread
        elif fill_rate < self._config.fill_rate_low_threshold:
            old = params["spread_multiplier"]
            new = max(old * 0.95, 1.0 - self._config.max_spread_adjustment_pct / 100.0)
            if new != old:
                params["spread_multiplier"] = new
                adjustments.append(ParameterAdjustment(
                    strategy=strategy,
                    parameter="spread_multiplier",
                    old_value=old,
                    new_value=new,
                    reason=f"fill_rate={fill_rate:.3f} < {self._config.fill_rate_low_threshold}",
                    timestamp=now,
                ))

        # Rule 3: High slippage/impact -> reduce size
        if slippage > self._config.slippage_threshold_bps or impact > self._config.slippage_threshold_bps:
            old = params["size_multiplier"]
            new = max(old * 0.80, 1.0 - self._config.max_size_adjustment_pct / 100.0)
            if new != old:
                params["size_multiplier"] = new
                adjustments.append(ParameterAdjustment(
                    strategy=strategy,
                    parameter="size_multiplier",
                    old_value=old,
                    new_value=new,
                    reason=f"slippage={slippage:.1f}bps impact={impact:.1f}bps",
                    timestamp=now,
                ))

        # Rule 4: Good quality -> increase size
        elif fill_rate > self._config.fill_rate_high_threshold and adverse < self._config.adverse_selection_threshold * 0.5:
            old = params["size_multiplier"]
            new = min(old * 1.10, 1.0 + self._config.max_size_adjustment_pct / 100.0)
            if new != old:
                params["size_multiplier"] = new
                adjustments.append(ParameterAdjustment(
                    strategy=strategy,
                    parameter="size_multiplier",
                    old_value=old,
                    new_value=new,
                    reason=f"good_quality: fill_rate={fill_rate:.3f} adverse={adverse:.4f}",
                    timestamp=now,
                ))

        # Rule 5: Toxicity-driven spread widening (from ExecutionIntelligence alerts)
        vpin = metrics.get("vpin", 0.0)
        if vpin > 0.7:
            old = params["spread_multiplier"]
            scale = 1.15 if vpin > 0.8 else 1.10
            new = min(old * scale, 1.0 + self._config.max_spread_adjustment_pct / 100.0)
            if new != old:
                params["spread_multiplier"] = new
                adjustments.append(ParameterAdjustment(
                    strategy=strategy,
                    parameter="spread_multiplier",
                    old_value=old,
                    new_value=new,
                    reason=f"toxicity: vpin={vpin:.3f}",
                    timestamp=now,
                ))

        # Parameters stored in self._current_params[strategy]
        # and accessible via self.current_params(strategy).
        # Strategy pipelines can read these from Context.params.

        with self._lock:
            self._last_adjust[strategy] = now
            self._adjustments.extend(adjustments)

        if adjustments:
            for adj in adjustments:
                logger.info(
                    "Adaptive: %s.%s %.3f -> %.3f (%s)",
                    adj.strategy, adj.parameter, adj.old_value, adj.new_value, adj.reason,
                )

        return adjustments

    def current_params(self, strategy: str) -> dict[str, float]:
        """Get current adaptive parameters for a strategy."""
        with self._lock:
            return dict(self._current_params.get(strategy, {}))

    def get_multipliers(self, strategy: str) -> dict[str, float]:
        """Get spread and size multipliers for a strategy.

        Returns:
            {"spread_multiplier": x, "size_multiplier": y}
        """
        with self._lock:
            params = self._current_params.get(strategy, {})
            return {
                "spread_multiplier": params.get("spread_multiplier", 1.0),
                "size_multiplier": params.get("size_multiplier", 1.0),
            }

    def recent_adjustments(self, limit: int = 50) -> list[ParameterAdjustment]:
        return list(self._adjustments)[-limit:]

    def reset(self, strategy: str) -> None:
        """Reset adaptive parameters for a strategy to defaults."""
        with self._lock:
            self._current_params.pop(strategy, None)
            self._last_adjust.pop(strategy, None)
