from typing import List
"""Unit metric data generator."""

from datetime import datetime, timedelta
from pathlib import Path

import numpy as np
import polars as pl

from .config import DataConfig


class UnitMetricGenerator:
    """Generator for synthetic unit metric data."""

    def __init__(self, config: DataConfig):
        self.config = config
        self._validate_config()
        if self.config.anomaly:
            np.random.seed(self.config.anomaly.seed)

    def _validate_config(self) -> None:
        """Validate the configuration."""
        # Validate dates by attempting to parse them
        try:
            datetime.fromisoformat(self.config.start_date)
            datetime.fromisoformat(self.config.end_date)
        except ValueError as e:
            raise ValueError(f"Invalid date format: {e}")

        if self.config.variance_factor <= 0:
            raise ValueError("Variance factor must be positive")
        if self.config.frequency not in ["D", "M"]:
            raise ValueError("Frequency must be 'D' or 'M'")

    def _generate_base_metric(self) -> float:
        """Generate a base metric value within the configured scale."""
        min_val, max_val = self.config.scale.value
        return np.random.uniform(min_val, max_val)

    def _apply_variance(self, base_value: float) -> float:
        """Apply random variance to a base value."""
        variance = np.random.uniform(
            -self.config.variance_factor, self.config.variance_factor
        )
        return base_value * (1 + variance)

    def _apply_seasonality(self, dates: List[datetime]) -> np.ndarray:
        """Apply seasonal patterns to the data."""
        if not self.config.seasonality:
            return np.ones(len(dates))

        pattern = self.config.seasonality.pattern
        amplitude = self.config.seasonality.amplitude
        phase = self.config.seasonality.phase

        if pattern == "weekly":
            period = 7
            x = np.array([d.weekday() for d in dates])
        elif pattern == "monthly":
            period = 30
            x = np.array([d.day for d in dates])
        else:  # yearly
            period = 365
            x = np.array([d.timetuple().tm_yday for d in dates])

        return 1 + amplitude * np.sin(2 * np.pi * (x + phase) / period)

    def _apply_trend(self, length: int) -> np.ndarray:
        """Apply trend patterns to the data."""
        if not self.config.trend:
            return np.ones(length)

        x = np.arange(length)
        growth = self.config.trend.growth_rate

        if self.config.trend.type == "linear":
            return 1 + growth * x
        elif self.config.trend.type == "exponential":
            return np.exp(growth * x)
        else:  # logistic
            cap = self.config.trend.cap or 2.0
            return cap / (1 + np.exp(-growth * x))

    def _generate_anomaly_mask(self, length: int) -> np.ndarray:
        """Generate a mask for anomaly injection."""
        if not self.config.anomaly:
            return np.zeros(length)

        total_months = length / 30
        num_anomalies = int(total_months * self.config.anomaly.frequency)
        positions = np.linspace(0, length - 1, num_anomalies, dtype=int)

        mask = np.zeros(length)
        for pos in positions:
            end_pos = min(pos + self.config.anomaly.duration, length)
            mask[pos:end_pos] = 1

        return mask

    def _generate_date_range(self) -> List[datetime]:
        """Generate date range using datetime objects."""
        start_date = datetime.fromisoformat(self.config.start_date)
        end_date = datetime.fromisoformat(self.config.end_date)

        dates = []
        current = start_date

        if self.config.frequency == "D":
            # Daily frequency
            while current <= end_date:
                dates.append(current)
                current = current + timedelta(days=1)
        else:
            # Monthly frequency
            while current <= end_date:
                dates.append(current)
                # Move to next month
                if current.month == 12:
                    current = datetime(current.year + 1, 1, current.day)
                else:
                    try:
                        current = datetime(current.year, current.month + 1, current.day)
                    except ValueError:
                        # Handle month-end edge cases (e.g., Jan 31 -> Feb 28)
                        current = datetime(current.year, current.month + 1, 1)

        return dates

    def generate_data(self) -> pl.DataFrame:
        """Generate unit metric data based on configuration."""
        # Generate date range
        date_range = self._generate_date_range()

        # Generate base values
        base_values = np.array(
            [self._generate_base_metric() for _ in range(len(date_range))]
        )

        # Apply patterns
        seasonality = self._apply_seasonality(date_range)
        trend = self._apply_trend(len(date_range))
        base_values = base_values * seasonality * trend

        # Apply variance
        values = np.array([self._apply_variance(v) for v in base_values])

        # Apply anomalies
        if self.config.anomaly:
            anomaly_mask = self._generate_anomaly_mask(len(values))
            values = values * (1 + (anomaly_mask * (self.config.anomaly.magnitude - 1)))

        # Create base dataframe with Polars
        data = {
            "timestamp": date_range,
            "metric_name": [self.config.name] * len(date_range),
            "unit_value": values.astype(int),  # Convert to integers for cleaner output
        }

        df = pl.DataFrame(data)

        # Add group filters as additional columns
        if self.config.group_filters:
            for group, elements in self.config.group_filters.items():
                group_values = np.random.choice(elements, size=len(df))
                df = df.with_columns(pl.Series(name=group.lower(), values=group_values))

        return df

    def generate_monthly_files(self, output_dir: Path) -> List[Path]:
        """Generate monthly CSV files and return list of created file paths."""
        df = self.generate_data()

        # Ensure output directory exists
        output_dir.mkdir(parents=True, exist_ok=True)

        # Add year_month column for grouping
        df = df.with_columns(
            pl.col("timestamp").dt.strftime("%Y-%m").alias("year_month")
        )

        created_files = []

        # Group by year_month and save separate files
        for group_data in df.group_by(["year_month"]):
            year_month = group_data[0][0]  # The grouping key (first element of tuple)
            group_df = group_data[1]  # The grouped DataFrame

            # Remove the temporary year_month column
            output_df = group_df.drop("year_month")

            # Create filename
            filename = f"{self.config.name}_{year_month}.csv"
            file_path = output_dir / filename

            # Save to CSV
            output_df.write_csv(file_path)
            created_files.append(file_path)

        return created_files
