"""
ProcessingStrategyService - Optimal processing strategy selection for data operations.

Primary Responsibility: Select optimal processing strategies based on data characteristics
and system resources without performing actual data processing.

Focus Areas:
- Memory-based strategy selection: Determines whether to use in-memory, chunked, or streaming processing
- Compression strategy selection: Chooses optimal compression algorithm based on performance preferences
- Partitioning strategy selection: Decides between single-file and partitioned storage approaches
- File size and memory threshold evaluation: Analyzes data characteristics to guide strategy decisions

Does NOT handle: Actual data processing, file I/O operations, configuration loading, or business logic

This service preserves all existing strategy selection behavior including:
- Memory threshold-based processing decisions (60% and 100% thresholds)
- Compression algorithm selection (snappy vs lz4 based on speed vs space preferences)
- Partitioning decisions based on dataset size and row count thresholds
- Auto-detection of optimal strategies based on system resources
"""

from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from typing import (
    Any,
    Dict,
    Optional,
    Tuple,
)

import polars as pl
import psutil


class ProcessingStrategy(Enum):
    """Processing strategy options for data operations."""

    IN_MEMORY = "in_memory"
    CHUNKED = "chunked"
    STREAMING = "streaming"


class CompressionStrategy(Enum):
    """Compression strategy options."""

    SNAPPY = "snappy"  # Fast compression, larger files
    LZ4 = "lz4"  # Better compression, slower
    NONE = None  # No compression


class PartitioningStrategy(Enum):
    """Partitioning strategy options."""

    SINGLE_FILE = "single_file"  # Store as single data.parquet
    BY_PROVIDER = (
        "by_provider"  # Partition by cloud_provider (AWS.parquet, GCP.parquet, etc.)
    )
    BY_MONTH = "by_month"  # Partition by month (provider/YYYY-MM structure)


class ProcessingPreference(Enum):
    """Processing preference options."""

    SPEED = "speed"  # Optimize for fastest processing
    MEMORY = "memory"  # Optimize for lowest memory usage
    BALANCED = "balanced"  # Balance between speed and memory


class StoragePolicy(Enum):
    """Storage policy options based on available disk space."""

    CONSERVATIVE = "conservative"  # Keep more temporary files, more compression
    BALANCED = "balanced"  # Standard approach
    AGGRESSIVE = "aggressive"  # Minimize disk usage, less compression


@dataclass
class SystemResources:
    """Container for system resource information."""

    total_memory_gb: float
    available_memory_gb: float
    cpu_count: int
    disk_space_gb: float

    @classmethod
    def detect_current(cls) -> "SystemResources":
        """Detect current system resources automatically."""
        memory = psutil.virtual_memory()
        total_memory_gb = memory.total / (1024**3)
        available_memory_gb = memory.available / (1024**3)
        cpu_count = psutil.cpu_count(logical=True)
        disk_space_gb = psutil.disk_usage("/").free / (1024**3)

        return cls(
            total_memory_gb=total_memory_gb,
            available_memory_gb=available_memory_gb,
            cpu_count=cpu_count,
            disk_space_gb=disk_space_gb,
        )


@dataclass
class DataCharacteristics:
    """Container for data characteristics that influence strategy selection."""

    estimated_size_gb: float
    row_count: int
    column_count: int
    has_provider_column: bool = False
    provider_distribution: Optional[Dict[str, int]] = None

    @classmethod
    def from_dataframe(
        cls, df: pl.DataFrame, bytes_per_row: int = 100
    ) -> "DataCharacteristics":
        """Create DataCharacteristics from a polars DataFrame."""
        estimated_size_gb = (df.height * bytes_per_row) / (1024**3)
        has_provider_column = "cloud_provider" in df.columns

        provider_distribution = None
        if has_provider_column:
            provider_counts = df["cloud_provider"].value_counts()
            provider_distribution = {row[0]: row[1] for row in provider_counts.rows()}

        return cls(
            estimated_size_gb=estimated_size_gb,
            row_count=df.height,
            column_count=df.width,
            has_provider_column=has_provider_column,
            provider_distribution=provider_distribution,
        )

    @classmethod
    def estimate_from_file(
        cls, file_path: Path, avg_row_size_bytes: int = 100
    ) -> "DataCharacteristics":
        """Estimate data characteristics from file without loading it."""
        file_size_gb = file_path.stat().st_size / (1024**3)
        estimated_rows = int(file_path.stat().st_size / avg_row_size_bytes)

        # Conservative estimates for file-based analysis
        return cls(
            estimated_size_gb=file_size_gb,
            row_count=estimated_rows,
            column_count=10,  # Conservative estimate
            has_provider_column=True,  # Assume billing data has provider column
        )


@dataclass
class StrategyConfiguration:
    """Configuration container for strategy selection parameters."""

    memory_limit_gb: float
    chunk_size_mb: float = 256
    processing_preference: ProcessingPreference = ProcessingPreference.BALANCED
    storage_policy: StoragePolicy = StoragePolicy.BALANCED
    compression_enabled: bool = True
    large_dataset_threshold_rows: int = 1_000_000

    # Memory thresholds for strategy selection
    in_memory_threshold_factor: float = (
        0.6  # Use 60% of memory limit for in-memory threshold
    )
    streaming_threshold_factor: float = (
        1.0  # Use 100% of memory limit for streaming threshold
    )

    @classmethod
    def auto_detect(cls, system_resources: SystemResources) -> "StrategyConfiguration":
        """Auto-detect optimal configuration based on system resources."""
        # Memory limit: 80% of available memory, minimum 2GB
        memory_limit_gb = max(2.0, system_resources.available_memory_gb * 0.8)

        # Storage policy based on available disk space
        if system_resources.disk_space_gb > 50:
            storage_policy = StoragePolicy.CONSERVATIVE
        elif system_resources.disk_space_gb > 20:
            storage_policy = StoragePolicy.BALANCED
        else:
            storage_policy = StoragePolicy.AGGRESSIVE

        # Processing preference based on system capabilities
        if memory_limit_gb > 8 and system_resources.cpu_count > 4:
            processing_preference = ProcessingPreference.SPEED
        elif memory_limit_gb < 4:
            processing_preference = ProcessingPreference.MEMORY
        else:
            processing_preference = ProcessingPreference.BALANCED

        # Chunk size based on memory
        chunk_size_mb = min(256, max(64, memory_limit_gb * 32))  # 32MB per GB of memory

        return cls(
            memory_limit_gb=memory_limit_gb,
            chunk_size_mb=chunk_size_mb,
            processing_preference=processing_preference,
            storage_policy=storage_policy,
        )


@dataclass
class StrategyRecommendation:
    """Container for complete strategy recommendation."""

    processing_strategy: ProcessingStrategy
    compression_strategy: CompressionStrategy
    partitioning_strategy: PartitioningStrategy

    # Additional metadata about the decision
    rationale: str
    estimated_memory_usage_gb: float
    estimated_processing_time_factor: float  # Relative to baseline

    def to_dict(self) -> Dict[str, Any]:
        """Convert recommendation to dictionary for logging/serialization."""
        return {
            "processing_strategy": self.processing_strategy.value,
            "compression_strategy": self.compression_strategy.value,
            "partitioning_strategy": self.partitioning_strategy.value,
            "rationale": self.rationale,
            "estimated_memory_usage_gb": self.estimated_memory_usage_gb,
            "estimated_processing_time_factor": self.estimated_processing_time_factor,
        }


class ProcessingStrategyService:
    """
    Service for selecting optimal processing strategies based on data characteristics and system resources.

    Provides focused strategy selection algorithms while preserving all existing
    decision logic and threshold-based strategy selection patterns.

    Key Features:
    - Memory-based processing strategy selection (in-memory, chunked, streaming)
    - Compression algorithm selection based on performance preferences
    - Partitioning strategy selection based on data characteristics
    - Auto-detection of optimal settings based on system resources
    - Comprehensive strategy recommendation with rationale
    """

    def __init__(self, config: Optional[StrategyConfiguration] = None):
        """Initialize ProcessingStrategyService with configuration."""
        if config is None:
            # Auto-detect optimal configuration based on current system
            system_resources = SystemResources.detect_current()
            config = StrategyConfiguration.auto_detect(system_resources)

        self.config = config
        self._system_resources = SystemResources.detect_current()

    # === Public Interface Methods ===

    def recommend_strategy(
        self, data_characteristics: DataCharacteristics
    ) -> StrategyRecommendation:
        """
        Recommend complete processing strategy based on data characteristics.

        Args:
            data_characteristics: Information about the dataset to process

        Returns:
            StrategyRecommendation with processing, compression, and partitioning strategies
        """
        # Select each strategy component
        processing_strategy = self.select_processing_strategy(data_characteristics)
        compression_strategy = self.select_compression_strategy()
        partitioning_strategy = self.select_partitioning_strategy(data_characteristics)

        # Calculate estimated resource usage
        estimated_memory_gb = self._estimate_memory_usage(
            data_characteristics, processing_strategy
        )
        processing_time_factor = self._estimate_processing_time_factor(
            data_characteristics, processing_strategy, compression_strategy
        )

        # Generate rationale for the strategy selection
        rationale = self._generate_rationale(
            data_characteristics,
            processing_strategy,
            compression_strategy,
            partitioning_strategy,
        )

        return StrategyRecommendation(
            processing_strategy=processing_strategy,
            compression_strategy=compression_strategy,
            partitioning_strategy=partitioning_strategy,
            rationale=rationale,
            estimated_memory_usage_gb=estimated_memory_gb,
            estimated_processing_time_factor=processing_time_factor,
        )

    def select_processing_strategy(
        self, data_characteristics: DataCharacteristics
    ) -> ProcessingStrategy:
        """
        Select optimal processing strategy based on data size and memory limits.

        Uses the established threshold-based approach:
        - In-memory: data size < 60% of memory limit
        - Chunked: data size between 60% and 100% of memory limit
        - Streaming: data size > 100% of memory limit

        Args:
            data_characteristics: Information about the dataset

        Returns:
            ProcessingStrategy enum value
        """
        data_size_gb = data_characteristics.estimated_size_gb
        memory_limit_gb = self.config.memory_limit_gb

        # Apply the established threshold logic
        in_memory_threshold = memory_limit_gb * self.config.in_memory_threshold_factor
        streaming_threshold = memory_limit_gb * self.config.streaming_threshold_factor

        if data_size_gb < in_memory_threshold:
            return ProcessingStrategy.IN_MEMORY
        elif data_size_gb <= streaming_threshold:
            return ProcessingStrategy.CHUNKED
        else:
            return ProcessingStrategy.STREAMING

    def select_compression_strategy(self) -> CompressionStrategy:
        """
        Select optimal compression strategy based on processing preferences.

        Follows existing logic:
        - Speed preference: Use snappy (fast compression)
        - Memory/Balanced preference: Use lz4 (better compression ratio)
        - Disabled compression: No compression

        Returns:
            CompressionStrategy enum value
        """
        if not self.config.compression_enabled:
            return CompressionStrategy.NONE

        if self.config.processing_preference == ProcessingPreference.SPEED:
            return CompressionStrategy.SNAPPY
        else:
            return CompressionStrategy.LZ4

    def select_partitioning_strategy(
        self, data_characteristics: DataCharacteristics
    ) -> PartitioningStrategy:
        """
        Select optimal partitioning strategy based on data characteristics.

        Decision logic:
        - Large datasets (>1M rows or >chunk_size): Partition by provider if available
        - Small datasets: Single file storage
        - Provider distribution analysis for partitioning effectiveness

        Args:
            data_characteristics: Information about the dataset

        Returns:
            PartitioningStrategy enum value
        """
        # Check if dataset qualifies as "large"
        is_large_dataset = self._is_large_dataset(data_characteristics)

        if not is_large_dataset:
            return PartitioningStrategy.SINGLE_FILE

        # For large datasets, check if partitioning by provider makes sense
        if (
            data_characteristics.has_provider_column
            and data_characteristics.provider_distribution
        ):
            # Analyze provider distribution to see if partitioning is beneficial
            provider_counts = data_characteristics.provider_distribution

            # If we have multiple providers with reasonable distribution, partition by provider
            if len(provider_counts) > 1 and min(provider_counts.values()) > 1000:
                return PartitioningStrategy.BY_PROVIDER

        # Default for large datasets without good partitioning opportunities
        return PartitioningStrategy.SINGLE_FILE

    def estimate_processing_requirements(
        self, data_characteristics: DataCharacteristics
    ) -> Tuple[float, float, str]:
        """
        Estimate processing requirements for given data characteristics.

        Args:
            data_characteristics: Information about the dataset

        Returns:
            Tuple of (estimated_memory_gb, estimated_time_factor, strategy_description)
        """
        recommendation = self.recommend_strategy(data_characteristics)

        return (
            recommendation.estimated_memory_usage_gb,
            recommendation.estimated_processing_time_factor,
            recommendation.rationale,
        )

    def validate_strategy_feasibility(
        self,
        data_characteristics: DataCharacteristics,
        requested_strategy: Optional[ProcessingStrategy] = None,
    ) -> Tuple[bool, str]:
        """
        Validate whether a strategy is feasible given current system resources.

        Args:
            data_characteristics: Information about the dataset
            requested_strategy: Optional specific strategy to validate

        Returns:
            Tuple of (is_feasible, explanation)
        """
        if requested_strategy is None:
            requested_strategy = self.select_processing_strategy(data_characteristics)

        estimated_memory = self._estimate_memory_usage(
            data_characteristics, requested_strategy
        )
        available_memory = self._system_resources.available_memory_gb

        if estimated_memory > available_memory:
            return False, (
                f"Strategy '{requested_strategy.value}' requires {estimated_memory:.1f}GB "
                f"but only {available_memory:.1f}GB available"
            )

        # Additional feasibility checks
        if requested_strategy == ProcessingStrategy.IN_MEMORY:
            # Check if data size is reasonable for in-memory processing
            if (
                data_characteristics.estimated_size_gb
                > self.config.memory_limit_gb * 0.8
            ):
                return False, "Dataset too large for reliable in-memory processing"

        return True, f"Strategy '{requested_strategy.value}' is feasible"

    # === Private Helper Methods ===

    def _is_large_dataset(self, data_characteristics: DataCharacteristics) -> bool:
        """Check if dataset qualifies as large based on established thresholds."""
        # Size-based threshold
        estimated_size_mb = data_characteristics.estimated_size_gb * 1024
        size_threshold_exceeded = estimated_size_mb > self.config.chunk_size_mb

        # Row count threshold
        row_threshold_exceeded = (
            data_characteristics.row_count > self.config.large_dataset_threshold_rows
        )

        return size_threshold_exceeded or row_threshold_exceeded

    def _estimate_memory_usage(
        self, data_characteristics: DataCharacteristics, strategy: ProcessingStrategy
    ) -> float:
        """Estimate memory usage for given strategy."""
        base_size_gb = data_characteristics.estimated_size_gb

        if strategy == ProcessingStrategy.IN_MEMORY:
            # In-memory processing needs full dataset plus overhead
            return base_size_gb * 1.5
        elif strategy == ProcessingStrategy.CHUNKED:
            # Chunked processing uses chunk size plus overhead
            chunk_size_gb = self.config.chunk_size_mb / 1024
            return min(chunk_size_gb * 2, base_size_gb * 0.3)
        else:  # STREAMING
            # Streaming uses minimal memory
            return min(0.5, base_size_gb * 0.1)

    def _estimate_processing_time_factor(
        self,
        data_characteristics: DataCharacteristics,
        processing_strategy: ProcessingStrategy,
        compression_strategy: CompressionStrategy,
    ) -> float:
        """Estimate relative processing time factor (1.0 = baseline)."""
        base_factor = 1.0

        # Processing strategy impact
        if processing_strategy == ProcessingStrategy.IN_MEMORY:
            base_factor *= 0.8  # Fastest
        elif processing_strategy == ProcessingStrategy.CHUNKED:
            base_factor *= 1.2  # Moderate overhead
        else:  # STREAMING
            base_factor *= 1.5  # Slower but memory-efficient

        # Compression impact
        if compression_strategy == CompressionStrategy.SNAPPY:
            base_factor *= 1.1  # Small compression overhead
        elif compression_strategy == CompressionStrategy.LZ4:
            base_factor *= 1.3  # Better compression, more time

        return base_factor

    def _generate_rationale(
        self,
        data_characteristics: DataCharacteristics,
        processing_strategy: ProcessingStrategy,
        compression_strategy: CompressionStrategy,
        partitioning_strategy: PartitioningStrategy,
    ) -> str:
        """Generate human-readable rationale for strategy selection."""
        size_gb = data_characteristics.estimated_size_gb
        memory_limit = self.config.memory_limit_gb

        rationale_parts = []

        # Processing strategy rationale
        if processing_strategy == ProcessingStrategy.IN_MEMORY:
            rationale_parts.append(
                f"In-memory processing selected: dataset ({size_gb:.1f}GB) fits comfortably "
                f"within {self.config.in_memory_threshold_factor * 100:.0f}% memory threshold "
                f"({memory_limit * self.config.in_memory_threshold_factor:.1f}GB)"
            )
        elif processing_strategy == ProcessingStrategy.CHUNKED:
            rationale_parts.append(
                f"Chunked processing selected: dataset ({size_gb:.1f}GB) exceeds in-memory "
                f"threshold but fits within memory limit ({memory_limit:.1f}GB)"
            )
        else:
            rationale_parts.append(
                f"Streaming processing selected: dataset ({size_gb:.1f}GB) exceeds "
                f"memory limit ({memory_limit:.1f}GB), requires streaming approach"
            )

        # Compression rationale
        if compression_strategy == CompressionStrategy.SNAPPY:
            rationale_parts.append("Snappy compression for speed optimization")
        elif compression_strategy == CompressionStrategy.LZ4:
            rationale_parts.append("LZ4 compression for balanced speed/size")
        else:
            rationale_parts.append("No compression to minimize processing overhead")

        # Partitioning rationale
        if partitioning_strategy == PartitioningStrategy.BY_PROVIDER:
            provider_count = len(data_characteristics.provider_distribution or {})
            rationale_parts.append(
                f"Provider partitioning selected: large dataset with {provider_count} providers"
            )
        else:
            rationale_parts.append(
                "Single file storage: dataset size or structure optimal for unified storage"
            )

        return "; ".join(rationale_parts)
