"""Healing pipeline models — Failure, Fix, Diagnosis, HealResult.

Provides structured types for the healing pipeline:
- ErrorSignature: structured error classification key
- Failure: a detected execution failure with context
- Fix: a fix action to apply for a failure
- Diagnosis: detailed analysis of a failure's root cause
- HealResult: outcome of a healing attempt
"""

import time
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any
from uuid import UUID, uuid4


@dataclass
class ErrorSignature:
    """Structured error signature for matching against known patterns.

    Signatures are hierarchical: error_type:tool:action:message_pattern
    This allows matching at different granularity levels — from exact
    message matches down to broad error-type matches.
    """

    error_type: str = ""
    tool: str = ""
    action: str = ""
    message_pattern: str = ""

    def to_string(self) -> str:
        """Full colon-delimited signature string."""
        return f"{self.error_type}:{self.tool}:{self.action}:{self.message_pattern}"

    @classmethod
    def from_string(cls, sig: str) -> "ErrorSignature":
        """Parse a colon-delimited signature string."""
        parts = sig.split(":", 3)
        return cls(
            error_type=parts[0] if len(parts) > 0 else "",
            tool=parts[1] if len(parts) > 1 else "",
            action=parts[2] if len(parts) > 2 else "",
            message_pattern=parts[3] if len(parts) > 3 else "",
        )

    @classmethod
    def from_error(
        cls, error_type: str, tool: str, action: str, message: str,
        max_msg_len: int = 50,
    ) -> "ErrorSignature":
        """Create a signature from error details, truncating message."""
        return cls(
            error_type=error_type,
            tool=tool,
            action=action,
            message_pattern=message[:max_msg_len] if message else "",
        )

    def matches_at_level(self, other: "ErrorSignature", level: str) -> bool:
        """Check if this signature matches another at a given granularity.

        Levels: exact, domain (type+tool+action), action (type+tool), generic (type only).
        """
        if level == "exact":
            return self.to_string() == other.to_string()
        if level == "domain":
            return (
                self.error_type == other.error_type
                and self.tool == other.tool
                and self.action == other.action
            )
        if level == "action":
            return self.error_type == other.error_type and self.tool == other.tool
        if level == "generic":
            return self.error_type == other.error_type
        return False

    def to_dict(self) -> dict[str, Any]:
        return {
            "error_type": self.error_type,
            "tool": self.tool,
            "action": self.action,
            "message_pattern": self.message_pattern,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "ErrorSignature":
        return cls(
            error_type=data.get("error_type", ""),
            tool=data.get("tool", ""),
            action=data.get("action", ""),
            message_pattern=data.get("message_pattern", ""),
        )


# ── Severity levels ────────────────────────────────────────────────

SEVERITY_MAP: dict[str, int] = {
    "critical": 4,
    "high": 3,
    "medium": 2,
    "low": 1,
    "info": 0,
}

ERROR_TYPE_SEVERITY: dict[str, str] = {
    "auth": "high",
    "permission": "high",
    "timeout": "medium",
    "rate_limit": "medium",
    "network": "medium",
    "not_found": "low",
    "validation": "low",
    "resource": "high",
    "unknown": "medium",
}


@dataclass
class Failure:
    """A detected failure from execution.

    Captures the full context of a step failure including the error
    classification, affected tool/action, hierarchical signatures for
    pattern matching, and recovery metadata.
    """

    id: UUID = field(default_factory=uuid4)
    run_id: UUID = field(default_factory=uuid4)
    step_number: int = 0
    error_type: str = ""
    error_message: str = ""
    tool: str = ""
    action: str = ""
    signature: str = ""
    context: dict[str, Any] = field(default_factory=dict)
    recoverable: bool = True
    severity: str = "medium"
    occurred_at: float = field(default_factory=time.time)
    attempt_number: int = 1

    def __post_init__(self) -> None:
        if not self.severity or self.severity == "medium":
            self.severity = ERROR_TYPE_SEVERITY.get(self.error_type, "medium")

    def get_hierarchical_signatures(self) -> dict[str, str]:
        """Return signatures at each granularity level for pattern matching."""
        msg_prefix = self.error_message[:50] if self.error_message else ""
        return {
            "exact": f"{self.error_type}:{self.tool}:{self.action}:{msg_prefix}",
            "domain": f"{self.error_type}:{self.tool}:{self.action}",
            "action": f"{self.error_type}:{self.tool}",
            "generic": self.error_type,
        }

    def get_error_signature(self) -> ErrorSignature:
        """Return a structured ErrorSignature object."""
        return ErrorSignature.from_error(
            error_type=self.error_type,
            tool=self.tool,
            action=self.action,
            message=self.error_message,
        )

    @property
    def severity_level(self) -> int:
        """Numeric severity for comparison (0=info, 4=critical)."""
        return SEVERITY_MAP.get(self.severity, 2)

    @property
    def is_critical(self) -> bool:
        return self.severity in ("critical", "high")

    @property
    def age_seconds(self) -> float:
        return time.time() - self.occurred_at

    def to_dict(self) -> dict[str, Any]:
        return {
            "id": str(self.id),
            "run_id": str(self.run_id),
            "step_number": self.step_number,
            "error_type": self.error_type,
            "error_message": self.error_message,
            "tool": self.tool,
            "action": self.action,
            "signature": self.signature,
            "recoverable": self.recoverable,
            "severity": self.severity,
            "attempt_number": self.attempt_number,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "Failure":
        return cls(
            id=UUID(data["id"]) if data.get("id") else uuid4(),
            run_id=UUID(data["run_id"]) if data.get("run_id") else uuid4(),
            step_number=data.get("step_number", 0),
            error_type=data.get("error_type", ""),
            error_message=data.get("error_message", ""),
            tool=data.get("tool", ""),
            action=data.get("action", ""),
            signature=data.get("signature", ""),
            recoverable=data.get("recoverable", True),
            severity=data.get("severity", "medium"),
            attempt_number=data.get("attempt_number", 1),
        )


@dataclass
class Fix:
    """A fix to apply for a failure.

    Describes the remediation action: retry with modified params,
    switch to an alternative tool, skip the step, or escalate to
    the user. Tracks source (pattern vs AI vs rule) and confidence.
    """

    id: UUID = field(default_factory=uuid4)
    failure: Failure = field(default_factory=Failure)
    fix_type: str = ""  # retry, modify_params, skip, alternative_tool, escalate
    fix_params: dict[str, Any] = field(default_factory=dict)
    diagnosis: str = ""
    source: str = ""  # pattern, ai, rule
    pattern_id: UUID | None = None
    confidence: float = 0.5
    applied_at: float | None = None

    # Valid fix types
    VALID_FIX_TYPES = {"retry", "modify_params", "skip", "alternative_tool", "escalate"}

    @property
    def is_retry(self) -> bool:
        return self.fix_type == "retry"

    @property
    def is_escalation(self) -> bool:
        return self.fix_type == "escalate"

    @property
    def is_skip(self) -> bool:
        return self.fix_type == "skip"

    @property
    def is_from_pattern(self) -> bool:
        return self.source == "pattern"

    @property
    def is_high_confidence(self) -> bool:
        return self.confidence >= 0.7

    def mark_applied(self) -> None:
        """Record when this fix was applied."""
        self.applied_at = time.time()

    def get_retry_delay_ms(self) -> int:
        """Extract retry delay from fix params, default 1000ms."""
        return int(self.fix_params.get("delay_ms", 1000))

    def get_timeout_multiplier(self) -> float:
        """Extract timeout multiplier from fix params, default 1.0."""
        return float(self.fix_params.get("multiply_timeout", 1.0))

    def to_dict(self) -> dict[str, Any]:
        return {
            "id": str(self.id),
            "fix_type": self.fix_type,
            "fix_params": self.fix_params,
            "diagnosis": self.diagnosis,
            "source": self.source,
            "pattern_id": str(self.pattern_id) if self.pattern_id else None,
            "confidence": self.confidence,
            "failure_id": str(self.failure.id),
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "Fix":
        return cls(
            id=UUID(data["id"]) if data.get("id") else uuid4(),
            fix_type=data.get("fix_type", ""),
            fix_params=data.get("fix_params", {}),
            diagnosis=data.get("diagnosis", ""),
            source=data.get("source", ""),
            pattern_id=UUID(data["pattern_id"]) if data.get("pattern_id") else None,
            confidence=data.get("confidence", 0.5),
        )


@dataclass
class Diagnosis:
    """Detailed diagnosis of a failure's root cause.

    Contains the analysis reasoning, suggested fix type, and confidence.
    Can be converted to a Fix for application.
    """

    id: UUID = field(default_factory=uuid4)
    failure: Failure = field(default_factory=Failure)
    root_cause: str = ""
    fix_type: str = ""
    fix_params: dict[str, Any] = field(default_factory=dict)
    confidence: float = 0.5
    reasoning: str = ""
    source: str = "rule"  # rule, ai, pattern
    alternatives: list[dict[str, Any]] = field(default_factory=list)
    diagnosed_at: float = field(default_factory=time.time)

    def to_fix(self) -> Fix:
        """Convert this diagnosis into an applicable Fix."""
        return Fix(
            failure=self.failure,
            fix_type=self.fix_type,
            fix_params=self.fix_params,
            diagnosis=self.root_cause,
            source=self.source,
            confidence=self.confidence,
        )

    @property
    def has_alternatives(self) -> bool:
        return len(self.alternatives) > 0

    def best_alternative(self) -> dict[str, Any] | None:
        """Return the highest-confidence alternative, if any."""
        if not self.alternatives:
            return None
        return max(self.alternatives, key=lambda a: a.get("confidence", 0))

    def to_dict(self) -> dict[str, Any]:
        return {
            "id": str(self.id),
            "root_cause": self.root_cause,
            "fix_type": self.fix_type,
            "fix_params": self.fix_params,
            "confidence": self.confidence,
            "reasoning": self.reasoning,
            "source": self.source,
            "alternatives": self.alternatives,
            "failure_id": str(self.failure.id),
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "Diagnosis":
        return cls(
            id=UUID(data["id"]) if data.get("id") else uuid4(),
            root_cause=data.get("root_cause", ""),
            fix_type=data.get("fix_type", ""),
            fix_params=data.get("fix_params", {}),
            confidence=data.get("confidence", 0.5),
            reasoning=data.get("reasoning", ""),
            source=data.get("source", "rule"),
            alternatives=data.get("alternatives", []),
        )


@dataclass
class HealResult:
    """Result of a healing attempt.

    Indicates whether the healer found a viable fix, what fix was
    applied, and timing information. The healing pipeline returns
    this to the engine so it can decide whether to retry the step.
    """

    id: UUID = field(default_factory=uuid4)
    success: bool = False
    fix_applied: Fix | None = None
    diagnosis: Diagnosis | None = None
    message: str = ""
    duration_ms: int = 0
    attempts: int = 1
    created_at: datetime = field(default_factory=datetime.utcnow)

    @property
    def fix_type(self) -> str:
        if self.fix_applied:
            return self.fix_applied.fix_type
        return ""

    @property
    def confidence(self) -> float:
        if self.fix_applied:
            return self.fix_applied.confidence
        return 0.0

    @property
    def is_retry(self) -> bool:
        return self.fix_applied is not None and self.fix_applied.is_retry

    @property
    def is_escalation(self) -> bool:
        return self.fix_applied is not None and self.fix_applied.is_escalation

    @staticmethod
    def failed(message: str) -> "HealResult":
        """Create a failed heal result."""
        return HealResult(success=False, message=message)

    @staticmethod
    def succeeded(fix: Fix, message: str, duration_ms: int = 0) -> "HealResult":
        """Create a successful heal result."""
        return HealResult(
            success=True,
            fix_applied=fix,
            message=message,
            duration_ms=duration_ms,
        )

    def to_dict(self) -> dict[str, Any]:
        result: dict[str, Any] = {
            "id": str(self.id),
            "success": self.success,
            "message": self.message,
            "duration_ms": self.duration_ms,
            "attempts": self.attempts,
        }
        if self.fix_applied:
            result["fix_applied"] = self.fix_applied.to_dict()
        if self.diagnosis:
            result["diagnosis"] = self.diagnosis.to_dict()
        return result

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "HealResult":
        return cls(
            id=UUID(data["id"]) if data.get("id") else uuid4(),
            success=data.get("success", False),
            message=data.get("message", ""),
            duration_ms=data.get("duration_ms", 0),
            attempts=data.get("attempts", 1),
            fix_applied=Fix.from_dict(data["fix_applied"]) if data.get("fix_applied") else None,
            diagnosis=Diagnosis.from_dict(data["diagnosis"]) if data.get("diagnosis") else None,
        )
