"""Alpha decay tracking: detect edge erosion and predict strategy retirement."""

from __future__ import annotations

import logging
import math
import threading
import time
from collections import deque
from dataclasses import dataclass

from horizon._horizon import signal_half_life

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

# ---------------------------------------------------------------------------
# Data classes
# ---------------------------------------------------------------------------

_SECONDS_PER_DAY = 86_400.0


@dataclass(frozen=True)
class DecayProfile:
    """Fitted exponential decay profile for a strategy's edge."""

    strategy_name: str
    initial_edge: float
    current_edge: float
    half_life_days: float
    predicted_zero_crossing: float | None
    edge_type: str  # "structural" / "temporary" / "unknown"
    r_squared: float


@dataclass(frozen=True)
class DecayAlert:
    """Alert raised when a strategy's edge changes significantly."""

    strategy_name: str
    alert_type: str  # "approaching_zero" / "half_life_reached" / "edge_refreshed"
    message: str
    timestamp: float


# ---------------------------------------------------------------------------
# Tracker
# ---------------------------------------------------------------------------


class AlphaDecayTracker:
    """Track edge observations per strategy, fit exponential decay, and emit alerts.

    Uses log-linear OLS to estimate half-life and R-squared of the edge decay
    curve. The Rust ``signal_half_life`` helper is available for additional
    signal analysis but the primary fitting is done in pure Python so that
    we can extract R-squared and zero-crossing estimates.
    """

    def __init__(self, min_observations: int = 10) -> None:
        self._min_obs = max(min_observations, 3)
        self._observations: dict[str, list[tuple[float, float]]] = {}
        self._alerts: deque[DecayAlert] = deque(maxlen=10_000)
        self._prev_edge: dict[str, float] = {}
        self._lock = threading.Lock()

    # ------------------------------------------------------------------
    # Recording
    # ------------------------------------------------------------------

    def record_edge(self, strategy_name: str, edge: float, timestamp: float | None = None) -> None:
        """Append an edge observation for *strategy_name*."""
        ts = timestamp if timestamp is not None else time.time()
        with self._lock:
            self._observations.setdefault(strategy_name, []).append((ts, edge))

    # ------------------------------------------------------------------
    # Fitting
    # ------------------------------------------------------------------

    def fit_decay(self, strategy_name: str) -> DecayProfile | None:
        """Fit exponential decay via log-linear OLS for *strategy_name*.

        Returns ``None`` when there are fewer than ``min_observations`` usable
        data points (positive edge values).
        """
        with self._lock:
            obs = list(self._observations.get(strategy_name, []))

        if len(obs) < self._min_obs:
            return None

        # Filter to positive absolute edge values (log requires > 0)
        filtered: list[tuple[float, float]] = []
        for ts, val in obs:
            a = abs(val)
            if a > 0:
                filtered.append((ts, a))

        if len(filtered) < self._min_obs:
            return None

        # Normalise time to days since first observation
        t0 = filtered[0][0]
        t_vals = [(ts - t0) / _SECONDS_PER_DAY for ts, _ in filtered]
        y_vals = [math.log(v) for _, v in filtered]

        n = len(t_vals)
        sum_t = sum(t_vals)
        sum_y = sum(y_vals)
        sum_tt = sum(t * t for t in t_vals)
        sum_ty = sum(t * y for t, y in zip(t_vals, y_vals))

        denom = n * sum_tt - sum_t * sum_t
        if abs(denom) < 1e-15:
            return None

        b = (n * sum_ty - sum_t * sum_y) / denom
        a = (sum_y - b * sum_t) / n

        # R-squared
        y_mean = sum_y / n
        ss_tot = sum((y - y_mean) ** 2 for y in y_vals)
        ss_res = sum((y - (a + b * t)) ** 2 for t, y in zip(t_vals, y_vals))
        r_sq = 1.0 - ss_res / ss_tot if ss_tot > 1e-15 else 0.0

        # Half-life in days: log(edge) decays with slope b => half_life = -ln(2)/b
        if b < 0:
            half_life_days = -math.log(2) / b
        else:
            half_life_days = float("inf")

        # Predicted zero crossing: time (days) when edge < 0.005
        # log(0.005) = a + b * t_zero  =>  t_zero = (log(0.005) - a) / b
        predicted_zero: float | None = None
        if b < 0:
            t_zero = (math.log(0.005) - a) / b
            if t_zero > 0:
                predicted_zero = t0 + t_zero * _SECONDS_PER_DAY

        # Edge type classification
        if r_sq > 0.5 and half_life_days > 90:
            edge_type = "structural"
        elif r_sq > 0.5 and half_life_days < 30:
            edge_type = "temporary"
        else:
            edge_type = "unknown"

        initial_edge = filtered[0][1]
        current_edge = filtered[-1][1]

        return DecayProfile(
            strategy_name=strategy_name,
            initial_edge=initial_edge,
            current_edge=current_edge,
            half_life_days=half_life_days,
            predicted_zero_crossing=predicted_zero,
            edge_type=edge_type,
            r_squared=r_sq,
        )

    # ------------------------------------------------------------------
    # Retirement decision
    # ------------------------------------------------------------------

    def should_retire(self, strategy_name: str, min_edge: float = 0.01) -> bool:
        """Return ``True`` if the strategy's edge has decayed below *min_edge* and the decay fit is credible."""
        profile = self.fit_decay(strategy_name)
        if profile is None:
            return False
        return profile.current_edge < min_edge and profile.r_squared > 0.3

    # ------------------------------------------------------------------
    # Alerts
    # ------------------------------------------------------------------

    def check_alerts(self, min_edge: float = 0.01) -> list[DecayAlert]:
        """Check all tracked strategies and return new alerts."""
        with self._lock:
            strategy_names = list(self._observations.keys())

        now = time.time()
        new_alerts: list[DecayAlert] = []

        for name in strategy_names:
            profile = self.fit_decay(name)
            if profile is None:
                continue

            # approaching_zero
            if profile.current_edge < 2 * min_edge:
                alert = DecayAlert(
                    strategy_name=name,
                    alert_type="approaching_zero",
                    message=(
                        f"{name} edge {profile.current_edge:.4f} approaching zero "
                        f"(threshold {min_edge:.4f})"
                    ),
                    timestamp=now,
                )
                new_alerts.append(alert)
                logger.warning("Alpha decay alert: %s", alert.message)

            # half_life_reached
            with self._lock:
                obs = self._observations.get(name, [])
                elapsed_days = (obs[-1][0] - obs[0][0]) / _SECONDS_PER_DAY if len(obs) >= 2 else 0.0
            if profile.half_life_days != float("inf") and elapsed_days > profile.half_life_days:
                alert = DecayAlert(
                    strategy_name=name,
                    alert_type="half_life_reached",
                    message=(
                        f"{name} elapsed {elapsed_days:.1f}d > half_life {profile.half_life_days:.1f}d"
                    ),
                    timestamp=now,
                )
                new_alerts.append(alert)
                logger.info("Alpha decay alert: %s", alert.message)

            # edge_refreshed: current_edge > 1.5 * previous edge
            with self._lock:
                prev = self._prev_edge.get(name)
            if prev is not None and prev > 0 and profile.current_edge > 1.5 * prev:
                alert = DecayAlert(
                    strategy_name=name,
                    alert_type="edge_refreshed",
                    message=(
                        f"{name} edge refreshed {prev:.4f} -> {profile.current_edge:.4f}"
                    ),
                    timestamp=now,
                )
                new_alerts.append(alert)
                logger.info("Alpha decay alert: %s", alert.message)

            # Update previous edge for next round
            with self._lock:
                self._prev_edge[name] = profile.current_edge

        with self._lock:
            self._alerts.extend(new_alerts)

        return new_alerts

    # ------------------------------------------------------------------
    # Queries
    # ------------------------------------------------------------------

    def decay_profiles(self) -> list[DecayProfile]:
        """Return fitted decay profiles for all tracked strategies."""
        with self._lock:
            strategy_names = list(self._observations.keys())
        profiles: list[DecayProfile] = []
        for name in strategy_names:
            profile = self.fit_decay(name)
            if profile is not None:
                profiles.append(profile)
        return profiles

    def recent_alerts(self, limit: int = 20) -> list[DecayAlert]:
        """Return the most recent alerts (up to *limit*)."""
        with self._lock:
            return list(self._alerts)[-limit:]
