"""Failure detector — classifies execution failures by type and severity.

Analyzes execution results to detect, classify, and enrich failures.
Supports:
- Pattern-based error classification (timeout, auth, network, etc.)
- Severity assignment based on error type
- Batch detection across multiple results
- Error chain tracking for cascading failures
- Detection statistics
"""

import re
from typing import Any
from uuid import UUID

from xyglue.plan.models import Step
from xyglue.execute.models import ExecutionResult
from xyglue.heal.models import Failure, ERROR_TYPE_SEVERITY


class FailureDetector:
    """Detects and classifies failures from execution results.

    Uses regex patterns to classify errors into types (timeout, auth,
    network, etc.), assigns severity, determines recoverability, and
    tracks detection statistics for observability.
    """

    ERROR_PATTERNS: dict[str, list[str]] = {
        "timeout": [
            r"timeout", r"timed?\s*out", r"deadline exceeded",
            r"took too long", r"exceeded.*time",
        ],
        "auth": [
            r"auth", r"unauthorized", r"forbidden", r"401", r"403",
            r"invalid.*token", r"expired.*token", r"invalid.*key",
            r"api.?key", r"credentials",
        ],
        "not_found": [
            r"not found", r"404", r"no such", r"missing",
            r"does not exist", r"nonexistent", r"no.*available",
        ],
        "rate_limit": [
            r"rate limit", r"too many requests", r"429", r"throttl",
            r"quota exceeded", r"slow down", r"backoff",
        ],
        "network": [
            r"connection", r"network", r"dns", r"unreachable",
            r"econnrefused", r"econnreset", r"etimedout",
            r"socket", r"host.*not.*found", r"ssl",
        ],
        "validation": [
            r"invalid", r"validation", r"malformed", r"bad request",
            r"400", r"schema", r"required field", r"type error",
            r"parse error", r"json.*error", r"syntax error",
        ],
        "permission": [
            r"permission", r"denied", r"access.*denied",
            r"not allowed", r"insufficient.*priv",
        ],
        "resource": [
            r"out of memory", r"disk full", r"quota",
            r"resource.*exhausted", r"no space", r"oom",
            r"memory.*limit", r"too large", r"payload.*too",
        ],
        "conflict": [
            r"conflict", r"409", r"already exists",
            r"duplicate", r"unique.*constraint", r"deadlock",
        ],
        "server_error": [
            r"500", r"502", r"503", r"504", r"internal.*error",
            r"service.*unavailable", r"bad.*gateway",
        ],
    }

    # Error types that are NOT recoverable by default
    NON_RECOVERABLE: set[str] = {"auth", "permission"}

    # Error types that are always retryable
    ALWAYS_RETRYABLE: set[str] = {
        "timeout", "rate_limit", "network", "server_error",
    }

    def __init__(self) -> None:
        self._detection_count: int = 0
        self._type_counts: dict[str, int] = {}
        self._recent_failures: list[Failure] = []
        self._max_recent: int = 100

    # ── Single detection ───────────────────────────────────────────

    def detect(
        self, run_id: UUID, step: Step, result: ExecutionResult
    ) -> Failure | None:
        """Detect and classify a failure from an execution result.

        Returns None if the result was successful (no failure).
        """
        if result.success:
            return None

        error_msg = result.error or ""
        error_type = self._classify_error(error_msg)
        severity = ERROR_TYPE_SEVERITY.get(error_type, "medium")
        signature = f"{error_type}:{step.tool}:{step.action}:{error_msg[:50]}"

        failure = Failure(
            run_id=run_id,
            step_number=step.number,
            error_type=error_type,
            error_message=error_msg,
            tool=step.tool,
            action=step.action,
            signature=signature,
            context=self._build_context(step, result),
            recoverable=self._is_recoverable(error_type, error_msg),
            severity=severity,
        )

        # Track statistics
        self._detection_count += 1
        self._type_counts[error_type] = self._type_counts.get(error_type, 0) + 1
        self._track_recent(failure)

        return failure

    # ── Batch detection ────────────────────────────────────────────

    def detect_batch(
        self,
        run_id: UUID,
        steps: list[Step],
        results: list[ExecutionResult],
    ) -> list[Failure]:
        """Detect failures across multiple step results.

        Returns a list of failures (only for failed results).
        Steps and results must be aligned by index.
        """
        failures: list[Failure] = []
        for step, result in zip(steps, results):
            failure = self.detect(run_id, step, result)
            if failure is not None:
                failures.append(failure)
        return failures

    # ── Error classification ───────────────────────────────────────

    def _classify_error(self, error_msg: str) -> str:
        """Classify an error message into an error type using regex patterns.

        Tries all patterns and returns the first match. Returns 'unknown'
        if no patterns match.
        """
        error_lower = error_msg.lower()
        for error_type, patterns in self.ERROR_PATTERNS.items():
            for pattern in patterns:
                if re.search(pattern, error_lower):
                    return error_type
        return "unknown"

    def classify_error(self, error_msg: str) -> str:
        """Public API for error classification (for external callers)."""
        return self._classify_error(error_msg)

    def _is_recoverable(self, error_type: str, error_msg: str = "") -> bool:
        """Determine if a failure type is potentially recoverable.

        Auth and permission errors are not recoverable by default.
        Additionally, certain message patterns indicate non-recoverability.
        """
        if error_type in self.NON_RECOVERABLE:
            return False
        # Check for specific non-recoverable message patterns
        non_recoverable_messages = [
            "permanently deleted",
            "account suspended",
            "banned",
            "revoked",
        ]
        msg_lower = error_msg.lower()
        for pattern in non_recoverable_messages:
            if pattern in msg_lower:
                return False
        return True

    def is_retryable(self, error_type: str) -> bool:
        """Check if an error type is always safe to retry."""
        return error_type in self.ALWAYS_RETRYABLE

    # ── Context enrichment ─────────────────────────────────────────

    def _build_context(
        self, step: Step, result: ExecutionResult,
    ) -> dict[str, Any]:
        """Build context dict enriching the failure with step metadata."""
        ctx: dict[str, Any] = {
            "step_tool": step.tool,
            "step_action": step.action,
            "step_idempotent": step.idempotent,
            "step_has_fallback": step.has_fallback,
            "step_has_rollback": step.has_rollback,
            "duration_ms": result.duration_ms,
            "was_retried": result.was_retried,
            "used_fallback": result.used_fallback,
        }
        if step.fallback_tool:
            ctx["fallback_tool"] = step.fallback_tool
        if result.metadata:
            ctx["result_metadata"] = result.metadata
        return ctx

    # ── Error chain detection ──────────────────────────────────────

    def detect_cascade(self, failures: list[Failure]) -> list[list[Failure]]:
        """Detect cascading failure chains.

        Groups consecutive failures that may be causally related (same
        tool, escalating severity, or network-related chains).
        Returns a list of chains, where each chain is a list of failures.
        """
        if len(failures) < 2:
            return []

        chains: list[list[Failure]] = []
        current_chain: list[Failure] = [failures[0]]

        for i in range(1, len(failures)):
            prev = failures[i - 1]
            curr = failures[i]

            # Same tool, consecutive steps = likely cascade
            is_cascade = (
                curr.tool == prev.tool
                or curr.error_type == prev.error_type
                or (prev.error_type == "network" and curr.error_type in ("timeout", "network"))
                or curr.step_number == prev.step_number + 1
            )

            if is_cascade:
                current_chain.append(curr)
            else:
                if len(current_chain) >= 2:
                    chains.append(current_chain)
                current_chain = [curr]

        if len(current_chain) >= 2:
            chains.append(current_chain)

        return chains

    # ── Recent failure tracking ────────────────────────────────────

    def _track_recent(self, failure: Failure) -> None:
        """Track recent failures for cascade detection and stats."""
        self._recent_failures.append(failure)
        if len(self._recent_failures) > self._max_recent:
            self._recent_failures = self._recent_failures[-self._max_recent:]

    def get_recent_failures(self, limit: int = 10) -> list[Failure]:
        """Get the most recent detected failures."""
        return list(self._recent_failures[-limit:])

    def get_recent_by_type(self, error_type: str) -> list[Failure]:
        """Get recent failures of a specific type."""
        return [f for f in self._recent_failures if f.error_type == error_type]

    # ── Statistics ─────────────────────────────────────────────────

    def get_stats(self) -> dict[str, Any]:
        """Return detection statistics."""
        return {
            "total_detected": self._detection_count,
            "by_type": dict(self._type_counts),
            "recent_count": len(self._recent_failures),
        }

    def reset_stats(self) -> None:
        """Reset all detection statistics."""
        self._detection_count = 0
        self._type_counts.clear()
        self._recent_failures.clear()
