"""Anomaly detection for time series data.

Identifies outliers in demand or sales time series using statistical methods:
    1. Z-score: Rolling mean/std, flag points where |z| > threshold
    2. IQR: Rolling quartile-based bounds, flag points outside [Q1-1.5*IQR, Q3+1.5*IQR]

Both methods use a rolling window so they adapt to local trends.

Backends:
    - builtin: Pure-math z-score and IQR detection (no dependencies)
    - adtk: Advanced anomaly detection toolkit (richer models)

Usage via provider:
    from platoon.learning.providers import get_provider
    ad = get_provider("anomaly")
    result = ad.detect(series, dates, method="zscore", window=30, threshold=2.5)

Direct usage:
    from platoon.learning.anomaly import detect_anomalies
    result = detect_anomalies(series, dates, method="zscore")
"""

from dataclasses import dataclass, field
from typing import List, Optional

from platoon.learning.providers import ModelProvider, register_provider


# ---------------------------------------------------------------------------
# Result types
# ---------------------------------------------------------------------------


@dataclass
class AnomalyPoint:
    """A single detected anomaly."""

    date: str
    value: float
    expected: float  # rolling mean or median at this point
    z_score: float
    direction: str  # "spike" or "drop"
    severity: str  # "low", "medium", "high"


@dataclass
class AnomalyResult:
    """Output of anomaly detection."""

    anomalies: List[AnomalyPoint]
    total_points: int
    anomaly_count: int
    anomaly_rate: float  # anomaly_count / total_points
    series_mean: float
    series_std: float
    method: str


# ---------------------------------------------------------------------------
# Pure-math implementations
# ---------------------------------------------------------------------------


def _rolling_stats(series: List[float], window: int):
    """Compute rolling mean and std for each point."""
    n = len(series)
    means = []
    stds = []
    for i in range(n):
        start = max(0, i - window + 1)
        chunk = series[start:i + 1]
        m = sum(chunk) / len(chunk)
        if len(chunk) < 2:
            s = 0.0
        else:
            s = (sum((x - m) ** 2 for x in chunk) / len(chunk)) ** 0.5
        means.append(m)
        stds.append(s)
    return means, stds


def _rolling_quartiles(series: List[float], window: int):
    """Compute rolling Q1, Q3 for each point."""
    n = len(series)
    q1s = []
    q3s = []
    for i in range(n):
        start = max(0, i - window + 1)
        chunk = sorted(series[start:i + 1])
        k = len(chunk)
        q1_idx = k // 4
        q3_idx = (3 * k) // 4
        q1s.append(chunk[q1_idx])
        q3s.append(chunk[min(q3_idx, k - 1)])
    return q1s, q3s


def _severity(z: float, threshold: float) -> str:
    """Classify severity based on how far z exceeds the threshold."""
    excess = abs(z) - threshold
    if excess < 1.0:
        return "low"
    elif excess < 2.0:
        return "medium"
    return "high"


def detect_anomalies(
    series: List[float],
    dates: Optional[List[str]] = None,
    method: str = "zscore",
    window: int = 30,
    threshold: float = 2.5,
) -> AnomalyResult:
    """Detect anomalies in a time series.

    Args:
        series: Numeric values (e.g. daily units sold).
        dates: Optional date labels for each point.
        method: "zscore" or "iqr".
        window: Rolling window size in points.
        threshold: Z-score threshold for flagging (zscore method only).

    Returns:
        AnomalyResult with list of detected anomalies and summary stats.
    """
    n = len(series)
    if dates is None:
        dates = [str(i) for i in range(n)]

    # Series-level stats
    series_mean = sum(series) / n if n > 0 else 0.0
    series_std = (sum((x - series_mean) ** 2 for x in series) / n) ** 0.5 if n > 0 else 0.0

    anomalies: List[AnomalyPoint] = []

    if method == "iqr":
        q1s, q3s = _rolling_quartiles(series, window)
        means, _ = _rolling_stats(series, window)

        for i in range(n):
            iqr = q3s[i] - q1s[i]
            lower = q1s[i] - 1.5 * iqr
            upper = q3s[i] + 1.5 * iqr
            val = series[i]

            if val < lower or val > upper:
                # Compute a pseudo-z for severity
                spread = iqr if iqr > 0 else 1.0
                z = (val - means[i]) / spread
                direction = "spike" if val > upper else "drop"
                anomalies.append(AnomalyPoint(
                    date=dates[i],
                    value=val,
                    expected=round(means[i], 2),
                    z_score=round(z, 2),
                    direction=direction,
                    severity=_severity(z, 1.0),  # IQR uses lower effective threshold
                ))
    else:
        # Z-score method
        means, stds = _rolling_stats(series, window)

        for i in range(n):
            val = series[i]
            std = stds[i]
            if std == 0:
                continue
            z = (val - means[i]) / std
            if abs(z) > threshold:
                direction = "spike" if z > 0 else "drop"
                anomalies.append(AnomalyPoint(
                    date=dates[i],
                    value=val,
                    expected=round(means[i], 2),
                    z_score=round(z, 2),
                    direction=direction,
                    severity=_severity(z, threshold),
                ))

    return AnomalyResult(
        anomalies=anomalies,
        total_points=n,
        anomaly_count=len(anomalies),
        anomaly_rate=round(len(anomalies) / n, 4) if n > 0 else 0.0,
        series_mean=round(series_mean, 2),
        series_std=round(series_std, 2),
        method=method,
    )


# ---------------------------------------------------------------------------
# Built-in provider
# ---------------------------------------------------------------------------


@register_provider
class BuiltinAnomalyProvider(ModelProvider):
    """Pure-math anomaly detection — z-score and IQR methods."""

    name = "builtin_anomaly"
    domain = "anomaly"
    backend = "builtin"

    def detect(
        self,
        series: List[float],
        dates: Optional[List[str]] = None,
        method: str = "zscore",
        window: int = 30,
        threshold: float = 2.5,
    ) -> AnomalyResult:
        """Detect anomalies in a time series."""
        return detect_anomalies(series, dates, method=method, window=window, threshold=threshold)


# ---------------------------------------------------------------------------
# Library stub
# ---------------------------------------------------------------------------


@register_provider
class AdtkAnomalyProvider(ModelProvider):
    """Advanced anomaly detection via ADTK (Anomaly Detection Toolkit).

    TODO: Implement detect() using adtk.detector (VolatilityShiftAD, SeasonalAD, etc.)
    Requires: adtk
    """

    name = "adtk_anomaly"
    domain = "anomaly"
    backend = "adtk"

    @classmethod
    def is_available(cls) -> bool:
        try:
            import adtk  # noqa: F401
            return True
        except ImportError:
            return False

    def detect(
        self,
        series: List[float],
        dates: Optional[List[str]] = None,
        method: str = "zscore",
        window: int = 30,
        threshold: float = 2.5,
    ) -> AnomalyResult:
        """Detect anomalies using ADTK models."""
        raise NotImplementedError("adtk anomaly provider not yet implemented")
