from __future__ import annotations

import numpy as np
import pandas as pd


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

    values = clean.values
    x_axis = np.arange(len(values), dtype=float)
    mean_val = float(np.mean(values))
    if mean_val == 0:
        return []

    slope, intercept = np.polyfit(x_axis, values, 1)
    fitted = slope * x_axis + intercept
    ss_res = float(np.sum((values - fitted) ** 2))
    ss_tot = float(np.sum((values - np.mean(values)) ** 2))
    r_squared = 1.0 - (ss_res / ss_tot) if ss_tot > 0 else 0.0
    normalized_slope = float(slope / mean_val)

    patterns = []
    patterns.extend(_trend_patterns(normalized_slope, r_squared))
    patterns.extend(_volatility_pattern(values, mean_val))
    patterns.extend(_recent_change_pattern(values))
    patterns.extend(_change_point_pattern(clean))
    patterns.extend(_seasonality_pattern(values))
    patterns.extend(_momentum_pattern(values))

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


def _trend_patterns(normalized_slope: float, r_squared: float) -> list[dict]:
    if abs(normalized_slope) < 0.02 or r_squared < 0.5:
        return []
    return [
        {
            "pattern": "upward_trend" if normalized_slope > 0 else "downward_trend",
            "strength": round(abs(normalized_slope), 4),
            "confidence": round(r_squared, 3),
            "details": {
                "slope_pct_per_period": round(normalized_slope * 100.0, 2),
                "fit_r2": round(r_squared, 3),
            },
            "explanation": (
                f"consistent {'upward' if normalized_slope > 0 else 'downward'} drift "
                f"of {abs(normalized_slope) * 100.0:.2f}% per period"
            ),
        }
    ]


def _volatility_pattern(values: np.ndarray, mean_val: float) -> list[dict]:
    cv = float(np.std(values) / abs(mean_val))
    if cv < 0.2:
        return []
    return [
        {
            "pattern": "high_volatility",
            "strength": round(cv, 4),
            "confidence": 0.7,
            "details": {"coefficient_of_variation": round(cv, 4)},
            "explanation": "metric fluctuates significantly relative to its mean",
        }
    ]


def _recent_change_pattern(values: np.ndarray) -> list[dict]:
    if len(values) < 3 or values[-2] == 0:
        return []
    recent_change = float((values[-1] - values[-2]) / values[-2])
    if abs(recent_change) < 0.1:
        return []
    return [
        {
            "pattern": "recent_spike" if recent_change > 0 else "recent_drop",
            "strength": round(abs(recent_change), 4),
            "confidence": 0.65,
            "details": {"last_period_change_pct": round(recent_change * 100.0, 2)},
            "explanation": f"latest period moved {recent_change * 100.0:.1f}% vs prior period",
        }
    ]


def _change_point_pattern(clean: pd.Series) -> list[dict]:
    if len(clean) < 8:
        return []

    rolling = clean.rolling(window=3, min_periods=3).mean().dropna()
    if len(rolling) < 4:
        return []

    deltas = rolling.diff().dropna()
    if deltas.empty:
        return []

    baseline_std = float(deltas.std())
    if baseline_std == 0:
        return []

    peak_idx = deltas.abs().idxmax()
    z_score = float(abs(deltas.loc[peak_idx]) / baseline_std)
    if z_score < 2.0:
        return []

    return [
        {
            "pattern": "change_point",
            "strength": round(z_score, 4),
            "confidence": min(0.95, round(0.55 + min(z_score, 5.0) * 0.08, 3)),
            "period": str(peak_idx),
            "details": {
                "rolling_delta": round(float(deltas.loc[peak_idx]), 4),
                "delta_std": round(baseline_std, 4),
            },
            "explanation": "rolling average shows a structural break around this period",
        }
    ]


def _seasonality_pattern(values: np.ndarray) -> list[dict]:
    if len(values) < 12:
        return []

    candidate_lags = [2, 3, 4, 6, 12]
    best_lag = None
    best_corr = 0.0
    for lag in candidate_lags:
        if lag >= len(values) or (len(values) < (lag * 2)):
            continue
        corr = np.corrcoef(values[:-lag], values[lag:])[0, 1]
        if np.isnan(corr):
            continue
        if corr > best_corr:
            best_corr = float(corr)
            best_lag = lag

    if best_lag is None or best_corr < 0.65:
        return []

    return [
        {
            "pattern": "seasonality_signal",
            "strength": round(best_corr, 4),
            "confidence": round(min(0.9, 0.5 + best_corr * 0.4), 3),
            "details": {"lag": best_lag, "autocorrelation": round(best_corr, 4)},
            "explanation": f"repeating behavior every ~{best_lag} periods",
        }
    ]


def _momentum_pattern(values: np.ndarray) -> list[dict]:
    if len(values) < 5:
        return []

    diffs = np.diff(values)
    up_streak = _longest_streak(diffs > 0)
    down_streak = _longest_streak(diffs < 0)
    streak = max(up_streak, down_streak)

    if streak < 3:
        return []

    direction = "upward" if up_streak >= down_streak else "downward"
    strength = streak / max(len(values) - 1, 1)
    return [
        {
            "pattern": "momentum_run",
            "strength": round(strength, 4),
            "confidence": round(min(0.9, 0.45 + (strength * 0.5)), 3),
            "details": {"direction": direction, "streak_length": int(streak)},
            "explanation": f"{direction} movement sustained for {streak} consecutive steps",
        }
    ]


def _longest_streak(mask: np.ndarray) -> int:
    best = 0
    current = 0
    for flag in mask:
        if flag:
            current += 1
            if current > best:
                best = current
        else:
            current = 0
    return best
