"""
Processing strategy system for automatic data optimization.

Selects optimal processing strategy based on data characteristics and system resources.
"""

import math
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Callable

import polars as pl

from .config import DataManagerConfig
from .models import DataStream


class ProcessingStrategy(ABC):
    """Base class for data processing strategies"""

    def __init__(self, config: DataManagerConfig):
        self.config = config

    @property
    @abstractmethod
    def name(self) -> str:
        """Strategy name for reporting"""
        pass

    @abstractmethod
    def should_apply(self, data_source: Path, operation: str) -> bool:
        """Determine if this strategy is optimal"""
        pass

    @abstractmethod
    def load_data(self, source: Path) -> pl.DataFrame:
        """Load data using this strategy (service called BY pipeline)"""
        pass

    def save_data(self, data: pl.DataFrame, destination: Path) -> Path:
        """Save data using this strategy (service called BY pipeline)"""
        # Default implementation - can be overridden by strategies
        data.write_parquet(destination / "data.parquet")
        return destination

    def _estimate_size_gb(self, data_source: Path) -> float:
        """Estimate data size in GB"""
        if data_source.is_file():
            return data_source.stat().st_size / (1024**3)
        elif data_source.is_dir():
            total_size = 0
            for file_path in data_source.rglob("*"):
                if file_path.is_file():
                    total_size += file_path.stat().st_size
            return total_size / (1024**3)
        return 0.0


class InMemoryStrategy(ProcessingStrategy):
    """Process entire dataset in memory - fastest for small datasets"""

    @property
    def name(self) -> str:
        return "in_memory"

    def should_apply(self, data_source: Path, operation: str) -> bool:
        estimated_size = self._estimate_size_gb(data_source)
        memory_threshold = (
            self.config.memory_limit_gb * 0.6
        )  # Use 60% of available memory
        return estimated_size < memory_threshold

    def load_data(self, source: Path) -> pl.DataFrame:
        """Load entire dataset into memory for processing"""
        if source.is_file():
            if source.suffix == ".parquet":
                return pl.read_parquet(source)
            elif source.suffix == ".csv":
                return pl.read_csv(source)
            else:
                # Try to infer format
                try:
                    return pl.read_csv(source)
                except:
                    pass
        elif source.is_dir():
            parquet_files = list(source.glob("**/*.parquet"))
            if parquet_files:
                return pl.read_parquet(parquet_files)

        # Enhanced error message with context
        from pathlib import Path as PathLib

        current_dir = PathLib.cwd()
        absolute_path = source.resolve()

        error_msg = f"Cannot load data from {source}\n"
        error_msg += f"Tried absolute path: {absolute_path}\n"
        error_msg += f"Current working directory: {current_dir}\n"
        error_msg += f"Supported formats: .csv, .parquet\n"

        # Check if user might need to be in cloudzero-demo-billing directory
        expected_dir = current_dir / "cloudzero-demo-billing"
        if expected_dir.exists() and current_dir.name != "cloudzero-demo-billing":
            error_msg += f"💡 Tip: Run 'cd cloudzero-demo-billing' first, or provide full path to your reference data file"
        elif not (current_dir / "orgs").exists():
            error_msg += f"💡 Tip: Make sure you're running from the cloudzero-demo-billing directory"

        raise ValueError(error_msg)


class ChunkedStrategy(ProcessingStrategy):
    """Process dataset in chunks - balanced approach"""

    @property
    def name(self) -> str:
        return "chunked"

    def should_apply(self, data_source: Path, operation: str) -> bool:
        estimated_size = self._estimate_size_gb(data_source)
        memory_threshold = self.config.memory_limit_gb * 0.6
        return memory_threshold <= estimated_size <= self.config.memory_limit_gb

    def load_data(self, source: Path) -> pl.DataFrame:
        """Load dataset using chunked approach for memory efficiency"""
        # For now, same as InMemory - chunking will be handled at pipeline level
        # In full implementation, this would use lazy loading
        if source.is_file():
            if source.suffix == ".parquet":
                return pl.read_parquet(source)
            elif source.suffix == ".csv":
                return pl.read_csv(source)
            else:
                # Try to infer format
                try:
                    return pl.read_csv(source)
                except:
                    pass
        elif source.is_dir():
            parquet_files = list(source.glob("**/*.parquet"))
            if parquet_files:
                return pl.read_parquet(parquet_files)

        raise ValueError(
            f"Cannot load data from {source} (ChunkedStrategy supports .csv, .parquet)"
        )


class StreamingStrategy(ProcessingStrategy):
    """Stream processing for very large datasets"""

    @property
    def name(self) -> str:
        return "streaming"

    def should_apply(self, data_source: Path, operation: str) -> bool:
        estimated_size = self._estimate_size_gb(data_source)
        return estimated_size > self.config.memory_limit_gb

    def load_data(self, source: Path) -> pl.DataFrame:
        """Load dataset using streaming/lazy approach for large datasets"""
        # Use lazy loading for memory efficiency
        if source.is_file():
            if source.suffix == ".parquet":
                # For very large files, we'd want to use scan_parquet and lazy operations
                # For now, fall back to regular loading but could be enhanced
                return pl.read_parquet(source)
            elif source.suffix == ".csv":
                return pl.read_csv(source)
            else:
                # Try to infer format
                try:
                    return pl.read_csv(source)
                except:
                    pass
        elif source.is_dir():
            parquet_files = list(source.glob("**/*.parquet"))
            if parquet_files:
                # Could use scan_parquet for truly lazy loading in future
                return pl.read_parquet(parquet_files)

        raise ValueError(
            f"Cannot load data from {source} (StreamingStrategy supports .csv, .parquet)"
        )


class ProcessingStrategySelector:
    """Selects optimal processing strategy based on data characteristics"""

    def __init__(self, config: DataManagerConfig):
        self.strategies = [
            InMemoryStrategy(config),
            ChunkedStrategy(config),
            StreamingStrategy(config),
        ]

    def select_strategy(self, data_source: Path, operation: str) -> ProcessingStrategy:
        """Select best strategy for given data source and operation"""
        for strategy in self.strategies:
            if strategy.should_apply(data_source, operation):
                return strategy

        # Fallback to chunked strategy
        return self.strategies[1]
