"""Alpha Decay Tracker -- detect when trading edges are dying.

Monitors the information coefficient (IC) between predictions and outcomes
over time, estimating half-life and trend to detect decaying alpha signals.

Usage:
    # Standalone tracking
    tracker = AlphaDecayTracker(window=100)
    report = tracker.update(predictions=[0.6, 0.7], outcomes=[1.0, 0.0])
    if report and report.is_decaying:
        print(f"Alpha decaying! IC={report.current_ic:.3f}, half_life={report.half_life:.1f}s")

    # Pipeline integration
    hz.run(
        pipeline=[
            alpha_decay_pipeline(window=100),
            model,
            quoter,
        ],
        ...
    )
"""

from __future__ import annotations

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

from horizon.context import Context

logger = logging.getLogger("horizon")


@dataclass
class AlphaDecayReport:
    """Report on the current state of alpha decay."""

    current_ic: float = 0.0
    rolling_ic: list[tuple[float, float]] = field(default_factory=list)
    half_life: float = 0.0
    ic_trend: float = 0.0
    is_decaying: bool = False
    rolling_sharpe: list[tuple[float, float]] = field(default_factory=list)
    sharpe_trend: float = 0.0
    time_to_zero: float | None = None


class AlphaDecayTracker:
    """Stateful tracker that accumulates predictions and outcomes over time.

    Computes rolling IC (Spearman rank correlation), estimates half-life
    via AR(1) fit, and detects trend via linear regression on rolling IC.

    Args:
        window: Rolling window size for IC calculation.
        alert_threshold: IC trend slope below which alpha is considered decaying.
    """

    def __init__(self, window: int = 100, alert_threshold: float = -0.05) -> None:
        from horizon._horizon import auth_require_pro
        auth_require_pro()

        self._window = window
        self._alert_threshold = alert_threshold

        # Accumulated data
        self._predictions: deque[float] = deque(maxlen=window)
        self._outcomes: deque[float] = deque(maxlen=window)
        self._timestamps: deque[float] = deque(maxlen=window)

        # Rolling IC history: (timestamp, ic)
        self._ic_history: list[tuple[float, float]] = []

        # Rolling returns for Sharpe calculation
        self._returns: deque[float] = deque(maxlen=window)
        self._sharpe_history: list[tuple[float, float]] = []

    def update(
        self,
        predictions: list[float],
        outcomes: list[float],
        timestamp: float | None = None,
    ) -> AlphaDecayReport | None:
        """Add a new batch of predictions/outcomes and compute report.

        Args:
            predictions: Model prediction values (e.g., predicted probabilities).
            outcomes: Realized outcome values (e.g., 1.0 for win, 0.0 for loss).
            timestamp: Observation timestamp. Defaults to current time.

        Returns:
            AlphaDecayReport if enough data has accumulated, None otherwise.
        """
        if not predictions or not outcomes:
            return None

        ts = timestamp if timestamp is not None else _time.time()

        n = min(len(predictions), len(outcomes))
        for i in range(n):
            self._predictions.append(predictions[i])
            self._outcomes.append(outcomes[i])
            self._timestamps.append(ts)

            # Compute per-observation return (prediction error as proxy)
            pred = predictions[i]
            outcome = outcomes[i]
            # Return: positive if prediction and outcome agree, negative otherwise
            ret = outcome * pred - (1.0 - outcome) * pred
            self._returns.append(ret)

        # Need at least half the window to produce meaningful results
        if len(self._predictions) < max(self._window // 2, 10):
            return None

        return self.report()

    def report(self) -> AlphaDecayReport:
        """Force compute the current alpha decay state.

        Returns:
            AlphaDecayReport with current metrics.
        """
        preds = list(self._predictions)
        outs = list(self._outcomes)
        now = self._timestamps[-1] if self._timestamps else _time.time()

        # Compute current IC (Spearman rank correlation)
        current_ic = _spearman_rank_correlation(preds, outs)

        # Record IC observation
        self._ic_history.append((now, current_ic))

        # Compute rolling Sharpe
        returns = list(self._returns)
        sharpe = _rolling_sharpe(returns)
        self._sharpe_history.append((now, sharpe))

        # IC trend: linear regression slope on IC history
        ic_values = [ic for _, ic in self._ic_history]
        ic_trend = _linear_regression_slope(ic_values)

        # Sharpe trend
        sharpe_values = [s for _, s in self._sharpe_history]
        sharpe_trend = _linear_regression_slope(sharpe_values)

        # Half-life: fit AR(1) to IC series
        half_life = _ar1_half_life(ic_values)

        # Time to zero: linear extrapolation
        time_to_zero: float | None = None
        if ic_trend < 0 and current_ic > 0:
            # At current rate, how long until IC reaches 0?
            time_to_zero = -current_ic / ic_trend
        elif ic_trend < 0 and current_ic <= 0:
            time_to_zero = 0.0

        # Decaying if IC trend is below alert threshold
        is_decaying = ic_trend < self._alert_threshold

        return AlphaDecayReport(
            current_ic=current_ic,
            rolling_ic=list(self._ic_history),
            half_life=half_life,
            ic_trend=ic_trend,
            is_decaying=is_decaying,
            rolling_sharpe=list(self._sharpe_history),
            sharpe_trend=sharpe_trend,
            time_to_zero=time_to_zero,
        )


def alpha_decay_pipeline(
    window: int = 100,
    alert_threshold: float = -0.05,
) -> Callable[[Context], None]:
    """Pipeline function for hz.run() that tracks alpha decay.

    Uses predictions from ``ctx.params.get("predictions")`` and outcomes
    derived from fills to track alpha decay over time.

    Injects into ctx.params:
        - "alpha_ic": current information coefficient
        - "alpha_half_life": estimated half-life of IC
        - "alpha_decaying": bool indicating if alpha is decaying

    Logs a warning when is_decaying transitions to True.

    Args:
        window: Rolling window for IC calculation.
        alert_threshold: IC trend slope below which alpha is considered decaying.

    Returns:
        Pipeline function: (Context) -> None
    """
    tracker = AlphaDecayTracker(window=window, alert_threshold=alert_threshold)
    _was_decaying: list[bool] = [False]  # Mutable container for closure

    def _track(ctx: Context) -> None:
        predictions = ctx.params.get("predictions")
        if predictions is None:
            # No predictions available this cycle
            ctx.params["alpha_ic"] = 0.0
            ctx.params["alpha_half_life"] = 0.0
            ctx.params["alpha_decaying"] = False
            return

        # Build outcomes from recent fills
        recent_fills = ctx.params.get("_recent_fills", [])
        outcomes: list[float] = []
        for fill in recent_fills:
            # Use fill price as realized outcome probability
            price = getattr(fill, "price", None)
            if price is not None:
                outcomes.append(price)

        if not outcomes:
            # No outcomes to compare against
            ctx.params["alpha_ic"] = 0.0
            ctx.params["alpha_half_life"] = 0.0
            ctx.params["alpha_decaying"] = False
            return

        # Ensure predictions and outcomes are aligned (take min length)
        n = min(len(predictions), len(outcomes))
        report = tracker.update(predictions[:n], outcomes[:n])

        if report is None:
            ctx.params["alpha_ic"] = 0.0
            ctx.params["alpha_half_life"] = 0.0
            ctx.params["alpha_decaying"] = False
            return

        ctx.params["alpha_ic"] = report.current_ic
        ctx.params["alpha_half_life"] = report.half_life
        ctx.params["alpha_decaying"] = report.is_decaying

        # Log warning on transition to decaying
        if report.is_decaying and not _was_decaying[0]:
            logger.warning(
                "Alpha decay detected: IC=%.3f, trend=%.4f, half_life=%.1f",
                report.current_ic,
                report.ic_trend,
                report.half_life,
            )
        _was_decaying[0] = report.is_decaying

    _track.__name__ = "alpha_decay_tracker"
    return _track


# ---------------------------------------------------------------------------
# Internal math helpers
# ---------------------------------------------------------------------------

def _rank(values: list[float]) -> list[float]:
    """Assign ranks to values, handling ties with average rank.

    Args:
        values: List of numeric values.

    Returns:
        List of ranks (1-based).
    """
    n = len(values)
    if n == 0:
        return []

    # Create (value, original_index) pairs and sort
    indexed = sorted(enumerate(values), key=lambda x: x[1])
    ranks = [0.0] * n

    i = 0
    while i < n:
        # Find range of tied values
        j = i + 1
        while j < n and indexed[j][1] == indexed[i][1]:
            j += 1

        # Average rank for tied values
        avg_rank = (i + j + 1) / 2.0  # 1-based average
        for k in range(i, j):
            ranks[indexed[k][0]] = avg_rank

        i = j

    return ranks


def _spearman_rank_correlation(x: list[float], y: list[float]) -> float:
    """Compute Spearman rank correlation between two lists.

    This is the Pearson correlation of the rank-transformed values.

    Args:
        x: First list of values.
        y: Second list of values.

    Returns:
        Spearman rank correlation coefficient in [-1, 1].
    """
    n = min(len(x), len(y))
    if n < 3:
        return 0.0

    x = x[:n]
    y = y[:n]

    rx = _rank(x)
    ry = _rank(y)

    # Pearson correlation of ranks
    mean_rx = sum(rx) / n
    mean_ry = sum(ry) / n

    cov = 0.0
    var_x = 0.0
    var_y = 0.0

    for i in range(n):
        dx = rx[i] - mean_rx
        dy = ry[i] - mean_ry
        cov += dx * dy
        var_x += dx * dx
        var_y += dy * dy

    denom = math.sqrt(var_x * var_y)
    if denom < 1e-15:
        return 0.0

    return cov / denom


def _linear_regression_slope(values: list[float]) -> float:
    """Compute the slope of a simple linear regression on values vs index.

    Args:
        values: List of y-values (x = 0, 1, 2, ...).

    Returns:
        Regression slope.
    """
    n = len(values)
    if n < 2:
        return 0.0

    # x = 0, 1, ..., n-1
    mean_x = (n - 1) / 2.0
    mean_y = sum(values) / n

    cov = 0.0
    var_x = 0.0

    for i in range(n):
        dx = i - mean_x
        dy = values[i] - mean_y
        cov += dx * dy
        var_x += dx * dx

    if var_x < 1e-15:
        return 0.0

    return cov / var_x


def _ar1_half_life(values: list[float]) -> float:
    """Estimate half-life from an AR(1) fit on a time series.

    Fits y[t] = phi * y[t-1] + epsilon, then computes
    half_life = -ln(2) / ln(|phi|).

    Args:
        values: Time series of IC observations.

    Returns:
        Estimated half-life in number of observations.
        Returns inf if the series is not mean-reverting.
    """
    n = len(values)
    if n < 3:
        return float("inf")

    # Simple OLS for AR(1): regress y[t] on y[t-1]
    y = values[1:]
    x = values[:-1]
    m = len(y)

    mean_x = sum(x) / m
    mean_y = sum(y) / m

    cov = 0.0
    var_x = 0.0

    for i in range(m):
        dx = x[i] - mean_x
        dy = y[i] - mean_y
        cov += dx * dy
        var_x += dx * dx

    if var_x < 1e-15:
        return float("inf")

    phi = cov / var_x

    # Half-life only meaningful for |phi| < 1 (stationary)
    abs_phi = abs(phi)
    if abs_phi >= 1.0 or abs_phi < 1e-10:
        return float("inf")

    ln_phi = math.log(abs_phi)
    if abs(ln_phi) < 1e-15:
        return float("inf")

    half_life = -math.log(2.0) / ln_phi
    return max(half_life, 0.0)


def _rolling_sharpe(returns: list[float]) -> float:
    """Compute Sharpe ratio (annualized) of a return series.

    Assumes returns are per-observation and uses no risk-free rate.

    Args:
        returns: List of return observations.

    Returns:
        Sharpe ratio. Returns 0.0 if insufficient data.
    """
    n = len(returns)
    if n < 2:
        return 0.0

    mean_r = sum(returns) / n
    var_r = sum((r - mean_r) ** 2 for r in returns) / n
    std_r = math.sqrt(var_r)

    if std_r < 1e-15:
        return 0.0

    return mean_r / std_r
