"""
Deployment schema loader and validator.

This module provides functionality to:
1. Load deployment schema from YAML definitions
2. Validate actual deployment state against schema
3. Find matching failure modes and suggest fixes
4. Track deployment runs and update schema based on learnings

Usage:
    from lightwave.schema.loaders.deployment import DeploymentSchemaLoader

    # Load schema
    loader = DeploymentSchemaLoader()
    schema = loader.load()

    # Validate a deployment config
    config = DeploymentConfig(
        cluster="platform-prod",
        service="platform-prod",
        image_uri="123456789.dkr.ecr.us-east-1.amazonaws.com/platform:abc1234",
    )
    errors = loader.validate_config(config)

    # Run preflight checks
    preflight_result = loader.run_phase("preflight", config)
    if not preflight_result.passed:
        # Find matching failure mode
        for step_result in preflight_result.failed_steps:
            failure_mode = loader.find_failure_mode(step_result.stderr)
            if failure_mode:
                print(f"Known issue: {failure_mode.cause}")
                print(f"Solution: {failure_mode.solution}")

Example with CLI:
    # Validate deployment readiness
    lw deploy validate --environment production

    # Run specific phase
    lw deploy run preflight --cluster platform-prod --service platform-prod

    # Check for known failure modes
    lw deploy diagnose "exec format error"
"""

from __future__ import annotations

import os
import subprocess
from datetime import datetime
from pathlib import Path
from typing import Any

import yaml

from lightwave.schema.pydantic.models.deployment import (
    DeploymentConfig,
    DeploymentRun,
    DeploymentValidator,
    EnvironmentDefinition,
    EnvironmentType,
    FailureMode,
    PhaseDefinition,
    PhaseResult,
    PhaseType,
    StepDefinition,
    StepResult,
    StepStatus,
    ValidationError,
    ValidationResult,
)


class DeploymentSchemaLoader:
    """Load and manage deployment schema from YAML definitions."""

    @property
    def default_schema_path(self) -> Path:
        """Get default schema path from centralized resolver."""
        from lightwave.schema.core.paths import get_sst_path

        return get_sst_path("deployment")

    def __init__(self, schema_path: Path | None = None) -> None:
        """Initialize the loader.

        Args:
            schema_path: Optional custom path to deployment.yaml
        """
        self.schema_path = schema_path or self.default_schema_path
        self._schema: dict[str, Any] | None = None
        self._environments: dict[str, EnvironmentDefinition] = {}
        self._phases: dict[PhaseType, PhaseDefinition] = {}
        self._failure_modes: list[FailureMode] = []

    def load(self) -> dict[str, Any]:
        """Load the deployment schema from YAML.

        Returns:
            The raw schema dictionary

        Raises:
            FileNotFoundError: If schema file doesn't exist
            yaml.YAMLError: If schema is invalid YAML
        """
        if self._schema is None:
            if not self.schema_path.exists():
                raise FileNotFoundError(f"Deployment schema not found at {self.schema_path}")

            with open(self.schema_path) as f:
                self._schema = yaml.safe_load(f)

            self._parse_environments()
            self._parse_phases()
            self._parse_failure_modes()

        return self._schema

    def _parse_environments(self) -> None:
        """Parse environment definitions from schema."""
        env_data = self._schema.get("environments", {}).get("definitions", {})
        for name, config in env_data.items():
            try:
                env_type = EnvironmentType(config.get("type", name))
                self._environments[name] = EnvironmentDefinition(
                    name=config.get("name", name),
                    type=env_type,
                    requires_approval=config.get("requires_approval", False),
                    auto_deploy=config.get("auto_deploy", False),
                    rollback_enabled=config.get("rollback_enabled", True),
                    domain_pattern=config.get("domain_pattern"),
                )
            except ValueError:
                # Skip invalid environment types
                pass

    def _parse_phases(self) -> None:
        """Parse phase definitions from schema."""
        phases_data = self._schema.get("phases", {})
        for phase_name, phase_config in phases_data.items():
            if phase_name == "description":
                continue

            try:
                phase_type = PhaseType(phase_name)
            except ValueError:
                continue

            steps = {}
            for step_name, step_config in phase_config.get("steps", {}).items():
                steps[step_name] = StepDefinition(
                    order=step_config.get("order", 1),
                    name=step_config.get("name", step_name),
                    description=step_config.get("description", ""),
                    commands=step_config.get("commands", []),
                    timeout_seconds=step_config.get("timeout_seconds", 60),
                    retryable=step_config.get("retryable", False),
                    max_retries=step_config.get("max_retries", 0),
                    retry_delay_seconds=step_config.get("retry_delay_seconds", 5),
                    depends_on=step_config.get("depends_on", []),
                    outputs=step_config.get("outputs", {}),
                    warning_only=step_config.get("warning_only", False),
                    services=step_config.get("services", []),
                    inputs=step_config.get("inputs", {}),
                    required_endpoints=step_config.get("required_endpoints", []),
                )

            self._phases[phase_type] = PhaseDefinition(
                order=phase_config.get("order", 1),
                name=phase_config.get("name", phase_name),
                description=phase_config.get("description", ""),
                trigger=phase_config.get("trigger", "manual"),
                blocking=phase_config.get("blocking", True),
                steps=steps,
            )

    def _parse_failure_modes(self) -> None:
        """Parse failure modes from schema."""
        failure_data = self._schema.get("failure_modes", {})
        for mode_name, mode_config in failure_data.items():
            if mode_name == "description":
                continue

            self._failure_modes.append(
                FailureMode(
                    pattern=mode_config.get("pattern", ""),
                    cause=mode_config.get("cause", ""),
                    solution=mode_config.get("solution", []),
                    prevention=mode_config.get("prevention", []),
                    diagnosis=mode_config.get("diagnosis", []),
                )
            )

    @property
    def environments(self) -> dict[str, EnvironmentDefinition]:
        """Get environment definitions."""
        if not self._environments:
            self.load()
        return self._environments

    @property
    def phases(self) -> dict[PhaseType, PhaseDefinition]:
        """Get phase definitions."""
        if not self._phases:
            self.load()
        return self._phases

    @property
    def failure_modes(self) -> list[FailureMode]:
        """Get failure mode definitions."""
        if not self._failure_modes:
            self.load()
        return self._failure_modes

    def get_environment(self, name: str) -> EnvironmentDefinition | None:
        """Get an environment definition by name."""
        return self.environments.get(name)

    def get_phase(self, phase_type: PhaseType | str) -> PhaseDefinition | None:
        """Get a phase definition by type."""
        if isinstance(phase_type, str):
            try:
                phase_type = PhaseType(phase_type)
            except ValueError:
                return None
        return self.phases.get(phase_type)

    def find_failure_mode(self, error_message: str) -> FailureMode | None:
        """Find a matching failure mode for an error.

        Args:
            error_message: The error message to match against

        Returns:
            Matching FailureMode or None
        """
        return DeploymentValidator.find_failure_mode(error_message, self.failure_modes)

    def validate_config(self, config: DeploymentConfig) -> list[ValidationError]:
        """Validate a deployment configuration.

        Args:
            config: The deployment configuration to validate

        Returns:
            List of validation errors (empty if valid)
        """
        errors = []

        # Check environment exists
        env_value = config.environment.value if hasattr(config.environment, "value") else config.environment
        env = self.get_environment(env_value)
        if env is None:
            errors.append(
                ValidationError(
                    step="config",
                    check_type="environment",
                    expected=list(self.environments.keys()),
                    actual=env_value,
                    message=f"Unknown environment: {env_value}",
                )
            )

        # Check required inputs
        required = self._schema.get("required_inputs", {})
        for _category, inputs in required.items():
            for input_def in inputs:
                input_name = input_def.get("name", "")
                # Convert to lowercase for config attribute lookup
                attr_name = input_name.lower()
                if hasattr(config, attr_name):
                    value = getattr(config, attr_name)
                    if value is None and "default" not in input_def:
                        errors.append(
                            ValidationError(
                                step="config",
                                check_type="required_input",
                                expected=input_name,
                                actual=None,
                                message=f"Missing required input: {input_name}",
                            )
                        )

        return errors

    def run_step(
        self,
        step: StepDefinition,
        config: DeploymentConfig,
        env_vars: dict[str, str] | None = None,
    ) -> StepResult:
        """Execute a deployment step.

        Args:
            step: The step definition to execute
            config: Deployment configuration
            env_vars: Additional environment variables

        Returns:
            StepResult with execution outcome
        """
        result = StepResult(
            step_name=step.name,
            status=StepStatus.RUNNING,
            started_at=datetime.now(),
        )

        # Build environment variables
        execution_env = os.environ.copy()
        execution_env.update(
            {
                "CLUSTER": config.cluster,
                "SERVICE": config.service,
                "REGION": config.region,
                "IMAGE_URI": config.image_uri,
                "CONTAINER_NAME": config.container_name,
                "SHORT_SHA": config.short_sha,
            }
        )
        if config.previous_task_def_arn:
            execution_env["PREVIOUS_TASK_DEF_ARN"] = config.previous_task_def_arn
        if config.health_check_url:
            execution_env["HEALTH_CHECK_URL"] = config.health_check_url
        if config.base_url:
            execution_env["BASE_URL"] = config.base_url
        if env_vars:
            execution_env.update(env_vars)

        # Execute each command
        all_stdout = []
        all_stderr = []

        for command in step.commands:
            # Substitute environment variables in command
            for key, value in execution_env.items():
                command = command.replace(f"${{{key}}}", value)
                command = command.replace(f"${key}", value)

            try:
                proc_result = subprocess.run(
                    command,
                    shell=True,
                    capture_output=True,
                    text=True,
                    timeout=step.timeout_seconds,
                    env=execution_env,
                )
                all_stdout.append(proc_result.stdout)
                all_stderr.append(proc_result.stderr)
                result.exit_code = proc_result.returncode

                if proc_result.returncode != 0:
                    break

            except subprocess.TimeoutExpired:
                result.exit_code = -1
                all_stderr.append(f"Command timed out after {step.timeout_seconds}s")
                break
            except Exception as e:
                result.exit_code = -1
                all_stderr.append(str(e))
                break

        result.stdout = "\n".join(all_stdout)
        result.stderr = "\n".join(all_stderr)
        result.completed_at = datetime.now()

        # Validate result
        validation = DeploymentValidator.validate_step(step, result)
        if validation.passed:
            result.status = StepStatus.PASSED
            result.outputs = validation.outputs
        elif step.warning_only:
            result.status = StepStatus.PASSED  # Warnings don't fail
            result.error_message = "Warning: " + (validation.errors[0].message if validation.errors else "Unknown")
        else:
            result.status = StepStatus.FAILED
            result.error_message = validation.errors[0].message if validation.errors else "Validation failed"

        return result

    def run_phase(
        self,
        phase_type: PhaseType | str,
        config: DeploymentConfig,
        env_vars: dict[str, str] | None = None,
    ) -> PhaseResult:
        """Execute all steps in a deployment phase.

        Args:
            phase_type: The phase to execute
            config: Deployment configuration
            env_vars: Additional environment variables

        Returns:
            PhaseResult with all step outcomes
        """
        if isinstance(phase_type, str):
            phase_type = PhaseType(phase_type)

        phase = self.get_phase(phase_type)
        if phase is None:
            raise ValueError(f"Unknown phase: {phase_type}")

        result = PhaseResult(
            phase_name=phase.name,
            phase_type=phase_type,
            status=StepStatus.RUNNING,
            started_at=datetime.now(),
        )

        # Collect outputs from completed steps
        step_outputs: dict[str, Any] = {}
        if env_vars:
            step_outputs.update(env_vars)

        # Execute steps in order
        for step_name, step_def in phase.ordered_steps:
            # Check dependencies
            deps_met = all(
                result.step_results.get(dep, StepResult(step_name=dep)).passed for dep in step_def.depends_on
            )

            if not deps_met:
                step_result = StepResult(
                    step_name=step_name,
                    status=StepStatus.SKIPPED,
                    error_message="Dependencies not met",
                )
            else:
                # Run the step
                step_result = self.run_step(step_def, config, step_outputs)

                # Collect outputs for subsequent steps
                step_outputs.update(step_result.outputs)

            result.step_results[step_name] = step_result

            # Stop on blocking failure
            if step_result.failed and phase.blocking:
                break

        result.completed_at = datetime.now()
        result.status = StepStatus.PASSED if result.passed else StepStatus.FAILED

        return result

    def create_deployment_run(
        self,
        config: DeploymentConfig,
        run_id: str | None = None,
    ) -> DeploymentRun:
        """Create a new deployment run.

        Args:
            config: Deployment configuration
            run_id: Optional custom run ID

        Returns:
            DeploymentRun instance
        """
        import uuid

        return DeploymentRun(
            id=run_id or str(uuid.uuid4())[:8],
            config=config,
            started_at=datetime.now(),
        )

    def diagnose_error(self, error_message: str) -> dict[str, Any]:
        """Diagnose an error and return suggested fixes.

        Args:
            error_message: The error message to diagnose

        Returns:
            Dictionary with diagnosis information
        """
        failure_mode = self.find_failure_mode(error_message)

        if failure_mode:
            return {
                "matched": True,
                "pattern": failure_mode.pattern,
                "cause": failure_mode.cause,
                "solution": failure_mode.solution,
                "prevention": failure_mode.prevention,
                "diagnosis_commands": failure_mode.diagnosis,
                "schema_update": (failure_mode.schema_update.model_dump() if failure_mode.schema_update else None),
            }
        else:
            return {
                "matched": False,
                "message": "No matching failure mode found",
                "suggestion": "Add this failure pattern to deployment.yaml",
            }

    def get_required_inputs(self, phase: PhaseType | str) -> list[dict[str, Any]]:
        """Get required inputs for a phase.

        Args:
            phase: The phase type

        Returns:
            List of required input definitions
        """
        required = self._schema.get("required_inputs", {})

        if isinstance(phase, str):
            phase = PhaseType(phase)

        # Common inputs are always required
        inputs = list(required.get("common", []))

        # Add phase-specific inputs
        phase_inputs = required.get(phase.value, [])
        inputs.extend(phase_inputs)

        return inputs


# =============================================================================
# SINGLETON INSTANCE
# =============================================================================

_loader: DeploymentSchemaLoader | None = None


def get_deployment_schema() -> DeploymentSchemaLoader:
    """Get the global deployment schema loader instance."""
    global _loader
    if _loader is None:
        _loader = DeploymentSchemaLoader()
        _loader.load()
    return _loader


def validate_deployment_config(config: DeploymentConfig) -> ValidationResult:
    """Validate a deployment configuration against the schema.

    Args:
        config: The deployment configuration to validate

    Returns:
        ValidationResult with any errors
    """
    loader = get_deployment_schema()
    errors = loader.validate_config(config)
    return ValidationResult(passed=len(errors) == 0, errors=errors)


def diagnose_deployment_error(error_message: str) -> dict[str, Any]:
    """Diagnose a deployment error and get suggestions.

    Args:
        error_message: The error message to diagnose

    Returns:
        Dictionary with diagnosis information
    """
    loader = get_deployment_schema()
    return loader.diagnose_error(error_message)
