"""Model advisor for automatic forecaster selection."""

from dataclasses import dataclass, field
from typing import Optional, List, Dict, Any, Tuple, Type, Literal

import numpy as np
import pandas as pd
from scipy import stats

from ad_inventory_forecast.core.base import BaseForecaster
from ad_inventory_forecast.core.validation import (
    check_data_sufficiency,
    ModelMode,
)
from ad_inventory_forecast.core.diagnostics import (
    StructuralBreakWarning,
    VolatilityAnalysis,
    detect_structural_break,
    analyze_volatility,
    detect_recent_trend_change,
)


@dataclass
class ModelRecommendation:
    """
    Recommendation for a single model.

    Attributes
    ----------
    model_name : str
        Name of the model (e.g., 'ETSForecaster').
    model_class : Type[BaseForecaster]
        The model class to instantiate.
    recommended_params : Dict[str, Any]
        Recommended parameters for this model.
    suitability : str
        One of 'recommended', 'possible', 'not_recommended'.
    reason : str
        Explanation for the suitability rating.
    warnings : List[str]
        Any warnings about using this model.
    """

    model_name: str
    model_class: Type[BaseForecaster]
    recommended_params: Dict[str, Any]
    suitability: Literal["recommended", "possible", "not_recommended"]
    reason: str
    warnings: List[str] = field(default_factory=list)


@dataclass
class DataAnalysis:
    """
    Analysis of input data characteristics.

    Attributes
    ----------
    n_observations : int
        Number of observations in the data.
    frequency : str
        Detected frequency ('D', 'W', 'M').
    equivalent_days : int
        Observations converted to day-equivalent.
    date_range : Tuple[str, str]
        Start and end dates.
    has_zeros : bool
        Whether the data contains zeros.
    has_negative : bool
        Whether the data contains negative values.
    trend_detected : bool
        Whether a significant trend was detected.
    trend_direction : Optional[str]
        'increasing', 'decreasing', or 'flat'.
    seasonal_strength : Optional[float]
        Estimated strength of seasonality (0-1).
    valid_seasonal_periods : List[int]
        Valid seasonal periods based on data length.
    model_mode : ModelMode
        Recommended model complexity mode.
    volatility : Optional[VolatilityAnalysis]
        Volatility analysis results (CV, level, interval multiplier).
    structural_break : Optional[StructuralBreakWarning]
        Structural break detection results.
    trend_change : Optional[str]
        Description of recent trend change, if detected.
    warnings : List[str]
        Warnings about data quality or limitations.
    """

    n_observations: int
    frequency: str
    equivalent_days: int
    date_range: Tuple[str, str]
    has_zeros: bool
    has_negative: bool
    trend_detected: bool
    trend_direction: Optional[str]
    seasonal_strength: Optional[float]
    valid_seasonal_periods: List[int]
    model_mode: ModelMode
    volatility: Optional[VolatilityAnalysis] = None
    structural_break: Optional[StructuralBreakWarning] = None
    trend_change: Optional[str] = None
    warnings: List[str] = field(default_factory=list)


@dataclass
class ModelAdvice:
    """
    Complete advice from ModelAdvisor.

    Attributes
    ----------
    data_analysis : DataAnalysis
        Analysis of the input data.
    recommendations : List[ModelRecommendation]
        Recommendations for each model type.
    selected_model : str
        Name of the recommended model.
    selected_params : Dict[str, Any]
        Parameters for the recommended model.
    selection_reasoning : str
        Explanation for why this model was selected.
    """

    data_analysis: DataAnalysis
    recommendations: List[ModelRecommendation]
    selected_model: str
    selected_params: Dict[str, Any]
    selection_reasoning: str


class ModelAdvisor:
    """
    Analyzes time series data and recommends appropriate forecasting models.

    This class provides structured recommendations that can be used by:
    - Wrapper applications to present choices to users
    - AutoForecaster for automatic model selection

    The advisor evaluates each available model based on:
    - Data length and frequency
    - Seasonal period validity
    - Model-specific requirements
    - Computational complexity

    Examples
    --------
    >>> advisor = ModelAdvisor()
    >>> advice = advisor.analyze(y)
    >>> print(advice.data_analysis)
    >>> print(advice.selected_model)
    >>> for rec in advice.recommendations:
    ...     print(f"{rec.model_name}: {rec.suitability} - {rec.reason}")
    """

    # Model requirements: min_obs, min_for_seasonality_factor, notes
    MODEL_REQUIREMENTS = {
        "InventoryForecaster": {
            "min_obs": 7,
            "seasonal_factor": 2,
            "notes": "Handles all cases with automatic fallback",
        },
        "ETSForecaster": {
            "min_obs": 14,
            "seasonal_factor": 2,
            "notes": "Requires 2*period observations for seasonality",
        },
        "SARIMAForecaster": {
            "min_obs": 30,
            "seasonal_factor": 3,
            "notes": "Auto-search can be expensive",
        },
        "MLEnsembleForecaster": {
            "min_obs": 90,
            "seasonal_factor": 4,
            "notes": "Needs lag features, recursive forecasting",
        },
    }

    def __init__(self, verbose: bool = False):
        """
        Initialize the ModelAdvisor.

        Parameters
        ----------
        verbose : bool, default False
            Whether to print analysis details.
        """
        self.verbose = verbose

    def analyze(self, y: pd.Series) -> ModelAdvice:
        """
        Analyze data and return model recommendations.

        Parameters
        ----------
        y : pd.Series
            Time series data with DatetimeIndex.

        Returns
        -------
        ModelAdvice
            Complete advice including data analysis and recommendations.
        """
        # Analyze data characteristics
        analysis = self._analyze_data(y)

        if self.verbose:
            self._print_analysis(analysis)

        # Evaluate each model
        recommendations = self._evaluate_models(analysis)

        if self.verbose:
            self._print_recommendations(recommendations)

        # Select best model
        selected_name, selected_params, reasoning = self._select_best(
            recommendations, analysis
        )

        if self.verbose:
            print(f"\nSelected: {selected_name}")
            print(f"Reason: {reasoning}")

        return ModelAdvice(
            data_analysis=analysis,
            recommendations=recommendations,
            selected_model=selected_name,
            selected_params=selected_params,
            selection_reasoning=reasoning,
        )

    def _analyze_data(self, y: pd.Series) -> DataAnalysis:
        """Analyze data characteristics."""
        n_obs = len(y)
        warnings = []

        # Infer frequency
        frequency = self._infer_frequency(y)

        # Calculate equivalent days
        if frequency == "M":
            equivalent_days = n_obs * 30
        elif frequency == "W":
            equivalent_days = n_obs * 7
        else:
            equivalent_days = n_obs

        # Date range
        date_range = (str(y.index.min().date()), str(y.index.max().date()))

        # Data quality checks
        has_zeros = bool((y == 0).any())
        has_negative = bool((y < 0).any())

        if has_negative:
            warnings.append("Data contains negative values - multiplicative models not available")
        if has_zeros:
            pct_zeros = (y == 0).sum() / len(y) * 100
            if pct_zeros > 10:
                warnings.append(f"Data is {pct_zeros:.1f}% zeros - consider intermittent demand models")

        # Trend detection
        trend_detected, trend_direction = self._detect_trend(y)

        # Seasonal strength estimation
        seasonal_strength = self._estimate_seasonal_strength(y, frequency)

        # Valid seasonal periods
        valid_seasonal_periods = self._get_valid_seasonal_periods(n_obs, frequency)

        # Model mode from validation
        sufficiency = check_data_sufficiency(n_obs, requested_horizon=30, frequency=frequency)

        if sufficiency.model_mode == ModelMode.SIMPLE_SMOOTHING:
            warnings.append(
                f"Limited history ({n_obs} observations, ~{equivalent_days} days). "
                f"Simple exponential smoothing will be used."
            )
        elif sufficiency.model_mode == ModelMode.NO_YEARLY_SEASONALITY:
            warnings.append(
                f"Medium history ({n_obs} observations, ~{equivalent_days} days). "
                f"Yearly seasonal patterns cannot be detected."
            )

        # Volatility analysis
        volatility = analyze_volatility(y)
        if volatility.volatility_level == "high":
            warnings.append(
                f"High volatility detected (CV={volatility.coefficient_of_variation:.0%}). "
                f"Prediction intervals will be widened."
            )
        elif volatility.volatility_level == "medium":
            warnings.append(
                f"Medium volatility detected (CV={volatility.coefficient_of_variation:.0%}). "
                f"Prediction intervals will be moderately widened."
            )

        # Structural break detection
        # Use 8 periods for weekly data, 4 for monthly
        break_window = 8 if frequency in ("D", "W") else 4
        structural_break = detect_structural_break(y, window=break_window)
        if structural_break.detected:
            warnings.append(structural_break.message)

        # Trend change detection
        trend_change = detect_recent_trend_change(y)
        if trend_change:
            warnings.append(f"Trend change detected: {trend_change}")

        return DataAnalysis(
            n_observations=n_obs,
            frequency=frequency,
            equivalent_days=equivalent_days,
            date_range=date_range,
            has_zeros=has_zeros,
            has_negative=has_negative,
            trend_detected=trend_detected,
            trend_direction=trend_direction,
            seasonal_strength=seasonal_strength,
            valid_seasonal_periods=valid_seasonal_periods,
            model_mode=sufficiency.model_mode,
            volatility=volatility,
            structural_break=structural_break,
            trend_change=trend_change,
            warnings=warnings,
        )

    def _infer_frequency(self, y: pd.Series) -> str:
        """Infer time series frequency."""
        if y.index.freq is not None:
            freq_str = y.index.freq.name
            if freq_str in ("D", "B"):
                return "D"
            elif freq_str in ("W", "W-SUN", "W-MON"):
                return "W"
            elif freq_str in ("M", "MS", "ME"):
                return "M"

        # Infer from median difference
        diffs = y.index.to_series().diff().dropna()
        if len(diffs) == 0:
            return "D"

        median_diff = diffs.median()

        if median_diff <= pd.Timedelta(days=2):
            return "D"
        elif median_diff <= pd.Timedelta(days=10):
            return "W"
        else:
            return "M"

    def _detect_trend(self, y: pd.Series) -> Tuple[bool, Optional[str]]:
        """Detect trend presence and direction."""
        t = np.arange(len(y))
        try:
            slope, _, r_value, p_value, _ = stats.linregress(t, y.values)

            if p_value < 0.05 and abs(r_value) > 0.3:
                direction = "increasing" if slope > 0 else "decreasing"
                return True, direction
        except Exception:
            pass

        return False, "flat"

    def _estimate_seasonal_strength(self, y: pd.Series, frequency: str) -> Optional[float]:
        """Estimate seasonal strength."""
        # Determine primary period
        if frequency == "D":
            period = 7
        elif frequency == "W":
            period = 52
        else:
            period = 12

        if len(y) < 2 * period:
            return None

        try:
            from statsmodels.tsa.stattools import acf
            acf_values = acf(y.values, nlags=period + 1, fft=True)
            seasonal_acf = abs(acf_values[period])
            return min(seasonal_acf, 1.0)
        except Exception:
            return None

    def _get_valid_seasonal_periods(self, n_obs: int, frequency: str) -> List[int]:
        """Determine valid seasonal periods based on data length."""
        periods = []

        if frequency == "D":
            if n_obs >= 14:  # 2 weeks
                periods.append(7)  # Weekly
            if n_obs >= 730:  # 2 years
                periods.append(365)  # Yearly
        elif frequency == "W":
            if n_obs >= 104:  # 2 years
                periods.append(52)  # Yearly
        elif frequency == "M":
            if n_obs >= 24:  # 2 years
                periods.append(12)  # Yearly

        return periods

    def _evaluate_models(self, analysis: DataAnalysis) -> List[ModelRecommendation]:
        """Evaluate each model's suitability for the data."""
        recommendations = []

        # Evaluate InventoryForecaster
        recommendations.append(self._evaluate_inventory_forecaster(analysis))

        # Evaluate ETSForecaster
        recommendations.append(self._evaluate_ets_forecaster(analysis))

        # Evaluate SARIMAForecaster
        recommendations.append(self._evaluate_sarima_forecaster(analysis))

        # Evaluate MLEnsembleForecaster
        recommendations.append(self._evaluate_ml_ensemble_forecaster(analysis))

        return recommendations

    def _evaluate_inventory_forecaster(self, analysis: DataAnalysis) -> ModelRecommendation:
        """Evaluate InventoryForecaster suitability."""
        from ad_inventory_forecast.models.decomposition import InventoryForecaster

        warnings = []
        params = {"model": "auto", "trend": "auto"}

        # Determine seasonal periods
        if analysis.valid_seasonal_periods:
            params["seasonal_periods"] = analysis.valid_seasonal_periods
        else:
            params["seasonal_periods"] = [7]

        # Check frequency compatibility
        if analysis.frequency == "M" and 7 in params.get("seasonal_periods", []):
            suitability = "not_recommended"
            reason = "Weekly seasonality (period=7) invalid for monthly data"
            warnings.append("Use ETSForecaster instead for monthly data")
        elif analysis.model_mode == ModelMode.SIMPLE_SMOOTHING:
            suitability = "possible"
            reason = "Will fall back to simple smoothing due to short history"
        elif analysis.n_observations < 30:
            suitability = "possible"
            reason = "Limited history, using simplified model"
        else:
            suitability = "recommended"
            reason = "Robust decomposition model with automatic fallback"

        return ModelRecommendation(
            model_name="InventoryForecaster",
            model_class=InventoryForecaster,
            recommended_params=params,
            suitability=suitability,
            reason=reason,
            warnings=warnings,
        )

    def _evaluate_ets_forecaster(self, analysis: DataAnalysis) -> ModelRecommendation:
        """Evaluate ETSForecaster suitability."""
        from ad_inventory_forecast.models.ets import ETSForecaster

        warnings = []
        params = {}

        # Trend configuration
        if analysis.trend_detected:
            params["trend"] = "add"
            params["damped_trend"] = True
        else:
            params["trend"] = None
            params["damped_trend"] = False

        # Seasonal configuration
        if analysis.valid_seasonal_periods and analysis.n_observations >= 2 * analysis.valid_seasonal_periods[0]:
            params["seasonal"] = "add"
            params["seasonal_periods"] = analysis.valid_seasonal_periods[0]
        else:
            params["seasonal"] = None
            params["seasonal_periods"] = None

        # Suitability assessment
        if analysis.n_observations < 14:
            suitability = "not_recommended"
            reason = f"Insufficient data ({analysis.n_observations} < 14 observations)"
        elif analysis.frequency == "M" and analysis.n_observations >= 12:
            suitability = "recommended"
            reason = "Good fit for monthly data with damped trend"
            if analysis.n_observations < 24:
                params["seasonal"] = None
                warnings.append("No seasonal component - need 24+ months for yearly seasonality")
        elif analysis.n_observations >= 30:
            suitability = "recommended"
            reason = "Good general-purpose model with automatic component selection"
        else:
            suitability = "possible"
            reason = "May work but limited by short history"

        return ModelRecommendation(
            model_name="ETSForecaster",
            model_class=ETSForecaster,
            recommended_params=params,
            suitability=suitability,
            reason=reason,
            warnings=warnings,
        )

    def _evaluate_sarima_forecaster(self, analysis: DataAnalysis) -> ModelRecommendation:
        """Evaluate SARIMAForecaster suitability."""
        from ad_inventory_forecast.models.sarima import SARIMAForecaster

        warnings = []
        params = {"auto": True}

        # Seasonal period
        if analysis.valid_seasonal_periods:
            params["seasonal_periods"] = analysis.valid_seasonal_periods[0]
        elif analysis.frequency == "D":
            params["seasonal_periods"] = 7
        elif analysis.frequency == "W":
            params["seasonal_periods"] = 52
        else:
            params["seasonal_periods"] = 12

        # Suitability assessment
        if analysis.n_observations < 30:
            suitability = "not_recommended"
            reason = f"Insufficient data ({analysis.n_observations} < 30 observations)"
        elif analysis.n_observations < 60:
            suitability = "possible"
            reason = "May work but auto-selection may be unreliable"
            warnings.append("Consider using stepwise=True for faster fitting")
        else:
            suitability = "possible"
            reason = "Statistical model with automatic order selection"
            warnings.append("Can be slow for large datasets")

        return ModelRecommendation(
            model_name="SARIMAForecaster",
            model_class=SARIMAForecaster,
            recommended_params=params,
            suitability=suitability,
            reason=reason,
            warnings=warnings,
        )

    def _evaluate_ml_ensemble_forecaster(self, analysis: DataAnalysis) -> ModelRecommendation:
        """Evaluate MLEnsembleForecaster suitability."""
        try:
            from ad_inventory_forecast.models.ml_ensemble import MLEnsembleForecaster
            has_ml = True
        except Exception:
            # Catch all exceptions including ImportError and XGBoostError
            has_ml = False
            MLEnsembleForecaster = None

        warnings = []
        params = {
            "model_type": "xgboost",
            "lag_features": [1, 7, 14],
            "rolling_windows": [7, 14],
        }

        if not has_ml:
            return ModelRecommendation(
                model_name="MLEnsembleForecaster",
                model_class=None,
                recommended_params=params,
                suitability="not_recommended",
                reason="XGBoost not installed",
                warnings=["Install xgboost: pip install xgboost"],
            )

        from ad_inventory_forecast.models.ml_ensemble import MLEnsembleForecaster

        # Adjust lags based on data length
        max_lag = min(28, analysis.n_observations // 4)
        params["lag_features"] = [l for l in [1, 7, 14, 28] if l <= max_lag]
        params["rolling_windows"] = [w for w in [7, 14, 28] if w <= max_lag]

        # Suitability assessment
        min_required = max(params["lag_features"]) + max(params["rolling_windows"]) + 10
        if analysis.n_observations < min_required:
            suitability = "not_recommended"
            reason = f"Insufficient data for lag features ({analysis.n_observations} < {min_required})"
        elif analysis.n_observations < 90:
            suitability = "possible"
            reason = "Limited data for ML model, may overfit"
            warnings.append("Consider simpler models for better generalization")
        else:
            suitability = "possible"
            reason = "ML ensemble with feature engineering"
            warnings.append("Recursive forecasting can accumulate errors")

        return ModelRecommendation(
            model_name="MLEnsembleForecaster",
            model_class=MLEnsembleForecaster,
            recommended_params=params,
            suitability=suitability,
            reason=reason,
            warnings=warnings,
        )

    def _select_best(
        self,
        recommendations: List[ModelRecommendation],
        analysis: DataAnalysis,
    ) -> Tuple[str, Dict[str, Any], str]:
        """Select the best model from recommendations."""
        # Priority order based on suitability
        recommended = [r for r in recommendations if r.suitability == "recommended"]
        possible = [r for r in recommendations if r.suitability == "possible"]

        # For monthly data, prefer ETS
        if analysis.frequency == "M":
            for rec in recommended:
                if rec.model_name == "ETSForecaster":
                    return (
                        rec.model_name,
                        rec.recommended_params,
                        f"Best fit for monthly data: {rec.reason}",
                    )

        # For very short history, use InventoryForecaster (has fallback)
        if analysis.model_mode == ModelMode.SIMPLE_SMOOTHING:
            for rec in recommendations:
                if rec.model_name == "InventoryForecaster":
                    return (
                        rec.model_name,
                        rec.recommended_params,
                        "Short history - using InventoryForecaster with simple smoothing fallback",
                    )

        # Return first recommended
        if recommended:
            rec = recommended[0]
            return rec.model_name, rec.recommended_params, rec.reason

        # Return first possible
        if possible:
            rec = possible[0]
            return rec.model_name, rec.recommended_params, f"Best available option: {rec.reason}"

        # Fallback to InventoryForecaster
        for rec in recommendations:
            if rec.model_name == "InventoryForecaster":
                return (
                    rec.model_name,
                    rec.recommended_params,
                    "Fallback to InventoryForecaster",
                )

        # Should never reach here
        return "InventoryForecaster", {"model": "auto"}, "Default fallback"

    def _print_analysis(self, analysis: DataAnalysis) -> None:
        """Print data analysis summary."""
        print("Analyzing data...")
        print(f"  Observations: {analysis.n_observations}")
        freq_names = {"D": "Daily", "W": "Weekly", "M": "Monthly"}
        print(f"  Frequency: {freq_names.get(analysis.frequency, analysis.frequency)} ({analysis.frequency})")
        print(f"  Equivalent days: {analysis.equivalent_days}")
        print(f"  Date range: {analysis.date_range[0]} to {analysis.date_range[1]}")
        if analysis.trend_detected:
            print(f"  Trend: Detected ({analysis.trend_direction})")
        else:
            print("  Trend: Not detected")
        if analysis.seasonal_strength is not None:
            print(f"  Seasonal strength: {analysis.seasonal_strength:.2f}")
        print(f"  Valid seasonal periods: {analysis.valid_seasonal_periods}")
        print(f"  Model mode: {analysis.model_mode.name}")
        # Volatility analysis
        if analysis.volatility:
            print(f"  Volatility: {analysis.volatility.volatility_level} "
                  f"(CV={analysis.volatility.coefficient_of_variation:.1%}, "
                  f"interval multiplier={analysis.volatility.interval_multiplier}x)")
        # Structural break
        if analysis.structural_break and analysis.structural_break.detected:
            print(f"  Structural break: Detected ({analysis.structural_break.pct_change:+.0%} change)")
        # Trend change
        if analysis.trend_change:
            print(f"  Trend change: {analysis.trend_change}")
        if analysis.warnings:
            print("  Warnings:")
            for w in analysis.warnings:
                print(f"    - {w}")

    def _print_recommendations(self, recommendations: List[ModelRecommendation]) -> None:
        """Print model recommendations."""
        print("\nEvaluating models...")
        for rec in recommendations:
            print(f"  {rec.model_name}: {rec.suitability}")
            print(f"    - {rec.reason}")
            for w in rec.warnings:
                print(f"    ! {w}")
