"""
Data models for upload operations.

Models used by upload services to represent upload configurations,
results, and workflow outcomes.
"""

from dataclasses import dataclass, field
from typing import Dict, List, Optional, Any
from pathlib import Path
import polars as pl
import yaml


@dataclass
class Organization:
    """Organization configuration for multi-org upload."""
    name: str
    api_token_env: str
    providers: List[str]  # List of provider names
    months: Optional[List[str]] = None  # Optional month override
    stage_overrides: Optional[Dict[str, str]] = None  # Optional stage overrides per provider
    connections: Optional[Dict[str, List[str]]] = None  # CloudZero connection IDs


@dataclass
class AdhocUploadConfig:
    """Configuration for ad-hoc CSV upload operations."""
    csv_path: str
    mapping_path: Optional[str] = None  # Path to custom column mapping file


@dataclass
class UploadConfig:
    """Configuration for upload operations."""
    dataset_name: Optional[str] = None  # For generated uploads
    organizations: List[Organization] = field(default_factory=list)
    months: Optional[List[str]] = None  # Global months (smart default)
    providers: Optional[List[str]] = None  # Global providers (smart default)
    adhoc: Optional[AdhocUploadConfig] = None  # For ad-hoc uploads
    is_dry_run: bool = False

    # S3 configuration (populated from environment)
    s3_bucket: str = ""
    s3_region: str = ""
    s3_root_folder: str = ""

    # CBF configuration
    cbf_mapping_path: Optional[Path] = None

    # Append mode configuration
    append_mode: bool = False  # Whether to append to existing drops
    source_drop_id: Optional[str] = None  # Specific source drop ID (if None, use manifest.json)

    # Named connections configuration
    connection_suffix: Optional[str] = None  # Optional suffix for connection variants (e.g., "Anomalies")
    
    # Compatibility properties for adhoc
    @property
    def adhoc_csv_path(self) -> Optional[Path]:
        """Get adhoc CSV path for compatibility."""
        return Path(self.adhoc.csv_path) if self.adhoc else None
        
    @property  
    def adhoc_mapping_path(self) -> Optional[Path]:
        """Get adhoc mapping path for compatibility."""
        return Path(self.adhoc.mapping_path) if self.adhoc and self.adhoc.mapping_path else None
    
    def is_generated_upload(self) -> bool:
        """Check if this is a generated dataset upload."""
        return self.adhoc_csv_path is None
    
    def is_adhoc_upload(self) -> bool:
        """Check if this is an ad-hoc CSV upload."""
        return self.adhoc_csv_path is not None
    
    def is_append_mode(self) -> bool:
        """Check if this is an append operation."""
        return self.append_mode
    
    def uses_manifest_discovery(self) -> bool:
        """Check if should use manifest.json for source drop discovery."""
        return self.append_mode and self.source_drop_id is None

    def get_s3_folder(self, provider: str) -> str:
        """
        Get S3 folder name for provider with optional suffix.

        Args:
            provider: Provider name (e.g., "AWS", "Azure", "GCP")

        Returns:
            S3 folder name:
            - "AWS" if no suffix
            - "AWS-Anomalies" if suffix = "Anomalies"

        Examples:
            >>> config.get_s3_folder("AWS")  # no suffix
            "AWS"
            >>> config.connection_suffix = "Anomalies"
            >>> config.get_s3_folder("AWS")
            "AWS-Anomalies"
        """
        if self.connection_suffix:
            return f"{provider}-{self.connection_suffix}"
        return provider

    def validate_connection_suffix(self) -> None:
        """
        Validate connection_suffix format.

        Raises:
            ValueError: If suffix format is invalid

        Rules:
            - Alphanumeric characters and hyphens only
            - No spaces or special characters
        """
        if not self.connection_suffix:
            return  # None is valid

        import re
        if not re.match(r'^[A-Za-z0-9-]+$', self.connection_suffix):
            raise ValueError(
                f"Invalid connection_suffix: '{self.connection_suffix}'. "
                f"Must contain only alphanumeric characters and hyphens. "
                f"Examples: 'Anomalies', 'Scenario-B', 'Q4-Spike'"
            )

    @classmethod
    def from_yaml(cls, config_path: Path) -> "UploadConfig":
        """
        Load upload configuration from YAML file with environment-based S3 config.
        
        Expected format (smart defaults structure):
        upload_config:
          dataset_name: "2025-01-15_14-30-22"
          months: ["2025-01", "2025-02"]  # Global default months
          providers: ["AWS", "GCP", "Azure"]  # Global default providers
          organizations:
            ORG_NAME:
              api_token_env: "CZ_ORG_API_KEY_ORGNAME"
              # months: ["2025-01"]  # Optional override
              # providers: ["AWS"]    # Optional override
              # stage_overrides:      # Optional stage overrides
              #   GCP: "usage"        # Force GCP to use usage stage
        """
        if not config_path.exists():
            raise FileNotFoundError(f"Upload config not found: {config_path}")
        
        with open(config_path, 'r') as f:
            raw_config = yaml.safe_load(f)
        
        # Extract main configuration section
        if "upload_config" not in raw_config:
            raise ValueError("Configuration must contain 'upload_config' section")
        
        config_data = raw_config["upload_config"]
        
        # Parse basic fields for generated uploads
        dataset_name = config_data.get("dataset_name")
        global_months = config_data.get("months")
        global_providers = config_data.get("providers")

        # Parse connection suffix for named connections
        connection_suffix = config_data.get("connection_suffix")

        # Parse append mode configuration
        append_mode = config_data.get("append_mode", False)
        source_drop_id = config_data.get("source_drop_id")
        
        # Parse ad-hoc configuration if present
        adhoc_config = None
        if "adhoc" in config_data:
            adhoc_data = config_data["adhoc"]
            adhoc_config = AdhocUploadConfig(
                csv_path=adhoc_data["csv_path"],
                mapping_path=adhoc_data.get("mapping_path")
            )
        
        # Parse smart defaults structure: organizations dict with inheritance
        organizations = cls._parse_smart_defaults_structure(config_data, global_months, global_providers)
        
        # Load S3 configuration from environment
        from src.services.environment_service import EnvironmentService
        env_service = EnvironmentService()
        s3_config = env_service.get_s3_config()
        
        # Validate required fields
        cls._validate_config(dataset_name, organizations, adhoc_config, append_mode)

        # Create config instance
        config = cls(
            dataset_name=dataset_name,
            months=global_months,
            providers=global_providers,
            organizations=organizations,
            adhoc=adhoc_config,
            append_mode=append_mode,
            source_drop_id=source_drop_id,
            connection_suffix=connection_suffix,
            s3_bucket=s3_config["bucket"],
            s3_region=s3_config["region"],
            s3_root_folder="billing-data",  # Default
            cbf_mapping_path=None  # CBF mapping loaded by DataProcessor
        )

        # Validate connection suffix format
        config.validate_connection_suffix()

        return config
    
    @classmethod  
    def _parse_smart_defaults_structure(cls, config_data: Dict[str, Any], global_months: Optional[List[str]], global_providers: Optional[List[str]]) -> List[Organization]:
        """
        Parse smart defaults configuration structure with global inheritance.
        
        Organizations inherit global months/providers unless overridden.
        """
        # Parse organizations definitions
        orgs_definitions = config_data.get("organizations", {})
        if not isinstance(orgs_definitions, dict):
            raise ValueError("'organizations' must be a dictionary mapping org names to definitions")
        
        if not orgs_definitions:
            raise ValueError("At least one organization must be defined")
        
        # Build organizations list with smart defaults
        organizations = []
        
        for org_name, org_def in orgs_definitions.items():
            if not isinstance(org_def, dict):
                raise ValueError(f"Organization definition for '{org_name}' must be a dictionary")
            
            # Parse API token environment variable (required)
            api_token_env = org_def.get("api_token_env")
            if not api_token_env:
                raise ValueError(f"Organization '{org_name}' missing required 'api_token_env' field")
            
            # Parse providers with inheritance from global defaults
            org_providers = org_def.get("providers")
            if org_providers is not None:
                if not isinstance(org_providers, list):
                    raise ValueError(f"Organization '{org_name}' providers must be a list of provider names")
                final_providers = org_providers
            else:
                # Inherit from global providers
                if global_providers is None:
                    raise ValueError(f"Organization '{org_name}' has no providers specified and no global providers defined")
                final_providers = global_providers.copy()
            
            # Parse months with inheritance from global defaults
            org_months = org_def.get("months")
            if org_months is not None:
                if not isinstance(org_months, list):
                    raise ValueError(f"Organization '{org_name}' months must be a list of month strings")
                final_months = org_months
            else:
                # Inherit from global months (can be None - will be handled by discovery)
                final_months = global_months.copy() if global_months else None
            
            # Parse optional stage overrides
            stage_overrides = org_def.get("stage_overrides")
            if stage_overrides is not None and not isinstance(stage_overrides, dict):
                raise ValueError(f"Organization '{org_name}' stage_overrides must be a dictionary")
            
            # Parse optional connections
            connections = org_def.get("connections")
            if connections is not None and not isinstance(connections, dict):
                raise ValueError(f"Organization '{org_name}' connections must be a dictionary")
            
            organizations.append(Organization(
                name=org_name,
                api_token_env=api_token_env,
                providers=final_providers,
                months=final_months,
                stage_overrides=stage_overrides,
                connections=connections
            ))
        
        return organizations
    
    @classmethod
    def _validate_config(cls, dataset_name: Optional[str], organizations: List[Organization], adhoc_config: Optional[AdhocUploadConfig], append_mode: bool = False):
        """Validate configuration requirements."""
        # For generated uploads, dataset_name is required
        if not adhoc_config and not dataset_name:
            raise ValueError("dataset_name is required for generated uploads")
        
        # For ad-hoc uploads, adhoc config is required
        if adhoc_config and not adhoc_config.csv_path:
            raise ValueError("csv_path is required for ad-hoc uploads")
        
        # Append mode validation
        if append_mode and adhoc_config:
            raise ValueError("append_mode is not supported with ad-hoc uploads")
        
        if append_mode and not dataset_name:
            raise ValueError("dataset_name is required for append mode operations")
        
        # Organizations are always required
        if not organizations:
            raise ValueError("At least one organization must be configured")


@dataclass
class DatasetChunk:
    """A chunk of dataset data for processing."""
    data: pl.DataFrame
    provider: str
    month: str
    stage: str
    organization: str
    
    def is_empty(self) -> bool:
        """Check if chunk contains data."""
        return self.data.is_empty()
    
    def row_count(self) -> int:
        """Get number of rows in chunk."""
        return len(self.data)


@dataclass
class UploadResult:
    """Result of uploading a dataset chunk."""
    s3_key: str
    filename: str  
    size_mb: float
    rows: int
    provider: str = ""
    month: str = ""
    billing_period: str = ""
    drop_id: str = ""
    error_message: Optional[str] = None
    
    def is_successful(self) -> bool:
        """Check if upload was successful."""
        return self.error_message is None


@dataclass
class OrganizationResult:
    """Result of processing a single organization."""
    organization: str
    status: str  # "success", "failed", "partial"
    drop_id: Optional[str] = None
    files_uploaded: int = 0
    files_failed: int = 0
    total_rows: int = 0
    providers: List[str] = field(default_factory=list)
    uploaded_files: List[UploadResult] = field(default_factory=list)
    failed_files: List[Dict[str, Any]] = field(default_factory=list)
    error_message: Optional[str] = None
    
    def is_successful(self) -> bool:
        """Check if organization processing was successful."""
        return self.status == "success"
    
    def is_failed(self) -> bool:
        """Check if organization processing failed completely."""
        return self.status == "failed"
    
    def is_partial(self) -> bool:
        """Check if organization processing was partially successful."""
        return self.status == "partial"


@dataclass 
class WorkflowResult:
    """Result of complete upload workflow."""
    status: str  # "success", "failed", "partial"
    dataset: str
    organizations_processed: int
    organizations_successful: int
    results: Dict[str, OrganizationResult]
    error_message: Optional[str] = None
    mode: str = "upload"  # "upload", "dry_run", "recreate_and_upload"
    
    # Additional metadata
    total_files: int = 0
    total_rows: int = 0
    processing_duration_seconds: Optional[float] = None
    
    def is_successful(self) -> bool:
        """Check if entire workflow was successful."""
        return self.status == "success"
    
    def is_failed(self) -> bool:
        """Check if entire workflow failed."""
        return self.status == "failed"
    
    def is_partial(self) -> bool:
        """Check if workflow was partially successful."""
        return self.status == "partial"
    
    def get_successful_organizations(self) -> List[str]:
        """Get list of organizations that succeeded."""
        return [
            org_name for org_name, result in self.results.items() 
            if result.is_successful()
        ]
    
    def get_failed_organizations(self) -> List[str]:
        """Get list of organizations that failed."""
        return [
            org_name for org_name, result in self.results.items()
            if result.is_failed()
        ]