"""Main decomposition-based forecaster for ad inventory."""

from typing import Optional, Union, List, Tuple, Literal, Dict, Any

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.preprocessing import (
    log_transform,
    inverse_log_transform,
    fill_missing_dates,
    handle_outliers,
)
from ad_inventory_forecast.core.diagnostics import (
    analyze_volatility,
    VolatilityAnalysis,
)
from ad_inventory_forecast.models.trend import (
    BaseTrend,
    LinearTrend,
    ExponentialTrend,
    PolynomialTrend,
    LOWESSTrend,
    DampedTrend,
    select_best_trend,
)
from ad_inventory_forecast.seasonality.extraction import (
    DecompositionResult,
    stl_decomposition,
    mstl_decomposition,
    apply_seasonality,
    estimate_seasonal_strength,
    estimate_trend_strength,
    select_decomposition_model,
)
from ad_inventory_forecast.seasonality.fourier import FourierSeasonality


class InventoryForecaster(BaseForecaster):
    """
    Production-grade forecaster for ad inventory using decomposition.

    Implements a Growth-Seasonality Decomposition approach:
    1. Extract trend component (growth)
    2. Extract seasonal component(s)
    3. Model residuals
    4. Combine for forecast

    Supports both additive (Y = T + S + R) and multiplicative
    (Y = T * S * R) decomposition models.

    Parameters
    ----------
    model : {'additive', 'multiplicative', 'auto'}, default 'auto'
        Decomposition model type:
        - 'additive': Y = T + S + R (use when variation is constant)
        - 'multiplicative': Y = T * S * R (use when variation grows with level)
        - 'auto': Automatically select based on data characteristics
    trend : {'linear', 'exponential', 'polynomial', 'lowess', 'damped', 'auto'}, default 'auto'
        Trend model type:
        - 'linear': T(t) = α + βt
        - 'exponential': T(t) = α * exp(βt)
        - 'polynomial': T(t) = polynomial up to degree 3
        - 'lowess': Non-parametric local regression
        - 'damped': Converging asymptotic trend
        - 'auto': Automatically select best trend model
    seasonal_periods : List[int], default [7]
        Seasonal periods to extract. Common values:
        - Daily data: [7] (weekly) or [7, 365] (weekly + annual)
        - Weekly data: [52] (annual)
        - Monthly data: [12] (annual)
    trend_dampening : bool, default True
        Whether to dampen trend extrapolation for long horizons.
        Helps prevent unrealistic growth projections.
    dampen_factor : float, default 0.98
        Dampening factor φ applied to trend growth rate.
        Values < 1 cause trend to flatten over time.
    robust : bool, default True
        Whether to use robust methods for decomposition (reduces
        sensitivity to outliers).
    handle_zeros : bool, default True
        Whether to handle zero values in the data (required for
        multiplicative model).

    Attributes
    ----------
    is_fitted_ : bool
        Whether the model has been fitted.
    model_ : str
        Selected decomposition model ('additive' or 'multiplicative').
    trend_model_ : BaseTrend
        Fitted trend model.
    decomposition_ : DecompositionResult
        Result of seasonal decomposition.
    seasonal_strength_ : float
        Estimated strength of seasonality (0-1).
    trend_strength_ : float
        Estimated strength of trend (0-1).
    residual_std_ : float
        Standard deviation of residuals (for prediction intervals).

    Examples
    --------
    >>> import pandas as pd
    >>> import numpy as np
    >>> dates = pd.date_range('2024-01-01', periods=365, freq='D')
    >>> trend = np.linspace(100, 120, 365)
    >>> seasonal = 10 * np.sin(np.arange(365) * 2 * np.pi / 7)
    >>> y = pd.Series(trend + seasonal + np.random.randn(365) * 2, index=dates)
    >>> forecaster = InventoryForecaster(model='additive', seasonal_periods=[7])
    >>> forecaster.fit(y)
    >>> forecast = forecaster.predict(horizon=30)

    Notes
    -----
    For multiplicative decomposition, the series is log-transformed before
    fitting and inverse-transformed after prediction. This converts
    Y = T * S * R into log(Y) = log(T) + log(S) + log(R).
    """

    TREND_MODELS = {
        "linear": LinearTrend,
        "exponential": ExponentialTrend,
        "polynomial": PolynomialTrend,
        "lowess": LOWESSTrend,
        "damped": DampedTrend,
    }

    def __init__(
        self,
        model: Literal["additive", "multiplicative", "auto", "add", "mul"] = "auto",
        trend: Literal[
            "linear", "exponential", "polynomial", "lowess", "damped", "auto",
            "add", "mul"  # SDK aliases
        ] = "auto",
        seasonal_periods: List[int] = None,
        trend_dampening: bool = True,
        dampen_factor: float = 0.98,
        robust: bool = True,
        handle_zeros: bool = True,
    ):
        """Initialize the InventoryForecaster."""
        super().__init__()

        # Handle SDK-style aliases: trend='mul'/'add' maps to model parameter
        # This supports the SDK pattern: InventoryForecaster(trend='mul')
        if trend in ("mul", "add"):
            # SDK uses 'trend' for decomposition model type
            model = "multiplicative" if trend == "mul" else "additive"
            trend = "auto"  # Use auto for actual trend fitting

        # Normalize model aliases
        if model == "mul":
            model = "multiplicative"
        elif model == "add":
            model = "additive"

        self.model = model
        self.trend = trend
        self.seasonal_periods = seasonal_periods if seasonal_periods is not None else [7]
        self.trend_dampening = trend_dampening
        self.dampen_factor = dampen_factor
        self.robust = robust
        self.handle_zeros = handle_zeros

        # Fitted attributes
        self.model_ = None
        self.trend_model_ = None
        self.decomposition_ = None
        self.seasonal_strength_ = None
        self.trend_strength_ = None
        self.residual_std_ = None
        self.volatility_: Optional[VolatilityAnalysis] = None
        self._log_offset = 1.0
        self._last_date = None
        self._y_transformed = None
        self._model_mode = None
        self._simple_model = None
        self._baseline = None
        self.components_ = None

    def fit(
        self,
        y: Union[pd.Series, np.ndarray],
        X: Optional[Union[pd.DataFrame, np.ndarray]] = None,
    ) -> "InventoryForecaster":
        """
        Fit the forecaster to training data.

        Parameters
        ----------
        y : pd.Series or np.ndarray
            Target time series (impressions). If pd.Series, should have
            DatetimeIndex. Values must be non-negative.
        X : pd.DataFrame or np.ndarray, optional
            Exogenous features (not used in decomposition model).

        Returns
        -------
        self : InventoryForecaster
            Fitted forecaster instance.
        """
        from ad_inventory_forecast.core.validation import (
            check_data_sufficiency,
            ModelMode,
        )

        # Validate input
        y = self._validate_y(y)
        self.training_series_ = y.copy()
        self.n_obs_ = len(y)
        self.freq_ = self._infer_frequency(y)
        self._last_date = y.index[-1]

        # Check data sufficiency and set model mode
        import warnings as warn_module

        sufficiency = check_data_sufficiency(
            self.n_obs_, requested_horizon=30, frequency=self.freq_
        )
        self._model_mode = sufficiency.model_mode

        # Handle very short history with simple smoothing
        if sufficiency.model_mode == ModelMode.SIMPLE_SMOOTHING:
            warn_module.warn(
                f"Limited history ({self.n_obs_} observations < 30 days). "
                f"Using Simple Exponential Smoothing without trend or seasonality. "
                f"Forecast accuracy may be reduced for seasonal patterns.",
                UserWarning,
            )
            self._fit_simple_smoothing(y)
            return self

        # Filter seasonal periods for medium history (no yearly seasonality)
        if sufficiency.model_mode == ModelMode.NO_YEARLY_SEASONALITY:
            # Check if yearly seasonality was requested but will be filtered
            original_periods = self.seasonal_periods.copy()
            yearly_periods = [p for p in self.seasonal_periods if p > 7]

            # Only keep weekly seasonality (periods <= 7)
            self.seasonal_periods = [p for p in self.seasonal_periods if p <= 7]
            if not self.seasonal_periods:
                self.seasonal_periods = [7]

            # Always warn about limited yearly pattern detection
            warn_module.warn(
                f"Training data has {self.n_obs_} observations (< 365 days). "
                f"Yearly seasonal patterns (summer lulls, holiday spikes, annual events) "
                f"cannot be detected. Forecasts for periods with strong yearly seasonality "
                f"may have reduced accuracy. For best results, provide 12+ months of history.",
                UserWarning,
            )

            if yearly_periods:
                warn_module.warn(
                    f"Requested seasonal periods {yearly_periods} have been removed "
                    f"(insufficient data). Using weekly seasonality only.",
                    UserWarning,
                )

        # Handle missing dates
        y = fill_missing_dates(y, frequency=self.freq_, fill_method="linear")

        # Handle zeros for multiplicative model
        if self.handle_zeros and (y == 0).any():
            # Add small offset to zeros
            zero_mask = y == 0
            y = y.copy()
            y[zero_mask] = y[~zero_mask].min() * 0.1

        # Select decomposition model
        if self.model == "auto":
            self.model_ = self._select_model(y)
        else:
            self.model_ = self.model

        # Apply log transform for multiplicative model
        if self.model_ == "multiplicative":
            if (y <= 0).any():
                raise ValueError(
                    "Multiplicative model requires positive values. "
                    "Set handle_zeros=True or use additive model."
                )
            y_work = log_transform(y, offset=self._log_offset)
        else:
            y_work = y.copy()

        self._y_transformed = y_work

        # Fit trend model
        self.trend_model_ = self._fit_trend(y_work)

        # Detrend the series
        trend_values = self.trend_model_.get_fitted_values()
        detrended = y_work - trend_values

        # Extract seasonality
        self.decomposition_ = self._extract_seasonality(y_work, detrended)

        # Calculate strength metrics
        self.seasonal_strength_ = estimate_seasonal_strength(self.decomposition_)
        self.trend_strength_ = estimate_trend_strength(self.decomposition_)

        # Calculate residual statistics for prediction intervals
        residuals = self.decomposition_.residual.dropna()
        self.residual_std_ = residuals.std()

        # Volatility analysis for interval widening
        self.volatility_ = analyze_volatility(y)

        self.is_fitted_ = True
        return self

    def _fit_simple_smoothing(self, y: pd.Series) -> None:
        """Fit simple exponential smoothing for very short history (<30 days)."""
        from statsmodels.tsa.holtwinters import SimpleExpSmoothing

        self._simple_model = SimpleExpSmoothing(y.values).fit(optimized=True)
        self._baseline = self._simple_model.fittedvalues[-1]
        self.is_fitted_ = True

        # Store minimal components for compatibility
        self.components_ = {
            "trend": np.full(len(y), self._baseline),
            "seasonal": np.zeros(len(y)),
            "residual": y.values - self._baseline,
        }

        # Calculate residual std for prediction intervals
        self.residual_std_ = np.std(self.components_["residual"])

        # Volatility analysis for interval widening
        self.volatility_ = analyze_volatility(y)

        # Set model type for multiplicative/additive tracking
        self.model_ = "additive"

    def predict(
        self,
        horizon: int,
        return_conf_int: bool = False,
        alpha: float = 0.05,
        return_df: bool = True,
    ) -> Union[pd.Series, pd.DataFrame, Tuple[pd.Series, pd.DataFrame]]:
        """
        Generate forecasts for the specified horizon.

        Parameters
        ----------
        horizon : int
            Number of periods to forecast.
        return_conf_int : bool, default False
            Whether to return confidence intervals (legacy parameter).
            If True, returns tuple of (Series, DataFrame).
        alpha : float, default 0.05
            Significance level for confidence intervals (e.g., 0.05 for 95% CI).
        return_df : bool, default True
            If True (default), returns DataFrame with 'predicted_impressions',
            'lower_bound', 'upper_bound' columns (SDK-compliant format).
            If False, returns Series (legacy format).

        Returns
        -------
        forecast_df : pd.DataFrame
            If return_df=True (default): DataFrame with columns:
            - 'predicted_impressions': Point forecasts
            - 'lower_bound': Lower confidence bound
            - 'upper_bound': Upper confidence bound
        forecasts : pd.Series
            If return_df=False: Point forecasts with DatetimeIndex.
        conf_int : pd.DataFrame, optional
            If return_conf_int=True: Confidence intervals with 'lower' and
            'upper' columns. Only returned if return_conf_int=True.
        """
        import warnings as warn_module

        from ad_inventory_forecast.core.validation import (
            check_data_sufficiency,
            ModelMode,
        )

        self._check_is_fitted()

        # Check data sufficiency and cap horizon
        sufficiency = check_data_sufficiency(self.n_obs_, horizon, frequency=self.freq_)

        if sufficiency.horizon_was_capped:
            warn_module.warn(sufficiency.warning_message, UserWarning)

        effective_horizon = sufficiency.effective_horizon

        # Use simple smoothing forecast if that's what was fitted
        if self._model_mode == ModelMode.SIMPLE_SMOOTHING:
            return self._predict_simple_smoothing(
                effective_horizon, return_conf_int, alpha, return_df
            )

        # Generate forecast index
        forecast_index = self._generate_forecast_index(self._last_date, effective_horizon)

        # Project trend forward
        trend_forecast = self._forecast_trend(effective_horizon, forecast_index)

        # Apply seasonality
        seasonal_forecast = self._forecast_seasonality(effective_horizon, forecast_index)

        # Combine (additive in transformed space)
        forecast = trend_forecast + seasonal_forecast

        # Inverse transform for multiplicative model
        if self.model_ == "multiplicative":
            forecast = inverse_log_transform(forecast, offset=self._log_offset)

        # Ensure non-negative
        forecast = forecast.clip(lower=0)

        forecast_series = pd.Series(
            forecast.values, index=forecast_index, name="forecast"
        )

        # Calculate confidence intervals
        conf_int = self._calculate_confidence_intervals(
            forecast_series, effective_horizon, alpha
        )

        # Return SDK-compliant DataFrame by default
        if return_df and not return_conf_int:
            return pd.DataFrame({
                "predicted_impressions": forecast_series.values,
                "lower_bound": conf_int["lower"].values,
                "upper_bound": conf_int["upper"].values,
            }, index=forecast_index)

        # Legacy return modes
        if return_conf_int:
            return forecast_series, conf_int

        return forecast_series

    def _predict_simple_smoothing(
        self,
        horizon: int,
        return_conf_int: bool = False,
        alpha: float = 0.05,
        return_df: bool = True,
    ) -> Union[pd.Series, pd.DataFrame, Tuple[pd.Series, pd.DataFrame]]:
        """Predict using simple exponential smoothing."""
        forecast_arr = self._simple_model.forecast(horizon)

        # Build forecast index with proper pandas frequency codes
        freq_map = {"D": "D", "W": "W", "M": "MS"}
        freq = freq_map.get(self.freq_ or "D", "D")
        forecast_index = pd.date_range(
            start=self._last_date, periods=horizon + 1, freq=freq
        )[1:]

        # Convert to numpy array if needed (statsmodels may return Series or ndarray)
        if hasattr(forecast_arr, "values"):
            forecast_values = forecast_arr.values
        else:
            forecast_values = np.asarray(forecast_arr)

        forecast_series = pd.Series(
            forecast_values, index=forecast_index, name="forecast"
        )

        # Simple confidence interval based on residual std with volatility adjustment
        z = 1.96 if alpha == 0.05 else stats.norm.ppf(1 - alpha / 2)
        volatility_multiplier = 1.0
        if self.volatility_ is not None:
            volatility_multiplier = self.volatility_.interval_multiplier

        adjusted_std = self.residual_std_ * volatility_multiplier
        lower = forecast_values - z * adjusted_std
        upper = forecast_values + z * adjusted_std

        # Ensure non-negative
        lower = np.maximum(lower, 0)

        conf_int = pd.DataFrame(
            {"lower": lower, "upper": upper},
            index=forecast_index,
        )

        # Return SDK-compliant DataFrame by default
        if return_df and not return_conf_int:
            return pd.DataFrame({
                "predicted_impressions": forecast_values,
                "lower_bound": lower,
                "upper_bound": upper,
            }, index=forecast_index)

        # Legacy return modes
        if return_conf_int:
            return forecast_series, conf_int

        return forecast_series

    def _select_model(self, y: pd.Series) -> str:
        """Automatically select decomposition model type."""
        # Check for zeros (can't use multiplicative)
        if (y <= 0).any():
            return "additive"

        # Use coefficient of variation analysis
        primary_period = self.seasonal_periods[0]
        return select_decomposition_model(y, primary_period)

    def _fit_trend(self, y: pd.Series) -> BaseTrend:
        """Fit trend model to the series."""
        t = np.arange(len(y))
        values = y.values

        if self.trend == "auto":
            # Select best trend model
            trend_model = select_best_trend(values, t)
        else:
            # Use specified trend model
            trend_class = self.TREND_MODELS.get(self.trend)
            if trend_class is None:
                raise ValueError(f"Unknown trend type: {self.trend}")

            if self.trend == "polynomial":
                trend_model = trend_class(degree=2)
            else:
                trend_model = trend_class()

            trend_model.fit(values, t)

        return trend_model

    def _extract_seasonality(
        self, y: pd.Series, detrended: pd.Series
    ) -> DecompositionResult:
        """Extract seasonal component(s) from the series."""
        if len(self.seasonal_periods) == 1:
            # Single seasonal period - use STL
            period = self.seasonal_periods[0]
            decomposition = stl_decomposition(
                y, period=period, robust=self.robust
            )
        else:
            # Multiple seasonal periods - use MSTL
            decomposition = mstl_decomposition(
                y,
                periods=self.seasonal_periods,
                stl_kwargs={"robust": self.robust},
            )

        return decomposition

    def _forecast_trend(
        self, horizon: int, forecast_index: pd.DatetimeIndex
    ) -> pd.Series:
        """Project trend forward for forecast horizon."""
        n_train = self.n_obs_
        t_forecast = np.arange(n_train, n_train + horizon)

        # Get trend values
        trend_values = self.trend_model_.predict(t_forecast)

        # Apply dampening if requested
        if self.trend_dampening and horizon > 1:
            trend_values = self._apply_trend_dampening(trend_values, horizon)

        return pd.Series(trend_values, index=forecast_index, name="trend_forecast")

    def _apply_trend_dampening(
        self, trend_values: np.ndarray, horizon: int
    ) -> np.ndarray:
        """Apply dampening to trend extrapolation."""
        # Get the last known trend value and growth
        last_trend = self.trend_model_.get_fitted_values()[-1]
        first_forecast = trend_values[0]
        initial_growth = first_forecast - last_trend

        # Calculate dampened trend
        dampened = np.zeros(horizon)
        dampened[0] = first_forecast

        for h in range(1, horizon):
            # Dampen the growth rate
            dampened_growth = initial_growth * (self.dampen_factor ** h)
            # Calculate incremental change from forecast
            if h < len(trend_values):
                incremental = trend_values[h] - trend_values[h - 1]
                dampened_incremental = incremental * (self.dampen_factor ** h)
                dampened[h] = dampened[h - 1] + dampened_incremental
            else:
                dampened[h] = dampened[h - 1]

        return dampened

    def _forecast_seasonality(
        self, horizon: int, forecast_index: pd.DatetimeIndex
    ) -> pd.Series:
        """Project seasonal component(s) forward."""
        seasonal_forecast = np.zeros(horizon)

        for period in self.seasonal_periods:
            # Get seasonal indices for this period
            indices = self.decomposition_.get_seasonal_indices(period)

            # Determine starting position
            # Last observation is at position (n_obs - 1) % period
            last_position = (self.n_obs_ - 1) % period
            start_position = (last_position + 1) % period

            # Apply indices
            for h in range(horizon):
                pos = (start_position + h) % period
                seasonal_forecast[h] += indices.iloc[pos]

        return pd.Series(
            seasonal_forecast, index=forecast_index, name="seasonal_forecast"
        )

    def _calculate_confidence_intervals(
        self, forecast: pd.Series, horizon: int, alpha: float
    ) -> pd.DataFrame:
        """Calculate prediction intervals with volatility-based widening."""
        # Use residual std and normal quantiles
        z = stats.norm.ppf(1 - alpha / 2)

        # Increase uncertainty with horizon (fan out)
        h = np.arange(1, horizon + 1)
        uncertainty = self.residual_std_ * np.sqrt(h)

        # Apply volatility multiplier for high-volatility series
        volatility_multiplier = 1.0
        if self.volatility_ is not None:
            volatility_multiplier = self.volatility_.interval_multiplier

        uncertainty = uncertainty * volatility_multiplier

        # For multiplicative model, transform back
        if self.model_ == "multiplicative":
            # Approximate: multiply forecast by exp(±z*σ)
            multiplier = np.exp(z * uncertainty / forecast.values.clip(min=1))
            lower = forecast.values / multiplier
            upper = forecast.values * multiplier
        else:
            lower = forecast.values - z * uncertainty
            upper = forecast.values + z * uncertainty

        # Ensure non-negative
        lower = np.maximum(lower, 0)

        return pd.DataFrame(
            {"lower": lower, "upper": upper},
            index=forecast.index,
        )

    def get_components(self) -> Dict[str, pd.Series]:
        """
        Get decomposition components.

        Returns
        -------
        components : dict
            Dictionary with 'observed', 'trend', 'seasonal', 'residual' keys.
        """
        self._check_is_fitted()

        components = {
            "observed": self.decomposition_.observed,
            "trend": self.decomposition_.trend,
            "residual": self.decomposition_.residual,
        }

        # Handle multiple seasonal components
        if isinstance(self.decomposition_.seasonal, pd.DataFrame):
            for col in self.decomposition_.seasonal.columns:
                components[col] = self.decomposition_.seasonal[col]
            # Also add total seasonal
            components["seasonal"] = self.decomposition_.seasonal.sum(axis=1)
        else:
            components["seasonal"] = self.decomposition_.seasonal

        return components

    def get_diagnostics(self) -> Dict[str, Any]:
        """
        Get diagnostic information about the fitted model.

        Returns
        -------
        diagnostics : dict
            Dictionary with model diagnostics including strength metrics,
            residual statistics, model selections, and volatility analysis.
        """
        self._check_is_fitted()

        # Handle simple smoothing case where decomposition_ is not set
        if self.decomposition_ is not None:
            residuals = self.decomposition_.residual.dropna()
            residual_mean = residuals.mean()
            residual_skewness = stats.skew(residuals)
            residual_kurtosis = stats.kurtosis(residuals)
        else:
            residual_mean = 0.0
            residual_skewness = 0.0
            residual_kurtosis = 0.0

        diagnostics = {
            "model": self.model_,
            "trend_type": type(self.trend_model_).__name__ if self.trend_model_ else "SimpleSmoothing",
            "seasonal_periods": self.seasonal_periods,
            "n_observations": self.n_obs_,
            "frequency": self.freq_,
            "trend_strength": self.trend_strength_,
            "seasonal_strength": self.seasonal_strength_,
            "residual_mean": residual_mean,
            "residual_std": self.residual_std_,
            "residual_skewness": residual_skewness,
            "residual_kurtosis": residual_kurtosis,
        }

        # Add volatility diagnostics
        if self.volatility_ is not None:
            diagnostics["volatility_cv"] = self.volatility_.coefficient_of_variation
            diagnostics["volatility_level"] = self.volatility_.volatility_level
            diagnostics["interval_multiplier"] = self.volatility_.interval_multiplier

        return diagnostics

    def plot_components(self):
        """
        Plot decomposition components.

        Returns
        -------
        fig : matplotlib.figure.Figure
            Figure with four subplots: Observed, Trend, Seasonal, Residual.

        Raises
        ------
        ImportError
            If matplotlib is not installed.
        """
        self._check_is_fitted()

        try:
            import matplotlib.pyplot as plt
        except ImportError:
            raise ImportError("matplotlib is required for plotting")

        components = self.get_components()

        fig, axes = plt.subplots(4, 1, figsize=(12, 10), sharex=True)

        # Plot observed
        axes[0].plot(components["observed"], color="black", linewidth=0.8)
        axes[0].set_title("Observed")
        axes[0].set_ylabel("Value")

        # Plot trend
        axes[1].plot(components["trend"], color="blue", linewidth=1.2)
        axes[1].set_title(f"Trend ({type(self.trend_model_).__name__})")
        axes[1].set_ylabel("Value")

        # Plot seasonal
        axes[2].plot(components["seasonal"], color="green", linewidth=0.8)
        axes[2].set_title(f"Seasonal (periods={self.seasonal_periods})")
        axes[2].set_ylabel("Value")
        axes[2].axhline(y=0, color="gray", linestyle="--", linewidth=0.5)

        # Plot residual
        axes[3].plot(components["residual"], color="red", linewidth=0.5)
        axes[3].set_title("Residual")
        axes[3].set_ylabel("Value")
        axes[3].set_xlabel("Date")
        axes[3].axhline(y=0, color="gray", linestyle="--", linewidth=0.5)

        # Add strength annotations
        fig.text(
            0.02,
            0.98,
            f"Trend strength: {self.trend_strength_:.2f}\n"
            f"Seasonal strength: {self.seasonal_strength_:.2f}",
            transform=fig.transFigure,
            fontsize=10,
            verticalalignment="top",
        )

        plt.tight_layout()
        return fig

    def plot_forecast(
        self,
        forecast: pd.Series,
        conf_int: Optional[pd.DataFrame] = None,
        history_periods: int = 90,
    ):
        """
        Plot forecast with historical data.

        Parameters
        ----------
        forecast : pd.Series
            Forecast values from predict().
        conf_int : pd.DataFrame, optional
            Confidence intervals from predict(return_conf_int=True).
        history_periods : int, default 90
            Number of historical periods to show.

        Returns
        -------
        fig : matplotlib.figure.Figure
            Forecast plot.
        """
        self._check_is_fitted()

        try:
            import matplotlib.pyplot as plt
        except ImportError:
            raise ImportError("matplotlib is required for plotting")

        fig, ax = plt.subplots(figsize=(12, 6))

        # Plot historical data
        history = self.training_series_.iloc[-history_periods:]
        ax.plot(history.index, history.values, color="black", label="Historical")

        # Plot forecast
        ax.plot(
            forecast.index,
            forecast.values,
            color="blue",
            linewidth=2,
            label="Forecast",
        )

        # Plot confidence intervals
        if conf_int is not None:
            ax.fill_between(
                forecast.index,
                conf_int["lower"],
                conf_int["upper"],
                color="blue",
                alpha=0.2,
                label="95% CI",
            )

        ax.set_title("Ad Inventory Forecast")
        ax.set_xlabel("Date")
        ax.set_ylabel("Impressions")
        ax.legend()

        plt.tight_layout()
        return fig
