"""
Deployment workflow Pydantic models.

This module provides typed models for validating deployment workflows against
the deployment.yaml schema definition. These models enable:

- Type-safe deployment configuration
- Runtime validation of deployment state
- Clear error messages when deployment fails
- Automatic schema iteration when failures occur

Usage:
    from lightwave.schema.pydantic.models.deployment import (
        DeploymentConfig,
        DeploymentPhase,
        DeploymentStep,
        ValidationResult,
    )

    # Validate a deployment phase
    result = DeploymentPhase.validate_phase("ci", actual_state)
    if not result.passed:
        for error in result.errors:
            print(f"Failed: {error.step} - {error.message}")

Example:
    # Define expected deployment configuration
    config = DeploymentConfig(
        environment="production",
        cluster="platform-prod",
        service="platform-prod",
        image_uri="123456789.dkr.ecr.us-east-1.amazonaws.com/platform:abc1234",
    )

    # Run preflight checks
    preflight = PreflightResult.run(config)
    if preflight.all_passed:
        deploy_result = DeployResult.run(config, preflight.outputs)
"""

from __future__ import annotations

import re
import subprocess
from datetime import datetime
from enum import Enum
from pathlib import Path
from typing import Any, Self

from pydantic import Field, field_validator

from lightwave.schema.pydantic.core.base import LightwaveBaseSchema, SSTSchema

# =============================================================================
# ENUMS
# =============================================================================


class EnvironmentType(str, Enum):
    """Deployment environment types."""

    LOCAL = "local"
    STAGING = "staging"
    PRODUCTION = "production"


class PhaseType(str, Enum):
    """Deployment phase types."""

    CI = "ci"
    BUILD = "build"
    PREFLIGHT = "preflight"
    DEPLOY = "deploy"
    VERIFY = "verify"


class StepStatus(str, Enum):
    """Status of a deployment step."""

    PENDING = "pending"
    RUNNING = "running"
    PASSED = "passed"
    FAILED = "failed"
    SKIPPED = "skipped"
    RETRYING = "retrying"


class ValidationType(str, Enum):
    """Types of validation checks."""

    EXIT_CODE = "exit_code"
    OUTPUT_EQUALS = "output_equals"
    OUTPUT_MATCHES = "output_matches"
    OUTPUT_GREATER_THAN = "output_greater_than"
    OUTPUT_EMPTY = "output_empty"
    HTTP_STATUS = "http_status"
    FILE_EXISTS = "file_exists"
    FILE_CONTAINS = "file_contains"
    IMAGE_EXISTS = "image_exists"
    COVERAGE_MINIMUM = "coverage_minimum"


# =============================================================================
# VALIDATION SCHEMAS
# =============================================================================


class ValidationCriteria(LightwaveBaseSchema):
    """Criteria for validating a step's output."""

    exit_code: int | None = Field(None, description="Expected exit code")
    output_equals: str | None = Field(None, description="Exact output match")
    output_matches: str | None = Field(None, description="Regex pattern to match")
    output_greater_than: float | None = Field(None, description="Numeric comparison")
    output_empty: bool | None = Field(None, description="Output should be empty")
    http_status: int | None = Field(None, description="Expected HTTP status")
    file_exists: str | None = Field(None, description="File path that must exist")
    file_contains: FileContains | None = Field(None, description="File content check")
    image_exists: bool | None = Field(None, description="Docker image exists")
    coverage_minimum: float | None = Field(None, ge=0, le=100)
    all_outputs_equal: str | None = Field(None, description="All outputs must equal")
    all_http_status: int | None = Field(None, description="All HTTP responses status")
    image_pushed_within_minutes: int | None = Field(None, ge=1)


class FileContains(LightwaveBaseSchema):
    """File content validation."""

    file: str
    substring: str


# =============================================================================
# STEP SCHEMAS
# =============================================================================


class StepDefinition(SSTSchema):
    """Definition of a deployment step from YAML."""

    order: int = Field(..., ge=1)
    name: str = Field(..., min_length=1)
    description: str = Field(...)
    commands: list[str] = Field(default_factory=list)
    validation: ValidationCriteria = Field(default_factory=ValidationCriteria)
    timeout_seconds: int = Field(default=60, ge=1)
    retryable: bool = Field(default=False)
    max_retries: int = Field(default=0, ge=0)
    retry_delay_seconds: int = Field(default=5, ge=1)
    depends_on: list[str] = Field(default_factory=list)
    outputs: dict[str, str] = Field(default_factory=dict)
    warning_only: bool = Field(default=False)
    services: list[str] = Field(default_factory=list)
    inputs: dict[str, Any] = Field(default_factory=dict)
    rollback: RollbackCommand | None = Field(None)
    required_endpoints: list[str] = Field(default_factory=list)


class RollbackCommand(LightwaveBaseSchema):
    """Rollback command definition."""

    command: str = Field(...)
    timeout_seconds: int = Field(default=120, ge=1)


class StepResult(LightwaveBaseSchema):
    """Result of executing a deployment step."""

    step_name: str
    status: StepStatus = Field(default=StepStatus.PENDING)
    started_at: datetime | None = None
    completed_at: datetime | None = None
    exit_code: int | None = None
    stdout: str = ""
    stderr: str = ""
    outputs: dict[str, Any] = Field(default_factory=dict)
    error_message: str | None = None
    retry_count: int = 0

    @property
    def duration_seconds(self) -> float | None:
        """Calculate step duration."""
        if self.started_at and self.completed_at:
            return (self.completed_at - self.started_at).total_seconds()
        return None

    @property
    def passed(self) -> bool:
        """Check if step passed."""
        return self.status == StepStatus.PASSED

    @property
    def failed(self) -> bool:
        """Check if step failed."""
        return self.status == StepStatus.FAILED


# =============================================================================
# PHASE SCHEMAS
# =============================================================================


class PhaseDefinition(SSTSchema):
    """Definition of a deployment phase from YAML."""

    order: int = Field(..., ge=1)
    name: str = Field(..., min_length=1)
    description: str
    trigger: str
    blocking: bool = True
    steps: dict[str, StepDefinition] = Field(default_factory=dict)

    @property
    def ordered_steps(self) -> list[tuple[str, StepDefinition]]:
        """Return steps in execution order."""
        return sorted(self.steps.items(), key=lambda x: x[1].order)


class PhaseResult(LightwaveBaseSchema):
    """Result of executing a deployment phase."""

    phase_name: str
    phase_type: PhaseType
    status: StepStatus = Field(default=StepStatus.PENDING)
    started_at: datetime | None = None
    completed_at: datetime | None = None
    step_results: dict[str, StepResult] = Field(default_factory=dict)

    @property
    def passed(self) -> bool:
        """Check if all steps passed."""
        return all(r.passed for r in self.step_results.values())

    @property
    def failed_steps(self) -> list[StepResult]:
        """Get list of failed steps."""
        return [r for r in self.step_results.values() if r.failed]

    @property
    def duration_seconds(self) -> float | None:
        """Calculate phase duration."""
        if self.started_at and self.completed_at:
            return (self.completed_at - self.started_at).total_seconds()
        return None


# =============================================================================
# ENVIRONMENT SCHEMAS
# =============================================================================


class InfraConfig(LightwaveBaseSchema):
    """Infrastructure configuration for an environment."""

    cluster: str | None = None
    state_bucket: str | None = None
    region: str | None = Field(default="us-east-1")


class EnvironmentDefinition(SSTSchema):
    """Definition of a deployment environment."""

    name: str
    type: EnvironmentType
    requires_approval: bool = False
    auto_deploy: bool = False
    rollback_enabled: bool = True
    domain_pattern: str | None = None
    infra: InfraConfig = Field(default_factory=InfraConfig)


# =============================================================================
# DEPLOYMENT CONFIG
# =============================================================================


class DeploymentConfig(LightwaveBaseSchema):
    """Configuration for a deployment run."""

    # Target environment
    environment: EnvironmentType = Field(default=EnvironmentType.PRODUCTION)

    # AWS/ECS configuration
    cluster: str = Field(..., min_length=1)
    service: str = Field(..., min_length=1)
    region: str = Field(default="us-east-1")

    # Image configuration
    image_uri: str = Field(..., min_length=1)
    container_name: str = Field(default="web")

    # Health check configuration
    health_check_url: str | None = None
    health_check_timeout: int = Field(default=90, ge=1)
    base_url: str | None = None

    # Deployment options
    wait_for_stability: bool = True
    stability_timeout_minutes: int = Field(default=10, ge=1)

    # Tracking
    previous_task_def_arn: str | None = None

    @field_validator("image_uri")
    @classmethod
    def validate_image_uri(cls, v: str) -> str:
        """Validate ECR image URI format."""
        ecr_pattern = r"^\d+\.dkr\.ecr\.[a-z0-9-]+\.amazonaws\.com/[\w-]+:\w+"
        if not re.match(ecr_pattern, v):
            raise ValueError(
                f"Invalid ECR image URI format: {v}. Expected: <account>.dkr.ecr.<region>.amazonaws.com/<repo>:<tag>"
            )
        return v

    @property
    def short_sha(self) -> str:
        """Extract short SHA from image tag."""
        return self.image_uri.split(":")[-1]


# =============================================================================
# FAILURE MODE SCHEMAS
# =============================================================================


class FailureResolution(LightwaveBaseSchema):
    """Resolution steps for a failure mode."""

    steps: list[str] = Field(default_factory=list, alias="solution")
    prevention: list[str] = Field(default_factory=list)
    diagnosis: list[str] = Field(default_factory=list)


class SchemaUpdate(LightwaveBaseSchema):
    """Schema update suggested by a failure."""

    file: str
    path: str
    expected_value: Any


class FailureMode(SSTSchema):
    """Known failure mode and its resolution."""

    pattern: str
    cause: str
    solution: list[str] = Field(default_factory=list)
    prevention: list[str] = Field(default_factory=list)
    diagnosis: list[str] = Field(default_factory=list)
    schema_update: SchemaUpdate | None = None

    def matches(self, error_message: str) -> bool:
        """Check if this failure mode matches an error message."""
        return self.pattern.lower() in error_message.lower()


# =============================================================================
# ROLLBACK SCHEMAS
# =============================================================================


class RollbackConfig(SSTSchema):
    """Rollback configuration."""

    enabled: bool = True
    trigger_on: list[str] = Field(default_factory=list)
    strategy: str = Field(default="previous_task_definition")


class ManualRollback(LightwaveBaseSchema):
    """Manual rollback configuration."""

    command: str
    verification: list[str] = Field(default_factory=list)


# =============================================================================
# VALIDATION RESULT
# =============================================================================


class ValidationError(LightwaveBaseSchema):
    """A validation error from a step."""

    step: str
    check_type: str
    expected: Any
    actual: Any
    message: str


class ValidationResult(LightwaveBaseSchema):
    """Result of validating a deployment step."""

    passed: bool
    errors: list[ValidationError] = Field(default_factory=list)
    warnings: list[str] = Field(default_factory=list)
    outputs: dict[str, Any] = Field(default_factory=dict)

    @classmethod
    def success(cls, outputs: dict[str, Any] | None = None) -> Self:
        """Create a successful validation result."""
        return cls(passed=True, outputs=outputs or {})

    @classmethod
    def failure(cls, errors: list[ValidationError]) -> Self:
        """Create a failed validation result."""
        return cls(passed=False, errors=errors)


# =============================================================================
# DEPLOYMENT RUN
# =============================================================================


class DeploymentRun(LightwaveBaseSchema):
    """Complete deployment run with all phases."""

    id: str = Field(..., description="Unique deployment run ID")
    config: DeploymentConfig
    started_at: datetime = Field(default_factory=datetime.now)
    completed_at: datetime | None = None
    status: StepStatus = Field(default=StepStatus.RUNNING)
    phases: dict[PhaseType, PhaseResult] = Field(default_factory=dict)
    rollback_triggered: bool = False
    rollback_result: PhaseResult | None = None

    @property
    def passed(self) -> bool:
        """Check if deployment passed."""
        return self.status == StepStatus.PASSED

    @property
    def failed(self) -> bool:
        """Check if deployment failed."""
        return self.status == StepStatus.FAILED

    @property
    def duration_seconds(self) -> float | None:
        """Calculate total deployment duration."""
        if self.completed_at:
            return (self.completed_at - self.started_at).total_seconds()
        return None

    def get_failure_summary(self) -> str:
        """Get summary of failures for error reporting."""
        failures = []
        for phase_type, phase_result in self.phases.items():
            for step_result in phase_result.failed_steps:
                failures.append(
                    f"  {phase_type.value}/{step_result.step_name}: {step_result.error_message or 'Unknown error'}"
                )
        return "\n".join(failures) if failures else "No failures recorded"


# =============================================================================
# VALIDATOR FUNCTIONS
# =============================================================================


class DeploymentValidator:
    """Validator for deployment steps and phases."""

    @staticmethod
    def validate_step(
        step: StepDefinition,
        result: StepResult,
    ) -> ValidationResult:
        """Validate a step's result against its criteria."""
        errors = []
        criteria = step.validation

        # Check exit code
        if criteria.exit_code is not None and result.exit_code != criteria.exit_code:
            errors.append(
                ValidationError(
                    step=step.name,
                    check_type="exit_code",
                    expected=criteria.exit_code,
                    actual=result.exit_code,
                    message=f"Expected exit code {criteria.exit_code}, got {result.exit_code}",
                )
            )

        # Check output equals
        if criteria.output_equals is not None:
            stdout_clean = result.stdout.strip()
            if stdout_clean != criteria.output_equals:
                errors.append(
                    ValidationError(
                        step=step.name,
                        check_type="output_equals",
                        expected=criteria.output_equals,
                        actual=stdout_clean,
                        message=f"Expected output '{criteria.output_equals}', got '{stdout_clean}'",
                    )
                )

        # Check output matches regex
        if criteria.output_matches is not None:
            if not re.search(criteria.output_matches, result.stdout):
                errors.append(
                    ValidationError(
                        step=step.name,
                        check_type="output_matches",
                        expected=criteria.output_matches,
                        actual=result.stdout[:200],
                        message=f"Output did not match pattern '{criteria.output_matches}'",
                    )
                )

        # Check output greater than
        if criteria.output_greater_than is not None:
            try:
                value = float(result.stdout.strip())
                if value <= criteria.output_greater_than:
                    errors.append(
                        ValidationError(
                            step=step.name,
                            check_type="output_greater_than",
                            expected=f"> {criteria.output_greater_than}",
                            actual=value,
                            message=f"Expected value > {criteria.output_greater_than}, got {value}",
                        )
                    )
            except ValueError:
                errors.append(
                    ValidationError(
                        step=step.name,
                        check_type="output_greater_than",
                        expected="numeric value",
                        actual=result.stdout[:50],
                        message="Could not parse output as number",
                    )
                )

        # Check output empty
        if criteria.output_empty is True and result.stdout.strip():
            errors.append(
                ValidationError(
                    step=step.name,
                    check_type="output_empty",
                    expected="empty output",
                    actual=result.stdout[:200],
                    message="Expected empty output but got content",
                )
            )

        # Check file exists
        if criteria.file_exists is not None:
            if not Path(criteria.file_exists).exists():
                errors.append(
                    ValidationError(
                        step=step.name,
                        check_type="file_exists",
                        expected=criteria.file_exists,
                        actual="file not found",
                        message=f"Expected file '{criteria.file_exists}' does not exist",
                    )
                )

        # Check file contains
        if criteria.file_contains is not None:
            file_path = Path(criteria.file_contains.file)
            if file_path.exists():
                content = file_path.read_text()
                if criteria.file_contains.substring not in content:
                    errors.append(
                        ValidationError(
                            step=step.name,
                            check_type="file_contains",
                            expected=f"'{criteria.file_contains.substring}' in file",
                            actual="substring not found",
                            message=f"File '{file_path}' does not contain '{criteria.file_contains.substring}'",
                        )
                    )
            else:
                errors.append(
                    ValidationError(
                        step=step.name,
                        check_type="file_contains",
                        expected=criteria.file_contains.file,
                        actual="file not found",
                        message=f"File '{criteria.file_contains.file}' does not exist",
                    )
                )

        return ValidationResult(passed=len(errors) == 0, errors=errors)

    @staticmethod
    def find_failure_mode(
        error_message: str,
        failure_modes: list[FailureMode],
    ) -> FailureMode | None:
        """Find a matching failure mode for an error message."""
        for mode in failure_modes:
            if mode.matches(error_message):
                return mode
        return None


# =============================================================================
# COMMAND EXECUTOR
# =============================================================================


class CommandResult(LightwaveBaseSchema):
    """Result of executing a command."""

    command: str
    exit_code: int
    stdout: str
    stderr: str
    duration_seconds: float


def execute_command(
    command: str,
    timeout_seconds: int = 60,
    env: dict[str, str] | None = None,
) -> CommandResult:
    """Execute a shell command and return the result."""
    import time

    start_time = time.time()

    try:
        result = subprocess.run(
            command,
            shell=True,
            capture_output=True,
            text=True,
            timeout=timeout_seconds,
            env=env,
        )
        duration = time.time() - start_time

        return CommandResult(
            command=command,
            exit_code=result.returncode,
            stdout=result.stdout,
            stderr=result.stderr,
            duration_seconds=duration,
        )
    except subprocess.TimeoutExpired:
        duration = time.time() - start_time
        return CommandResult(
            command=command,
            exit_code=-1,
            stdout="",
            stderr=f"Command timed out after {timeout_seconds} seconds",
            duration_seconds=duration,
        )
    except Exception as e:
        duration = time.time() - start_time
        return CommandResult(
            command=command,
            exit_code=-1,
            stdout="",
            stderr=str(e),
            duration_seconds=duration,
        )
