"""Data validation utilities for ad inventory forecasting."""

from dataclasses import dataclass, field
from enum import Enum
from typing import List, Optional, Dict, Any, Tuple

import numpy as np
import pandas as pd


class ModelMode(Enum):
    """Model mode based on data history length."""

    SIMPLE_SMOOTHING = "simple"  # < 30 days: Level only, no trend/seasonality
    NO_YEARLY_SEASONALITY = "linear"  # 30-365 days: Level + trend, weekly seasonality only
    FULL_DECOMPOSITION = "full"  # > 365 days: Level + trend + yearly seasonality


@dataclass
class DataSufficiencyResult:
    """Result of data sufficiency check."""

    history_days: int
    requested_horizon: int
    effective_horizon: int
    model_mode: ModelMode
    horizon_was_capped: bool
    warning_message: Optional[str] = None


def check_data_sufficiency(
    history_length: int,
    requested_horizon: int,
    max_horizon_ratio: float = 0.5,
    frequency: Optional[str] = None,
) -> DataSufficiencyResult:
    """
    Check data sufficiency and determine appropriate model mode.

    Parameters
    ----------
    history_length : int
        Number of observations in training data.
    requested_horizon : int
        User-requested forecast horizon.
    max_horizon_ratio : float, default 0.5
        Maximum horizon as fraction of history.
    frequency : str, optional
        Data frequency ('D', 'W', 'M'). If provided, thresholds are
        adjusted accordingly. For monthly data, 21 observations = 21 months.

    Returns
    -------
    DataSufficiencyResult
        Contains effective_horizon, model_mode, and any warnings.

    Examples
    --------
    >>> result = check_data_sufficiency(20, requested_horizon=30)
    >>> result.model_mode
    ModelMode.SIMPLE_SMOOTHING
    >>> result.effective_horizon
    10

    >>> # Monthly data: 21 months is sufficient for full decomposition
    >>> result = check_data_sufficiency(21, requested_horizon=3, frequency='M')
    >>> result.model_mode
    ModelMode.FULL_DECOMPOSITION
    """
    # Convert observations to equivalent days based on frequency
    # This ensures thresholds work correctly for weekly/monthly data
    if frequency == "M":
        # Monthly: each observation ≈ 30 days
        equivalent_days = history_length * 30
    elif frequency == "W":
        # Weekly: each observation = 7 days
        equivalent_days = history_length * 7
    else:
        # Daily or unknown: 1:1
        equivalent_days = history_length

    # Horizon capping (based on observations, not equivalent days)
    max_horizon = int(history_length * max_horizon_ratio)
    horizon_capped = requested_horizon > max_horizon
    effective_horizon = min(requested_horizon, max_horizon) if max_horizon > 0 else 1

    # Model mode selection (based on equivalent days)
    if equivalent_days < 30:
        model_mode = ModelMode.SIMPLE_SMOOTHING
    elif equivalent_days < 365:
        model_mode = ModelMode.NO_YEARLY_SEASONALITY
    else:
        model_mode = ModelMode.FULL_DECOMPOSITION

    # Warning message
    warning = None
    if horizon_capped:
        warning = (
            f"Requested horizon ({requested_horizon}) exceeds safety limit. "
            f"Capped to {effective_horizon} ({max_horizon_ratio:.0%} of {history_length} observations)."
        )

    return DataSufficiencyResult(
        history_days=equivalent_days,
        requested_horizon=requested_horizon,
        effective_horizon=effective_horizon,
        model_mode=model_mode,
        horizon_was_capped=horizon_capped,
        warning_message=warning,
    )


@dataclass
class ValidationResult:
    """
    Result of data validation.

    Attributes
    ----------
    is_valid : bool
        Whether the data passed all validation checks.
    errors : List[str]
        List of error messages (blocking issues).
    warnings : List[str]
        List of warning messages (non-blocking issues).
    metadata : Dict[str, Any]
        Additional metadata about the validated data.
    """

    is_valid: bool
    errors: List[str] = field(default_factory=list)
    warnings: List[str] = field(default_factory=list)
    metadata: Dict[str, Any] = field(default_factory=dict)

    def __repr__(self) -> str:
        """Return string representation."""
        status = "VALID" if self.is_valid else "INVALID"
        return (
            f"ValidationResult({status}, "
            f"errors={len(self.errors)}, warnings={len(self.warnings)})"
        )

    def raise_if_invalid(self) -> None:
        """Raise ValueError if validation failed."""
        if not self.is_valid:
            error_msg = "; ".join(self.errors)
            raise ValueError(f"Data validation failed: {error_msg}")


class DataValidator:
    """
    Validates input data for ad inventory forecasting.

    Parameters
    ----------
    date_col : str, default 'date'
        Name of the date column.
    target_col : str, default 'impressions'
        Name of the target column.
    ad_unit_col : str, default 'ad_unit'
        Name of the ad unit column.
    min_history : int, default 180
        Minimum number of observations required.
    check_frequency : bool, default True
        Whether to check for consistent frequency.
    frequency_tolerance : float, default 0.1
        Tolerance for frequency consistency (as fraction of mean).

    Examples
    --------
    >>> validator = DataValidator()
    >>> result = validator.validate(df)
    >>> if result.is_valid:
    ...     print("Data is valid!")
    >>> else:
    ...     print("Errors:", result.errors)
    """

    # Minimum history requirements by granularity
    MIN_HISTORY = {
        "D": 180,  # 6 months of daily data
        "W": 26,   # 6 months of weekly data
        "M": 12,   # 12 months of monthly data
    }

    def __init__(
        self,
        date_col: str = "date",
        target_col: str = "impressions",
        ad_unit_col: str = "ad_unit",
        min_history: Optional[int] = None,
        check_frequency: bool = True,
        frequency_tolerance: float = 0.1,
    ):
        """Initialize the validator."""
        self.date_col = date_col
        self.target_col = target_col
        self.ad_unit_col = ad_unit_col
        self.min_history = min_history
        self.check_frequency = check_frequency
        self.frequency_tolerance = frequency_tolerance

    def validate(
        self,
        data: pd.DataFrame,
        require_ad_unit: bool = True,
    ) -> ValidationResult:
        """
        Validate input DataFrame.

        Parameters
        ----------
        data : pd.DataFrame
            Input data to validate.
        require_ad_unit : bool, default True
            Whether to require the ad_unit column.

        Returns
        -------
        result : ValidationResult
            Validation result with errors, warnings, and metadata.
        """
        errors = []
        warnings = []
        metadata = {}

        # Check that input is a DataFrame
        if not isinstance(data, pd.DataFrame):
            errors.append(f"Input must be a DataFrame, got {type(data).__name__}")
            return ValidationResult(is_valid=False, errors=errors)

        # Check for empty DataFrame
        if len(data) == 0:
            errors.append("DataFrame is empty")
            return ValidationResult(is_valid=False, errors=errors)

        # Check required columns
        required_cols = [self.date_col, self.target_col]
        if require_ad_unit:
            required_cols.append(self.ad_unit_col)

        missing_cols = [col for col in required_cols if col not in data.columns]
        if missing_cols:
            errors.append(f"Missing required columns: {missing_cols}")
            return ValidationResult(is_valid=False, errors=errors)

        # Validate date column
        date_errors, date_warnings, dates = self._validate_dates(data)
        errors.extend(date_errors)
        warnings.extend(date_warnings)

        if date_errors:
            return ValidationResult(is_valid=False, errors=errors, warnings=warnings)

        # Validate target column
        target_errors, target_warnings = self._validate_target(data)
        errors.extend(target_errors)
        warnings.extend(target_warnings)

        # Infer frequency and check minimum history
        freq, freq_warnings = self._infer_and_validate_frequency(data, dates)
        warnings.extend(freq_warnings)
        metadata["frequency"] = freq

        # Check sufficient history per series
        history_errors, history_warnings = self._validate_history(
            data, require_ad_unit, freq
        )
        errors.extend(history_errors)
        warnings.extend(history_warnings)

        # Check frequency consistency
        if self.check_frequency and not errors:
            freq_errors, freq_consistency_warnings = self._validate_frequency_consistency(
                data, dates, require_ad_unit
            )
            warnings.extend(freq_consistency_warnings)

        # Collect metadata
        metadata["n_rows"] = len(data)
        metadata["date_range"] = (dates.min(), dates.max())

        if require_ad_unit and self.ad_unit_col in data.columns:
            metadata["n_ad_units"] = data[self.ad_unit_col].nunique()
            metadata["ad_units"] = data[self.ad_unit_col].unique().tolist()

        is_valid = len(errors) == 0
        return ValidationResult(
            is_valid=is_valid,
            errors=errors,
            warnings=warnings,
            metadata=metadata,
        )

    def _validate_dates(
        self, data: pd.DataFrame
    ) -> Tuple[List[str], List[str], pd.Series]:
        """Validate and parse the date column."""
        errors = []
        warnings = []

        try:
            dates = pd.to_datetime(data[self.date_col])
        except Exception as e:
            errors.append(f"Cannot parse date column: {e}")
            return errors, warnings, pd.Series()

        # Check for null dates
        null_dates = dates.isna().sum()
        if null_dates > 0:
            errors.append(f"Date column contains {null_dates} null values")

        # Check for duplicate dates (per ad_unit if present)
        if self.ad_unit_col in data.columns:
            duplicates = data.groupby(self.ad_unit_col).apply(
                lambda x: x[self.date_col].duplicated().sum()
            ).sum()
            if duplicates > 0:
                warnings.append(f"Found {duplicates} duplicate dates within ad units")
        else:
            duplicates = dates.duplicated().sum()
            if duplicates > 0:
                warnings.append(f"Found {duplicates} duplicate dates")

        return errors, warnings, dates

    def _validate_target(self, data: pd.DataFrame) -> Tuple[List[str], List[str]]:
        """Validate the target column."""
        errors = []
        warnings = []

        target = data[self.target_col]

        # Check for numeric type
        if not pd.api.types.is_numeric_dtype(target):
            errors.append(f"Target column '{self.target_col}' is not numeric")
            return errors, warnings

        # Check for null values
        null_count = target.isna().sum()
        if null_count > 0:
            warnings.append(f"Target column contains {null_count} null values")

        # Check for negative values
        neg_count = (target < 0).sum()
        if neg_count > 0:
            errors.append(f"Target column contains {neg_count} negative values")

        # Check for zeros (warning only)
        zero_count = (target == 0).sum()
        zero_pct = zero_count / len(target) * 100
        if zero_pct > 50:
            warnings.append(
                f"Target column is {zero_pct:.1f}% zeros - consider using Croston/TSB"
            )
        elif zero_pct > 10:
            warnings.append(f"Target column contains {zero_pct:.1f}% zeros")

        return errors, warnings

    def _infer_and_validate_frequency(
        self, data: pd.DataFrame, dates: pd.Series
    ) -> Tuple[str, List[str]]:
        """Infer frequency and validate minimum history."""
        warnings = []

        # Calculate median time difference
        sorted_dates = dates.sort_values()
        diffs = sorted_dates.diff().dropna()

        if len(diffs) == 0:
            return "D", warnings

        median_diff = diffs.median()

        # Determine frequency
        if median_diff <= pd.Timedelta(days=2):
            freq = "D"
        elif median_diff <= pd.Timedelta(days=10):
            freq = "W"
        else:
            freq = "M"

        return freq, warnings

    def _validate_history(
        self, data: pd.DataFrame, require_ad_unit: bool, freq: str
    ) -> Tuple[List[str], List[str]]:
        """Validate sufficient history per series, warning if insufficient."""
        errors = []
        warnings = []

        # Determine minimum history requirement
        if self.min_history is not None:
            min_hist = self.min_history
        else:
            min_hist = self.MIN_HISTORY.get(freq, 180)

        if require_ad_unit and self.ad_unit_col in data.columns:
            # Check per ad unit
            series_counts = data.groupby(self.ad_unit_col).size()
            insufficient = series_counts[series_counts < min_hist]

            if len(insufficient) > 0:
                for ad_unit, count in insufficient.items():
                    # Changed from error to warning - model will use fallback
                    warnings.append(
                        f"Ad unit '{ad_unit}' has {count} observations, "
                        f"less than recommended {min_hist}. "
                        f"Model will use fallback configuration."
                    )
        else:
            # Check overall
            if len(data) < min_hist:
                # Changed from error to warning - model will use fallback
                warnings.append(
                    f"Insufficient history: {len(data)} < {min_hist} observations. "
                    f"Model will use fallback configuration."
                )

        return errors, warnings

    def _validate_frequency_consistency(
        self,
        data: pd.DataFrame,
        dates: pd.Series,
        require_ad_unit: bool,
    ) -> Tuple[List[str], List[str]]:
        """Validate that frequency is consistent within series."""
        errors = []
        warnings = []

        def check_consistency(series_dates: pd.Series, series_name: str = "") -> None:
            """Check frequency consistency for a series."""
            sorted_dates = series_dates.sort_values()
            diffs = sorted_dates.diff().dropna()

            if len(diffs) < 2:
                return

            mean_diff = diffs.mean()
            std_diff = diffs.std()

            if mean_diff > pd.Timedelta(0) and std_diff / mean_diff > self.frequency_tolerance:
                prefix = f"Series '{series_name}': " if series_name else ""
                warnings.append(
                    f"{prefix}Inconsistent frequency detected "
                    f"(std/mean = {std_diff / mean_diff:.2f})"
                )

        if require_ad_unit and self.ad_unit_col in data.columns:
            # Check per ad unit
            for ad_unit, group in data.groupby(self.ad_unit_col):
                series_dates = pd.to_datetime(group[self.date_col])
                check_consistency(series_dates, str(ad_unit))
        else:
            check_consistency(dates)

        return errors, warnings


def validate_series(
    y: pd.Series,
    min_history: int = 180,
    allow_zeros: bool = True,
) -> ValidationResult:
    """
    Validate a single time series.

    This is a convenience function for validating pd.Series input.

    Parameters
    ----------
    y : pd.Series
        Time series to validate. Should have DatetimeIndex.
    min_history : int, default 180
        Minimum number of observations required.
    allow_zeros : bool, default True
        Whether to allow zero values.

    Returns
    -------
    result : ValidationResult
        Validation result.
    """
    errors = []
    warnings = []
    metadata = {}

    # Check input type
    if not isinstance(y, pd.Series):
        errors.append(f"Input must be pd.Series, got {type(y).__name__}")
        return ValidationResult(is_valid=False, errors=errors)

    # Check for DatetimeIndex
    if not isinstance(y.index, pd.DatetimeIndex):
        errors.append("Series must have DatetimeIndex")
        return ValidationResult(is_valid=False, errors=errors)

    # Check minimum length
    if len(y) < min_history:
        errors.append(f"Insufficient history: {len(y)} < {min_history}")

    # Check for numeric type
    if not pd.api.types.is_numeric_dtype(y):
        errors.append("Series is not numeric")
        return ValidationResult(is_valid=False, errors=errors)

    # Check for nulls
    null_count = y.isna().sum()
    if null_count > 0:
        warnings.append(f"Series contains {null_count} null values")

    # Check for negatives
    neg_count = (y < 0).sum()
    if neg_count > 0:
        errors.append(f"Series contains {neg_count} negative values")

    # Check for zeros
    zero_count = (y == 0).sum()
    zero_pct = zero_count / len(y) * 100

    if not allow_zeros and zero_count > 0:
        errors.append(f"Series contains {zero_count} zeros")
    elif zero_pct > 50:
        warnings.append(f"Series is {zero_pct:.1f}% zeros")

    # Collect metadata
    metadata["n_obs"] = len(y)
    metadata["date_range"] = (y.index.min(), y.index.max())
    metadata["zero_pct"] = zero_pct

    is_valid = len(errors) == 0
    return ValidationResult(
        is_valid=is_valid,
        errors=errors,
        warnings=warnings,
        metadata=metadata,
    )
