"""Validation apply gate: run attached validations before apply.

Orchestrates the execution of attached validations before the apply phase
and gates apply based on required/informational validation outcomes.

Core types:

- **ValidationAttachment**: A reference to a validation attached to a resource.
- **ApplyValidationResult**: Per-validation pass/fail result.
- **ApplyValidationSummary**: Aggregated results with gating decision.
- **ValidationRunner**: Protocol for executing a single validation.
- **DefaultValidationRunner**: Stub implementation using text matching.
- **ApplyValidationGate**: Orchestrates validation execution and gating.

Usage::

    gate = ApplyValidationGate(runner=DefaultValidationRunner())
    summary = gate.run(attachments, plan_context)
    if summary.all_required_passed:
        # proceed to apply
    else:
        # block apply, record failures
"""

from __future__ import annotations

import time
from abc import ABC, abstractmethod
from datetime import UTC, datetime
from enum import StrEnum
from typing import Any

from pydantic import BaseModel, ConfigDict, Field

from cleveragents.domain.models.core.tool import ValidationMode

# ---------------------------------------------------------------------------
# Enums
# ---------------------------------------------------------------------------


class AttachmentScope(StrEnum):
    """Scope of a validation attachment."""

    DIRECT = "direct"
    PROJECT = "project"
    PLAN = "plan"


# ---------------------------------------------------------------------------
# Models
# ---------------------------------------------------------------------------


class ValidationAttachment(BaseModel):
    """A validation attached to a resource for apply gating.

    Represents a single validation-to-resource binding with scope
    and mode information.
    """

    attachment_id: str = Field(..., description="Unique attachment ULID")
    validation_name: str = Field(..., min_length=1, description="Validation tool name")
    resource_id: str = Field(..., description="Target resource ID")
    resource_name: str = Field(default="", description="Target resource name")
    mode: ValidationMode = Field(
        default=ValidationMode.REQUIRED,
        description="Whether failure blocks apply",
    )
    scope: AttachmentScope = Field(
        default=AttachmentScope.DIRECT,
        description="Attachment scope level",
    )
    scope_target: str = Field(
        default="",
        description="Scope target (project name or plan ID)",
    )
    arguments: dict[str, Any] = Field(
        default_factory=dict,
        description="Arguments to pass to the validation",
    )

    model_config = ConfigDict(
        str_strip_whitespace=True,
        validate_assignment=True,
    )


class ApplyValidationResult(BaseModel):
    """Result of executing a single validation."""

    attachment_id: str = Field(..., description="Attachment that was evaluated")
    validation_name: str = Field(..., description="Validation tool name")
    resource_id: str = Field(..., description="Resource the validation ran on")
    mode: ValidationMode = Field(..., description="Required or informational")
    passed: bool = Field(..., description="Whether the validation passed")
    message: str = Field(default="", description="Result message")
    data: dict[str, Any] = Field(
        default_factory=dict, description="Additional result data"
    )
    duration_ms: int = Field(default=0, ge=0, description="Execution time in ms")
    error: str | None = Field(
        default=None, description="Error message if execution failed"
    )
    timed_out: bool = Field(default=False, description="Whether execution timed out")

    model_config = ConfigDict(
        str_strip_whitespace=True,
        validate_assignment=True,
    )


class ApplyValidationSummary(BaseModel):
    """Aggregated results from all validations run before apply."""

    plan_id: str = Field(..., description="Plan ULID")
    results: list[ApplyValidationResult] = Field(
        default_factory=list,
        description="Per-validation results",
    )
    evaluated_at: datetime = Field(
        default_factory=lambda: datetime.now(tz=UTC),
        description="When the validation gate was evaluated",
    )

    model_config = ConfigDict(
        str_strip_whitespace=True,
        validate_assignment=True,
    )

    @property
    def total(self) -> int:
        """Total number of validations run."""
        return len(self.results)

    @property
    def required_passed(self) -> int:
        """Number of required validations that passed."""
        return sum(
            1 for r in self.results if r.mode == ValidationMode.REQUIRED and r.passed
        )

    @property
    def required_total(self) -> int:
        """Total number of required validations that were evaluated."""
        return sum(1 for r in self.results if r.mode == ValidationMode.REQUIRED)

    @property
    def required_failed(self) -> int:
        """Number of required validations that failed."""
        return sum(
            1
            for r in self.results
            if r.mode == ValidationMode.REQUIRED and not r.passed
        )

    @property
    def informational_passed(self) -> int:
        """Number of informational validations that passed."""
        return sum(
            1
            for r in self.results
            if r.mode == ValidationMode.INFORMATIONAL and r.passed
        )

    @property
    def informational_failed(self) -> int:
        """Number of informational validations that failed."""
        return sum(
            1
            for r in self.results
            if r.mode == ValidationMode.INFORMATIONAL and not r.passed
        )

    @property
    def all_required_passed(self) -> bool:
        """Whether all required validations passed."""
        if self.is_empty:
            return False
        return self.required_failed == 0

    @property
    def is_empty(self) -> bool:
        """Whether no validations were run."""
        return self.total == 0

    def to_plan_metadata(self) -> dict[str, Any]:
        """Convert to a dict for plan validation_summary field.

        Compatible with the plan model's ``validation_summary`` field.
        """
        return {
            "total": self.total,
            "required_passed": self.required_passed,
            "required_failed": self.required_failed,
            "informational_passed": self.informational_passed,
            "informational_failed": self.informational_failed,
            "all_required_passed": self.all_required_passed,
            "evaluated_at": self.evaluated_at.isoformat(),
            "results": [
                {
                    "attachment_id": r.attachment_id,
                    "validation_name": r.validation_name,
                    "resource_id": r.resource_id,
                    "mode": r.mode.value,
                    "passed": r.passed,
                    "message": r.message,
                    "duration_ms": r.duration_ms,
                    "error": r.error,
                    "timed_out": r.timed_out,
                }
                for r in self.results
            ],
        }

    def format_cli_output(self) -> str:
        """Format a human-readable summary for CLI display."""
        lines: list[str] = []
        status = "PASSED" if self.all_required_passed else "BLOCKED"
        lines.append(f"Validation Gate: {status}")
        lines.append(
            f"  Required: {self.required_passed} passed, {self.required_failed} failed"
        )
        lines.append(
            f"  Informational: {self.informational_passed} passed, "
            f"{self.informational_failed} failed"
        )
        if self.required_failed > 0:
            lines.append("  Failures:")
            for r in self.results:
                if r.mode == ValidationMode.REQUIRED and not r.passed:
                    msg = r.message or r.error or "no details"
                    lines.append(f"    - {r.validation_name} on {r.resource_id}: {msg}")
        return "\n".join(lines)


# ---------------------------------------------------------------------------
# Runner interface
# ---------------------------------------------------------------------------


class ValidationRunner(ABC):
    """Interface for executing a single validation.

    Implementations handle the actual invocation of a validation tool
    (e.g., subprocess, MCP call, in-process function).
    """

    @abstractmethod
    def run_validation(
        self,
        attachment: ValidationAttachment,
        context: dict[str, Any],
    ) -> ApplyValidationResult:
        """Execute a single validation.

        Args:
            attachment: The validation attachment to execute.
            context: Plan context (changeset summary, metadata, etc.).

        Returns:
            The validation result.
        """
        ...


class DefaultValidationRunner(ValidationRunner):
    """Default validation runner using text matching.

    For each attachment, checks if the validation name appears in the
    context values. This is a simple default for development and testing.
    Production implementations should use the full tool execution pipeline.
    """

    def run_validation(
        self,
        attachment: ValidationAttachment,
        context: dict[str, Any],
    ) -> ApplyValidationResult:
        """Execute a validation via text matching against context.

        Checks if the validation name (or any argument value) appears
        in the context. This is a placeholder — real implementations
        invoke the actual validation tool.
        """
        start = time.monotonic()
        try:
            context_text = " ".join(f"{k}={v}" for k, v in context.items()).lower()
            val_name = attachment.validation_name.lower()

            # Simple heuristic: check if validation name is referenced in context
            passed = val_name in context_text
            if not passed:
                # Check arguments
                for val in attachment.arguments.values():
                    if str(val).lower() in context_text:
                        passed = True
                        break

            elapsed = int((time.monotonic() - start) * 1000)
            return ApplyValidationResult(
                attachment_id=attachment.attachment_id,
                validation_name=attachment.validation_name,
                resource_id=attachment.resource_id,
                mode=attachment.mode,
                passed=passed,
                message="matched" if passed else "not matched",
                duration_ms=elapsed,
            )
        except Exception as exc:
            elapsed = int((time.monotonic() - start) * 1000)
            return ApplyValidationResult(
                attachment_id=attachment.attachment_id,
                validation_name=attachment.validation_name,
                resource_id=attachment.resource_id,
                mode=attachment.mode,
                passed=False,
                message="",
                error=str(exc),
                duration_ms=elapsed,
            )


# ---------------------------------------------------------------------------
# Apply validation gate
# ---------------------------------------------------------------------------


class ApplyValidationGate:
    """Orchestrates validation execution and apply gating.

    Collects applicable validation attachments, runs them via the
    provided runner, and produces an ``ApplyValidationSummary``
    that determines whether apply should proceed.
    """

    def __init__(self, runner: ValidationRunner) -> None:
        """Initialise the gate with a validation runner.

        Args:
            runner: The ``ValidationRunner`` to execute each validation.
        """
        self._runner = runner

    def run(
        self,
        plan_id: str,
        attachments: list[ValidationAttachment],
        context: dict[str, Any],
    ) -> ApplyValidationSummary:
        """Run all attached validations and return gating result.

        Executes each validation attachment against the provided context
        and returns an aggregated summary.

        Args:
            plan_id: The plan ULID.
            attachments: List of validation attachments to evaluate.
            context: Plan context for evaluation.

        Returns:
            An ``ApplyValidationSummary`` with per-validation results
            and the overall gating decision.
        """
        results: list[ApplyValidationResult] = []
        for attachment in attachments:
            result = self._runner.run_validation(attachment, context)
            results.append(result)

        return ApplyValidationSummary(
            plan_id=plan_id,
            results=results,
        )

    def should_block_apply(
        self,
        summary: ApplyValidationSummary,
    ) -> bool:
        """Determine if apply should be blocked.

        Args:
            summary: The validation summary to check.

        Returns:
            ``True`` if apply should be blocked (required failures exist).
        """
        return not summary.all_required_passed

    def get_failure_reasons(
        self,
        summary: ApplyValidationSummary,
    ) -> list[str]:
        """Extract human-readable failure reasons from a summary.

        Args:
            summary: The validation summary.

        Returns:
            List of failure reason strings for required failures.
        """
        reasons: list[str] = []
        for r in summary.results:
            if r.mode == ValidationMode.REQUIRED and not r.passed:
                if r.error:
                    reasons.append(
                        f"{r.validation_name} on {r.resource_id}: error: {r.error}"
                    )
                else:
                    reasons.append(
                        f"{r.validation_name} on {r.resource_id}: "
                        f"{r.message or 'failed'}"
                    )
        return reasons
