"""
Configuration Service - Handles all configuration loading and parsing.

Primary Responsibility: Load and parse configuration files (YAML/JSON)

Focus Areas:
- Pipeline configs: pipeline.jobs.yaml files with stage configurations
- Upload configs: Connection settings, API tokens, workspace configs
- Org configs: Organization-specific settings and billing configurations
- CLI configs: Command-line argument processing and defaults

Does NOT handle: Business logic decisions, directory management, logging setup

This service preserves all existing configuration loading behavior including:
- Multi-config discovery and merging
- Environment variable resolution (${VARIABLE_NAME} pattern)
- Comprehensive validation with detailed error attribution
- Default value application and fallback mechanisms
- Backward compatibility with legacy config formats
"""

import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import (
    Any,
    Dict,
    List,
    Optional,
    Union,
)

import yaml

# Note: Upload configuration now uses S3-based approach with YAML configs
from src.utils.config_discovery import ConfigDiscoveryError, discover_pipeline_configs
from src.utils.config_validation import ValidationError

# Import existing validation and error handling components
from src.utils.multi_config_loader import MultiConfigError, load_multi_pipeline_configs


@dataclass
class ConfigurationPaths:
    """
    Container for configuration file paths.

    Centralizes path construction logic and provides clear documentation
    of where different configuration files are expected to be found.
    """

    org_dir: Path
    org_config_path: Path = field(init=False)
    upload_config_path: Path = field(init=False)
    pipeline_configs_dir: Path = field(init=False)
    legacy_main_config_path: Path = field(init=False)
    telemetry_configs_dir: Path = field(init=False)

    def __post_init__(self):
        """Initialize derived paths based on org_dir."""
        self.org_config_path = self.org_dir / "config.yaml"
        self.upload_config_path = self.org_dir / "config.upload.yaml"
        self.pipeline_configs_dir = self.org_dir / "billing" / "pipeline-configs"
        self.legacy_main_config_path = self.org_dir / "billing" / "main.yaml"
        self.telemetry_configs_dir = self.org_dir / "telemetry" / "configs"


@dataclass
class ConfigurationDefaults:
    """
    Container for all configuration default values.

    Centralizes default value definitions to ensure consistency across
    the application and make defaults easy to modify when needed.
    """

    # Organization config defaults
    tenant_factor: int = 1
    memory_limit_gb: float = 4.0

    # Upload config defaults - generated uploads
    generated_concurrency: int = 6
    generated_max_retries: int = 5
    generated_timeout: float = 60.0
    generated_dry_run: bool = False
    generated_resume: bool = False

    # Pipeline config defaults
    element_distribution_strategy: str = "random"
    cluster_factor: float = 0.3
    pareto_factor: float = 0.8
    seed: int = 42

    # Stage config defaults
    k8s_probability: float = 0.3
    frequency: str = "H"  # hourly
    variance_factor: float = 0.2


class ConfigurationService:
    """
    Service for all configuration loading and parsing operations.

    Provides a single point of access for all configuration data while preserving
    all existing loading behavior, validation rules, and error handling patterns.

    Key Features:
    - Multi-config discovery and merging for pipeline configurations
    - Environment variable resolution with ${VARIABLE_NAME} syntax
    - Comprehensive validation with detailed error attribution
    - Default value application with clear precedence rules
    - Backward compatibility with legacy configuration formats
    - Secure handling of API tokens and sensitive configuration data
    """

    def __init__(self, org_dir: Union[str, Path]):
        """
        Initialize ConfigurationService for a specific organization.

        Args:
            org_dir: Path to organization directory containing configuration files
        """
        self.org_dir = Path(org_dir)
        self.paths = ConfigurationPaths(self.org_dir)
        self.defaults = ConfigurationDefaults()

        # Cache for loaded configurations to avoid repeated file I/O
        self._config_cache: Dict[str, Any] = {}

    def load_org_config(self) -> Dict[str, Any]:
        """
        Load main organization configuration.

        Loads config.yaml with default value application and environment
        variable resolution. Preserves all existing behavior including
        tenant_factor and memory_limit_gb defaults.

        Returns:
            Organization configuration dictionary with defaults applied

        Raises:
            FileNotFoundError: If config.yaml doesn't exist
            yaml.YAMLError: If YAML syntax is invalid
            EnvironmentError: If required environment variables are missing
        """
        cache_key = "org_config"
        if cache_key in self._config_cache:
            return self._config_cache[cache_key]

        try:
            config = self._load_yaml_file(self.paths.org_config_path)

            # Apply organization config defaults (preserve existing behavior)
            config.setdefault("tenant_factor", self.defaults.tenant_factor)
            config.setdefault("memory_limit_gb", self.defaults.memory_limit_gb)

            # Resolve environment variables in API token and other fields
            config = self._resolve_environment_variables(config)

            self._config_cache[cache_key] = config
            return config

        except FileNotFoundError:
            raise FileNotFoundError(
                f"Organization config not found: {self.paths.org_config_path}"
            )
        except yaml.YAMLError as e:
            raise yaml.YAMLError(
                f"Invalid YAML in organization config {self.paths.org_config_path}: {e}"
            )

    def load_upload_config(self) -> Dict[str, Any]:
        """
        DEPRECATED: Upload configuration now uses S3-based approach.
        
        Use upload.s3.config.UploadConfig.from_yaml() instead.
        """
        raise NotImplementedError(
            "Upload configuration now uses S3-based approach. "
            "Use upload.s3.config.UploadConfig.from_yaml() instead."
        )

    def load_pipeline_configs(
        self,
        stage_name: str = "generate",
        verbose: bool = False,
        run_dir: Optional[Path] = None,
    ) -> Dict[str, Any]:
        """
        Load and merge all pipeline configurations.

        Performs multi-config discovery to find all *.pipeline.yaml files,
        validates each one, and merges them into a unified configuration.
        Preserves all existing behavior including job metadata mapping.

        Args:
            stage_name: Target stage name for config loading
            verbose: Whether to print detailed progress
            run_dir: Optional run directory for metadata mapping

        Returns:
            Merged pipeline configuration with all jobs and metadata

        Raises:
            MultiConfigError: If multi-config loading fails
            ConfigValidationError: If any config fails validation
            FileNotFoundError: If no pipeline configs are found
        """
        cache_key = f"pipeline_configs_{stage_name}"
        if cache_key in self._config_cache:
            return self._config_cache[cache_key]

        try:
            # Use existing multi-config loader to preserve all behavior
            config = load_multi_pipeline_configs(
                org_dir=self.org_dir,
                stage_name=stage_name,
                verbose=verbose,
                run_dir=run_dir,
            )

            # Apply pipeline config defaults
            config = self._apply_pipeline_defaults(config)

            self._config_cache[cache_key] = config
            return config

        except MultiConfigError:
            # Fallback to legacy main.yaml for backward compatibility
            if self.paths.legacy_main_config_path.exists():
                legacy_config = self._load_yaml_file(self.paths.legacy_main_config_path)
                legacy_config = self._apply_pipeline_defaults(legacy_config)
                self._config_cache[cache_key] = legacy_config
                return legacy_config
            else:
                raise FileNotFoundError(
                    f"No pipeline configurations found in {self.paths.pipeline_configs_dir} "
                    f"and legacy config {self.paths.legacy_main_config_path} does not exist"
                )

    def load_telemetry_config(self, config_type: str) -> Dict[str, Any]:
        """
        Load telemetry configuration for a specific type.

        Loads configuration from telemetry/configs/{type}.yaml for
        unit metrics and allocation telemetry generation.

        Args:
            config_type: Type of telemetry config (e.g., "unit_metrics", "allocations")

        Returns:
            Telemetry configuration dictionary

        Raises:
            FileNotFoundError: If telemetry config doesn't exist
            yaml.YAMLError: If YAML syntax is invalid
        """
        cache_key = f"telemetry_{config_type}"
        if cache_key in self._config_cache:
            return self._config_cache[cache_key]

        telemetry_config_path = self.paths.telemetry_configs_dir / f"{config_type}.yaml"

        try:
            config = self._load_yaml_file(telemetry_config_path)
            self._config_cache[cache_key] = config
            return config

        except FileNotFoundError:
            raise FileNotFoundError(
                f"Telemetry config not found: {telemetry_config_path}"
            )

    def discover_pipeline_config_files(self) -> List[Path]:
        """
        Discover all pipeline configuration files.

        Returns list of all *.pipeline.yaml files found recursively
        in the pipeline-configs directory, sorted alphabetically.

        Returns:
            List of pipeline configuration file paths
        """
        try:
            config_files = discover_pipeline_configs(self.org_dir)
            return [config_file.path for config_file in config_files]
        except ConfigDiscoveryError:
            return []

    def validate_config_file(self, config_path: Path) -> None:
        """
        Validate a single configuration file.

        Performs basic YAML syntax validation and structure checking.

        Args:
            config_path: Path to configuration file to validate

        Raises:
            yaml.YAMLError: If YAML syntax is invalid
            ValidationError: If config structure is invalid
        """
        try:
            config = self._load_yaml_file(config_path)

            # Basic structure validation
            if not isinstance(config, dict):
                raise ValidationError(
                    config_file=config_path,
                    error_type="Structure",
                    message="Configuration must be a dictionary",
                )

            if "stages" not in config:
                raise ValidationError(
                    config_file=config_path,
                    error_type="Missing Section",
                    message="Configuration must contain a 'stages' section",
                )

        except yaml.YAMLError as e:
            raise ValidationError(
                config_file=config_path, error_type="YAML Syntax", message=str(e)
            )

    def resolve_environment_variable(self, value: str) -> str:
        """
        Resolve environment variable in configuration value.

        Supports ${VARIABLE_NAME} syntax for environment variable resolution.
        Preserves existing behavior including error handling for missing variables.

        Args:
            value: Configuration value that may contain environment variable

        Returns:
            Resolved value with environment variable substituted

        Raises:
            EnvironmentError: If environment variable is not set
        """
        return self._resolve_env_var(value)

    def clear_cache(self) -> None:
        """
        Clear configuration cache.

        Forces reload of all configurations on next access.
        Useful for testing or when configuration files have changed.
        """
        self._config_cache.clear()

    def get_configuration_status(self) -> Dict[str, bool]:
        """
        Get status of all configuration files.

        Returns dictionary indicating which configuration files exist
        and are accessible. Useful for diagnostics and validation.

        Returns:
            Dictionary mapping config type to existence status
        """
        return {
            "org_config": self.paths.org_config_path.exists(),
            "upload_config": self.paths.upload_config_path.exists(),
            "pipeline_configs_dir": self.paths.pipeline_configs_dir.exists(),
            "legacy_main_config": self.paths.legacy_main_config_path.exists(),
            "telemetry_configs_dir": self.paths.telemetry_configs_dir.exists(),
        }

    # Private helper methods (preserve existing behavior)

    def _load_yaml_file(self, path: Path) -> Dict[str, Any]:
        """Load YAML file with error handling (preserves existing _load_yaml behavior)."""
        try:
            with open(path) as f:
                return yaml.safe_load(f)
        except FileNotFoundError:
            raise FileNotFoundError(f"Configuration file not found: {path}")
        except yaml.YAMLError as e:
            raise yaml.YAMLError(f"Invalid YAML in {path}: {e}")
        except UnicodeDecodeError as e:
            raise UnicodeDecodeError(f"Encoding error in {path}: {e}")

    def _resolve_environment_variables(self, config: Dict[str, Any]) -> Dict[str, Any]:
        """Recursively resolve environment variables in configuration."""
        if isinstance(config, dict):
            return {
                key: self._resolve_environment_variables(value)
                for key, value in config.items()
            }
        elif isinstance(config, list):
            return [self._resolve_environment_variables(item) for item in config]
        elif (
            isinstance(config, str) and config.startswith("${") and config.endswith("}")
        ):
            return self._resolve_env_var(config)
        else:
            return config

    def _resolve_env_var(self, value: str) -> str:
        """Resolve single environment variable (preserves existing behavior)."""
        if value.startswith("${") and value.endswith("}"):
            var_name = value[2:-1]
            env_value = os.getenv(var_name)
            if env_value is None:
                raise OSError(
                    f"Environment variable '{var_name}' is not set but required by configuration"
                )
            return env_value
        return value

    def _apply_pipeline_defaults(self, config: Dict[str, Any]) -> Dict[str, Any]:
        """Apply default values to pipeline configuration."""
        # Apply defaults at appropriate levels without overriding existing values
        if "stages" in config:
            for stage_name, stage_config in config["stages"].items():
                if isinstance(stage_config, list):
                    # Stage has multiple jobs
                    for job in stage_config:
                        self._apply_job_defaults(job)
                else:
                    # Stage has single job or config
                    self._apply_job_defaults(stage_config)

        return config

    def _apply_job_defaults(self, job: Dict[str, Any]) -> None:
        """Apply default values to individual job configuration."""
        # Apply job-level defaults
        job.setdefault("seed", self.defaults.seed)

        # Apply stage-specific defaults
        if "k8s_probability" in job:
            job.setdefault("k8s_probability", self.defaults.k8s_probability)
        if "frequency" in job:
            job.setdefault("frequency", self.defaults.frequency)
        if "variance_factor" in job:
            job.setdefault("variance_factor", self.defaults.variance_factor)
