"""
Multi-Config Validation Framework

Provides comprehensive validation for multiple pipeline configuration files
with detailed error attribution and fail-fast behavior.
"""

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

import yaml

from .config_discovery import ConfigFile


@dataclass
class ValidationError(Exception):
    """Represents a validation error for a specific config file."""

    config_file: Path
    error_type: str
    message: str
    line_number: Optional[int] = None
    context: Optional[str] = None

    def __str__(self) -> str:
        location = f":{self.line_number}" if self.line_number else ""
        context_info = f" ({self.context})" if self.context else ""
        return f"{self.config_file}{location}: {self.error_type} - {self.message}{context_info}"


@dataclass
class ValidationResult:
    """Results of multi-config validation."""

    success: bool
    configs: List[ConfigFile]
    errors: List[ValidationError] = None
    warnings: List[str] = None

    @property
    def total_configs(self) -> int:
        return len(self.configs)

    @property
    def error_count(self) -> int:
        return len(self.errors) if self.errors else 0


class ConfigValidationError(Exception):
    """Raised when pipeline config validation fails."""

    def __init__(self, message: str, errors: List[ValidationError]):
        super().__init__(message)
        self.errors = errors
        self.error_count = len(errors)

    def get_error_summary(self) -> str:
        """Get formatted summary of all validation errors."""
        if not self.errors:
            return "No validation errors"

        error_lines = [f"❌ {self.error_count} validation error(s) found:"]

        for error in self.errors:
            error_lines.append(f"   • {error}")

        return "\n".join(error_lines)


def validate_yaml_syntax(config_file: ConfigFile) -> None:
    """
    Validate YAML syntax for a config file.

    Args:
        config_file: Config file to validate

    Raises:
        ValidationError: If YAML syntax is invalid
    """
    try:
        content = config_file.load_content()
        if content is None:
            raise ValidationError(
                config_file=config_file.relative_path,
                error_type="EmptyConfigError",
                message="Config file is empty or contains only whitespace",
            )
    except yaml.YAMLError as e:
        line_number = getattr(e, "problem_mark", None)
        line_num = line_number.line + 1 if line_number else None

        raise ValidationError(
            config_file=config_file.relative_path,
            error_type="YAMLSyntaxError",
            message=f"Invalid YAML syntax: {e}",
            line_number=line_num,
        )
    except Exception as e:
        raise ValidationError(
            config_file=config_file.relative_path,
            error_type="ConfigLoadError",
            message=f"Failed to load config file: {e}",
        )


def validate_config_structure(config_file: ConfigFile) -> None:
    """
    Validate basic structure of a pipeline config file.

    Args:
        config_file: Config file to validate

    Raises:
        ValidationError: If config structure is invalid
    """
    content = config_file.content or config_file.load_content()

    # Must have 'stages' key
    if "stages" not in content:
        raise ValidationError(
            config_file=config_file.relative_path,
            error_type="MissingStagesError",
            message="Config must contain a 'stages' section",
        )

    stages = content["stages"]
    if not isinstance(stages, dict):
        raise ValidationError(
            config_file=config_file.relative_path,
            error_type="InvalidStagesError",
            message="'stages' must be a dictionary/object",
        )

    # At least one stage must be defined
    if not stages:
        raise ValidationError(
            config_file=config_file.relative_path,
            error_type="EmptyStagesError",
            message="'stages' section is empty - at least one stage must be defined",
        )


def validate_stage_config(config_file: ConfigFile, stage_name: str) -> None:
    """
    Validate configuration for a specific stage.

    Args:
        config_file: Config file to validate
        stage_name: Name of the stage to validate

    Raises:
        ValidationError: If stage config is invalid
    """
    content = config_file.content or config_file.load_content()
    stages = content.get("stages", {})

    # Stage doesn't need to exist in every config file
    if stage_name not in stages:
        return

    stage_config = stages[stage_name]

    # Stage config can be a list (jobs) or dict (with jobs key)
    if isinstance(stage_config, list):
        jobs = stage_config
    elif isinstance(stage_config, dict):
        jobs = stage_config.get("jobs", [])
        if not isinstance(jobs, list):
            raise ValidationError(
                config_file=config_file.relative_path,
                error_type="InvalidJobsError",
                message=f"Stage '{stage_name}' jobs must be a list",
                context=f"stages.{stage_name}.jobs",
            )
    else:
        raise ValidationError(
            config_file=config_file.relative_path,
            error_type="InvalidStageConfigError",
            message=f"Stage '{stage_name}' must be a list or object with 'jobs' key",
            context=f"stages.{stage_name}",
        )

    # Validate individual jobs
    for job_index, job in enumerate(jobs):
        validate_job_config(config_file, stage_name, job_index, job)


def validate_job_config(
    config_file: ConfigFile, stage_name: str, job_index: int, job: Dict[str, Any]
) -> None:
    """
    Validate configuration for a specific job within a stage.

    Args:
        config_file: Config file containing the job
        stage_name: Name of the stage
        job_index: Index of the job within the stage
        job: Job configuration to validate

    Raises:
        ValidationError: If job config is invalid
    """
    if not isinstance(job, dict):
        raise ValidationError(
            config_file=config_file.relative_path,
            error_type="InvalidJobError",
            message=f"Job {job_index} must be an object/dictionary",
            context=f"stages.{stage_name}.jobs[{job_index}]",
        )

    # Stage-specific validation
    if stage_name == "generate":
        validate_generate_job(config_file, job_index, job)
    elif stage_name in ["k8s", "metadata", "usage", "discount", "tax"]:
        # These stages typically don't need jobs or have simpler configs
        pass


def validate_generate_job(
    config_file: ConfigFile, job_index: int, job: Dict[str, Any]
) -> None:
    """
    Validate a generate stage job configuration.

    Args:
        config_file: Config file containing the job
        job_index: Index of the job
        job: Job configuration to validate

    Raises:
        ValidationError: If generate job config is invalid
    """
    context = f"stages.generate.jobs[{job_index}]"

    # Required fields for generate jobs
    required_fields = ["start_date", "end_date", "total_cost", "resource_count"]
    for field in required_fields:
        if field not in job:
            raise ValidationError(
                config_file=config_file.relative_path,
                error_type="MissingRequiredFieldError",
                message=f"Generate job is missing required field '{field}'",
                context=context,
            )

    # Validate field types and values
    if not isinstance(job.get("total_cost"), (int, float)) or job["total_cost"] <= 0:
        raise ValidationError(
            config_file=config_file.relative_path,
            error_type="InvalidFieldValueError",
            message="'total_cost' must be a positive number",
            context=f"{context}.total_cost",
        )

    if not isinstance(job.get("resource_count"), int) or job["resource_count"] <= 0:
        raise ValidationError(
            config_file=config_file.relative_path,
            error_type="InvalidFieldValueError",
            message="'resource_count' must be a positive integer",
            context=f"{context}.resource_count",
        )

    # Validate filters if present
    filters = job.get("filters", [])
    if not isinstance(filters, list):
        raise ValidationError(
            config_file=config_file.relative_path,
            error_type="InvalidFieldValueError",
            message="'filters' must be a list",
            context=f"{context}.filters",
        )

    # Validate individual filters
    for filter_index, filter_config in enumerate(filters):
        if not isinstance(filter_config, dict):
            raise ValidationError(
                config_file=config_file.relative_path,
                error_type="InvalidFilterError",
                message=f"Filter {filter_index} must be an object",
                context=f"{context}.filters[{filter_index}]",
            )

        # Validate profile references
        validate_profile_references(
            config_file, f"{context}.filters[{filter_index}]", filter_config
        )


def validate_profile_references(
    config_file: ConfigFile, context: str, filter_config: Dict[str, Any]
) -> None:
    """
    Validate profile references in filter configuration.

    Args:
        config_file: Config file containing the filter
        context: Context path for error reporting
        filter_config: Filter configuration to validate

    Raises:
        ValidationError: If profile references are invalid
    """
    # Import here to avoid circular dependencies
    try:
        from ..profiles import validate_profile_exists
    except ImportError:
        # Profile validation will happen later in the pipeline
        return

    variance_profile = filter_config.get("variance_profile")
    anomaly_profile = filter_config.get("anomaly_profile")

    if variance_profile:
        try:
            validate_profile_exists(variance_profile, "variance")
        except ValueError as e:
            raise ValidationError(
                config_file=config_file.relative_path,
                error_type="InvalidVarianceProfileError",
                message=str(e),
                context=f"{context}.variance_profile",
            )

    if anomaly_profile:
        try:
            validate_profile_exists(anomaly_profile, "anomaly")
        except ValueError as e:
            raise ValidationError(
                config_file=config_file.relative_path,
                error_type="InvalidAnomalyProfileError",
                message=str(e),
                context=f"{context}.anomaly_profile",
            )


def validate_all_pipeline_configs(
    config_files: List[ConfigFile], stage_name: str
) -> ValidationResult:
    """
    Validate all pipeline configs before any processing starts.

    Args:
        config_files: List of discovered config files
        stage_name: Target stage name for validation

    Returns:
        ValidationResult with success status and any errors

    Raises:
        ConfigValidationError: If any config is invalid
    """
    validation_errors = []
    warnings = []

    for config_file in config_files:
        try:
            # Phase 1: YAML syntax validation
            validate_yaml_syntax(config_file)

            # Phase 2: Basic structure validation
            validate_config_structure(config_file)

            # Phase 3: Stage-specific validation
            validate_stage_config(config_file, stage_name)

        except ValidationError as e:
            validation_errors.append(e)
        except Exception as e:
            # Catch any unexpected errors and convert to ValidationError
            validation_errors.append(
                ValidationError(
                    config_file=config_file.relative_path,
                    error_type="UnexpectedValidationError",
                    message=f"Unexpected error during validation: {e}",
                )
            )

    # Check if any configs have jobs for the target stage
    configs_with_stage_jobs = []
    for config_file in config_files:
        if not config_file.content:
            continue

        stages = config_file.content.get("stages", {})
        if stage_name in stages:
            stage_config = stages[stage_name]

            # Extract jobs list
            if isinstance(stage_config, list):
                jobs = stage_config
            elif isinstance(stage_config, dict):
                jobs = stage_config.get("jobs", [])
            else:
                jobs = []

            if jobs:
                configs_with_stage_jobs.append(config_file.relative_path)

    # Warning if no configs have jobs for the target stage
    if not configs_with_stage_jobs:
        warnings.append(
            f"No config files contain jobs for stage '{stage_name}'. "
            f"Pipeline execution may not have any work to do."
        )

    # Fail if any validation errors occurred
    if validation_errors:
        raise ConfigValidationError(
            f"Validation failed for {len(validation_errors)} error(s) across {len(config_files)} config file(s)",
            errors=validation_errors,
        )

    return ValidationResult(
        success=True, configs=config_files, errors=[], warnings=warnings
    )


def validate_multi_config_compatibility(
    config_files: List[ConfigFile], stage_name: str
) -> List[str]:
    """
    Check for potential conflicts or issues when merging multiple configs.

    Args:
        config_files: List of validated config files
        stage_name: Target stage name

    Returns:
        List of warning messages about potential conflicts
    """
    warnings = []

    # Check for duplicate reference data files
    reference_files = []
    for config_file in config_files:
        if not config_file.content:
            continue

        stages = config_file.content.get("stages", {})
        stage_config = stages.get(stage_name, {})

        # Extract jobs
        if isinstance(stage_config, list):
            jobs = stage_config
        elif isinstance(stage_config, dict):
            jobs = stage_config.get("jobs", [])
        else:
            continue

        for job in jobs:
            ref_data = job.get("reference_data")
            if ref_data:
                reference_files.append((ref_data, config_file.relative_path))

    # Check for duplicate reference files
    ref_file_counts = {}
    for ref_file, config_file in reference_files:
        if ref_file not in ref_file_counts:
            ref_file_counts[ref_file] = []
        ref_file_counts[ref_file].append(config_file)

    for ref_file, config_files_using in ref_file_counts.items():
        if len(config_files_using) > 1:
            warnings.append(
                f"Reference data file '{ref_file}' is used by multiple configs: {', '.join(config_files_using)}. "
                f"This may result in duplicate data unless intended."
            )

    return warnings
