from __future__ import annotations

import numpy as np
import pandas as pd


def detect_series_outliers(series: pd.Series, z_threshold: float = 3.5) -> list[dict]:
    clean = series.dropna().astype(float)
    if len(clean) < 4:
        return []

    stats = _distribution_stats(clean)
    if stats["mad"] == 0 and stats["std"] == 0:
        return []

    robust_z = _robust_z_scores(clean, stats["median"], stats["mad"])
    iqr_flags = _iqr_flags(clean, stats["q1"], stats["q3"], factor=1.5)
    robust_flags = robust_z.abs() >= z_threshold
    flagged = clean[robust_flags | iqr_flags]

    outliers = []
    for period, value in flagged.items():
        z_val = float(robust_z.loc[period]) if period in robust_z.index else 0.0
        deviation_pct = _deviation_pct(float(value), stats["median"])
        score = max(abs(z_val), abs(deviation_pct) / 100.0)
        value = float(clean.loc[period])
        severity = _severity_from_score(score)
        methods = []
        if robust_flags.get(period, False):
            methods.append("robust_z")
        if iqr_flags.get(period, False):
            methods.append("iqr")
        outliers.append(
            {
                "period": str(period),
                "value": value,
                "score": round(float(score), 3),
                "direction": "high" if z_val > 0 else "low",
                "severity": severity,
                "deviation_pct": round(deviation_pct, 2),
                "median": round(stats["median"], 3),
                "method": "+".join(methods) if methods else "robust_z",
                "explanation": (
                    f"value is {abs(deviation_pct):.1f}% "
                    f"{'above' if deviation_pct >= 0 else 'below'} median"
                ),
            }
        )

    outliers.sort(key=lambda item: item["score"], reverse=True)
    return outliers


def _distribution_stats(series: pd.Series) -> dict:
    values = series.values
    q1 = float(np.percentile(values, 25))
    q3 = float(np.percentile(values, 75))
    return {
        "median": float(np.median(values)),
        "mad": float(np.median(np.abs(values - np.median(values)))),
        "std": float(np.std(values)),
        "q1": q1,
        "q3": q3,
    }


def _robust_z_scores(series: pd.Series, median: float, mad: float) -> pd.Series:
    if mad == 0:
        return pd.Series(np.zeros(len(series)), index=series.index)
    return 0.6745 * (series - median) / mad


def _iqr_flags(
    series: pd.Series, q1: float, q3: float, factor: float = 1.5
) -> pd.Series:
    iqr = q3 - q1
    if iqr == 0:
        return pd.Series(False, index=series.index)
    lower = q1 - (factor * iqr)
    upper = q3 + (factor * iqr)
    return (series < lower) | (series > upper)


def _deviation_pct(value: float, median: float) -> float:
    if median == 0:
        return 0.0 if value == 0 else 100.0
    return ((value - median) / abs(median)) * 100.0


def _severity_from_score(score: float) -> str:
    if score >= 6:
        return "critical"
    if score >= 4:
        return "high"
    if score >= 2.5:
        return "medium"
    return "low"
