"""Diagnostics for time series analysis and forecasting robustness."""

from dataclasses import dataclass
from typing import Optional

import numpy as np
import pandas as pd


@dataclass
class StructuralBreakWarning:
    """
    Warning information about detected structural breaks.

    Attributes
    ----------
    detected : bool
        Whether a structural break was detected.
    recent_mean : float
        Mean of the recent window.
    earlier_mean : float
        Mean of the earlier comparison window.
    pct_change : float
        Percentage change between windows.
    message : str
        Human-readable warning message.
    """

    detected: bool
    recent_mean: float
    earlier_mean: float
    pct_change: float
    message: str


@dataclass
class VolatilityAnalysis:
    """
    Analysis of time series volatility.

    Attributes
    ----------
    coefficient_of_variation : float
        CV = std / mean, measures relative variability.
    volatility_level : str
        One of 'low', 'medium', 'high'.
    interval_multiplier : float
        Suggested multiplier for prediction intervals (1.0, 1.25, or 1.5).
    """

    coefficient_of_variation: float
    volatility_level: str
    interval_multiplier: float


def detect_structural_break(
    y: pd.Series,
    window: int = 8,
    threshold: float = 0.5,
) -> StructuralBreakWarning:
    """
    Detect if recent behavior differs significantly from earlier periods.

    Compares the mean of the most recent `window` observations to the
    mean of the preceding `window` observations. A structural break is
    flagged if the percentage change exceeds `threshold`.

    Parameters
    ----------
    y : pd.Series
        Time series data.
    window : int, default 8
        Number of periods for comparison windows.
    threshold : float, default 0.5
        Percentage change threshold (0.5 = 50% change).

    Returns
    -------
    StructuralBreakWarning
        Warning object with detection results.

    Examples
    --------
    >>> import pandas as pd
    >>> import numpy as np
    >>> # Create series with structural break
    >>> dates = pd.date_range('2024-01-01', periods=20, freq='W')
    >>> values = np.concatenate([np.ones(10) * 100, np.ones(10) * 200])
    >>> y = pd.Series(values, index=dates)
    >>> warning = detect_structural_break(y)
    >>> print(warning.detected)  # True
    >>> print(f"{warning.pct_change:.0%}")  # 100%
    """
    if len(y) < 2 * window:
        return StructuralBreakWarning(
            detected=False,
            recent_mean=0.0,
            earlier_mean=0.0,
            pct_change=0.0,
            message="Insufficient data for structural break detection.",
        )

    recent = y.tail(window)
    earlier = y.iloc[-(2 * window) : -window]

    recent_mean = recent.mean()
    earlier_mean = earlier.mean()

    # Avoid division by zero
    if earlier_mean == 0:
        if recent_mean == 0:
            pct_change = 0.0
        else:
            pct_change = float("inf")
    else:
        pct_change = (recent_mean - earlier_mean) / earlier_mean

    if abs(pct_change) >= threshold:
        direction = "increased" if pct_change > 0 else "decreased"
        return StructuralBreakWarning(
            detected=True,
            recent_mean=recent_mean,
            earlier_mean=earlier_mean,
            pct_change=pct_change,
            message=(
                f"Structural break detected: Recent {window}-period mean "
                f"{direction} {abs(pct_change):.0%} vs earlier period. "
                f"Forecasts may be unreliable."
            ),
        )

    return StructuralBreakWarning(
        detected=False,
        recent_mean=recent_mean,
        earlier_mean=earlier_mean,
        pct_change=pct_change,
        message="No structural break detected.",
    )


def analyze_volatility(y: pd.Series) -> VolatilityAnalysis:
    """
    Analyze time series volatility and determine interval adjustment.

    Calculates the coefficient of variation (CV = std / mean) and maps
    it to a volatility level and prediction interval multiplier.

    CV Ranges and Multipliers:
    - Low (CV < 20%): multiplier = 1.0
    - Medium (20% <= CV < 35%): multiplier = 1.25
    - High (CV >= 35%): multiplier = 1.5

    Parameters
    ----------
    y : pd.Series
        Time series data (positive values).

    Returns
    -------
    VolatilityAnalysis
        Analysis results with CV, level, and interval multiplier.

    Examples
    --------
    >>> import pandas as pd
    >>> import numpy as np
    >>> # Low volatility series
    >>> y_low = pd.Series([100, 102, 98, 101, 99, 103, 97, 100])
    >>> analysis = analyze_volatility(y_low)
    >>> print(analysis.volatility_level)  # 'low'
    >>> print(analysis.interval_multiplier)  # 1.0

    >>> # High volatility series
    >>> y_high = pd.Series([100, 50, 150, 75, 200, 60, 180, 90])
    >>> analysis = analyze_volatility(y_high)
    >>> print(analysis.volatility_level)  # 'high'
    >>> print(analysis.interval_multiplier)  # 1.5
    """
    if len(y) == 0:
        return VolatilityAnalysis(
            coefficient_of_variation=0.0,
            volatility_level="low",
            interval_multiplier=1.0,
        )

    mean_val = y.mean()
    std_val = y.std()

    # Avoid division by zero
    if mean_val == 0 or np.isnan(mean_val):
        cv = 0.0
    else:
        cv = std_val / mean_val

    # Determine level and multiplier
    if cv < 0.20:
        level = "low"
        multiplier = 1.0
    elif cv < 0.35:
        level = "medium"
        multiplier = 1.25
    else:
        level = "high"
        multiplier = 1.5

    return VolatilityAnalysis(
        coefficient_of_variation=cv,
        volatility_level=level,
        interval_multiplier=multiplier,
    )


def get_volatility_multiplier(cv: float) -> float:
    """
    Get prediction interval multiplier based on coefficient of variation.

    Parameters
    ----------
    cv : float
        Coefficient of variation (std / mean).

    Returns
    -------
    float
        Multiplier for prediction intervals (1.0, 1.25, or 1.5).
    """
    if cv < 0.20:
        return 1.0
    elif cv < 0.35:
        return 1.25
    else:
        return 1.5


def detect_recent_trend_change(
    y: pd.Series,
    short_window: int = 4,
    long_window: int = 12,
) -> Optional[str]:
    """
    Detect if recent trend direction differs from longer-term trend.

    Compares the slope of the recent `short_window` periods to the
    slope of the longer `long_window` periods.

    Parameters
    ----------
    y : pd.Series
        Time series data.
    short_window : int, default 4
        Number of recent periods for short-term trend.
    long_window : int, default 12
        Number of periods for long-term trend.

    Returns
    -------
    Optional[str]
        Description of trend change, or None if no change detected.
    """
    if len(y) < long_window:
        return None

    from scipy import stats

    # Short-term trend
    t_short = np.arange(short_window)
    y_short = y.tail(short_window).values
    short_slope, _, _, _, _ = stats.linregress(t_short, y_short)

    # Long-term trend
    t_long = np.arange(long_window)
    y_long = y.tail(long_window).values
    long_slope, _, _, _, _ = stats.linregress(t_long, y_long)

    # Normalize slopes by mean for comparison
    mean_val = y.mean()
    if mean_val > 0:
        short_rate = short_slope / mean_val
        long_rate = long_slope / mean_val
    else:
        return None

    # Detect significant change in trend direction or magnitude
    if short_rate > 0.01 and long_rate < -0.01:
        return "Trend reversal: upward (recent) vs downward (longer-term)"
    elif short_rate < -0.01 and long_rate > 0.01:
        return "Trend reversal: downward (recent) vs upward (longer-term)"
    elif abs(short_rate) > 3 * abs(long_rate) and abs(short_rate) > 0.02:
        return f"Trend acceleration: recent growth rate {abs(short_rate / long_rate):.1f}x longer-term"

    return None
