"""
Agent Error Contract - Error Schema for Agent Execution Failures.

Derived from: definitions/ai/contracts/agent_error.yaml

This module provides typed error handling for agent executions. All error
codes and categories derive from the SST YAML definition.

Usage:
    from lightwave.schema.pydantic.contracts.ai import AgentError, ErrorCode

    error = AgentError(
        code=ErrorCode.TIMEOUT,
        message="Agent execution timed out after 300 seconds",
        recoverable=True,
        retry_after_ms=5000,
    )
"""

from __future__ import annotations

from enum import Enum
from typing import Any

from pydantic import Field, field_validator

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


class ErrorCode(str, Enum):
    """Machine-readable error codes derived from SST."""

    # Timeout errors
    TIMEOUT = "timeout"
    TOOL_TIMEOUT = "tool_timeout"

    # Rate limit errors
    RATE_LIMIT = "rate_limit"
    QUOTA_EXCEEDED = "quota_exceeded"

    # Authentication errors
    UNAUTHORIZED = "unauthorized"
    FORBIDDEN = "forbidden"
    INVALID_TENANT = "invalid_tenant"

    # Validation errors
    INVALID_REQUEST = "invalid_request"
    INVALID_AGENT_TYPE = "invalid_agent_type"
    MESSAGE_TOO_LONG = "message_too_long"

    # Agent errors
    AGENT_ERROR = "agent_error"
    AGENT_UNAVAILABLE = "agent_unavailable"
    MODEL_ERROR = "model_error"
    TOOL_ERROR = "tool_error"
    CONTEXT_OVERFLOW = "context_overflow"

    # System errors
    INTERNAL_ERROR = "internal_error"
    SERVICE_UNAVAILABLE = "service_unavailable"
    DATABASE_ERROR = "database_error"

    # Cancellation
    CANCELLED = "cancelled"
    USER_ABORT = "user_abort"

    # Routing errors
    HANDOFF_FAILED = "handoff_failed"
    ROUTING_FAILED = "routing_failed"


class ErrorCategory(str, Enum):
    """Error categories for grouping error codes."""

    TIMEOUT = "timeout"
    RATE_LIMIT = "rate_limit"
    AUTH = "auth"
    VALIDATION = "validation"
    AGENT = "agent"
    SYSTEM = "system"
    USER = "user"
    ROUTING = "routing"


# Mapping of error codes to their categories
ERROR_CODE_CATEGORIES: dict[ErrorCode, ErrorCategory] = {
    ErrorCode.TIMEOUT: ErrorCategory.TIMEOUT,
    ErrorCode.TOOL_TIMEOUT: ErrorCategory.TIMEOUT,
    ErrorCode.RATE_LIMIT: ErrorCategory.RATE_LIMIT,
    ErrorCode.QUOTA_EXCEEDED: ErrorCategory.RATE_LIMIT,
    ErrorCode.UNAUTHORIZED: ErrorCategory.AUTH,
    ErrorCode.FORBIDDEN: ErrorCategory.AUTH,
    ErrorCode.INVALID_TENANT: ErrorCategory.AUTH,
    ErrorCode.INVALID_REQUEST: ErrorCategory.VALIDATION,
    ErrorCode.INVALID_AGENT_TYPE: ErrorCategory.VALIDATION,
    ErrorCode.MESSAGE_TOO_LONG: ErrorCategory.VALIDATION,
    ErrorCode.AGENT_ERROR: ErrorCategory.AGENT,
    ErrorCode.AGENT_UNAVAILABLE: ErrorCategory.AGENT,
    ErrorCode.MODEL_ERROR: ErrorCategory.AGENT,
    ErrorCode.TOOL_ERROR: ErrorCategory.AGENT,
    ErrorCode.CONTEXT_OVERFLOW: ErrorCategory.AGENT,
    ErrorCode.INTERNAL_ERROR: ErrorCategory.SYSTEM,
    ErrorCode.SERVICE_UNAVAILABLE: ErrorCategory.SYSTEM,
    ErrorCode.DATABASE_ERROR: ErrorCategory.SYSTEM,
    ErrorCode.CANCELLED: ErrorCategory.USER,
    ErrorCode.USER_ABORT: ErrorCategory.USER,
    ErrorCode.HANDOFF_FAILED: ErrorCategory.ROUTING,
    ErrorCode.ROUTING_FAILED: ErrorCategory.ROUTING,
}

# Default retry delays for recoverable errors
DEFAULT_RETRY_DELAYS: dict[ErrorCode, int] = {
    ErrorCode.TIMEOUT: 5000,
    ErrorCode.TOOL_TIMEOUT: 3000,
    ErrorCode.RATE_LIMIT: 60000,
    ErrorCode.AGENT_ERROR: 1000,
    ErrorCode.AGENT_UNAVAILABLE: 5000,
    ErrorCode.MODEL_ERROR: 2000,
    ErrorCode.TOOL_ERROR: 1000,
    ErrorCode.INTERNAL_ERROR: 5000,
    ErrorCode.SERVICE_UNAVAILABLE: 10000,
    ErrorCode.DATABASE_ERROR: 5000,
    ErrorCode.HANDOFF_FAILED: 1000,
    ErrorCode.ROUTING_FAILED: 1000,
}

# Non-retryable error codes
NON_RETRYABLE_CODES: set[ErrorCode] = {
    ErrorCode.UNAUTHORIZED,
    ErrorCode.FORBIDDEN,
    ErrorCode.INVALID_TENANT,
    ErrorCode.INVALID_REQUEST,
    ErrorCode.INVALID_AGENT_TYPE,
    ErrorCode.MESSAGE_TOO_LONG,
    ErrorCode.QUOTA_EXCEEDED,
    ErrorCode.CONTEXT_OVERFLOW,
    ErrorCode.CANCELLED,
    ErrorCode.USER_ABORT,
}

# HTTP status code mapping
HTTP_STATUS_MAPPING: dict[ErrorCode, int] = {
    ErrorCode.TIMEOUT: 504,
    ErrorCode.TOOL_TIMEOUT: 504,
    ErrorCode.RATE_LIMIT: 429,
    ErrorCode.QUOTA_EXCEEDED: 429,
    ErrorCode.UNAUTHORIZED: 401,
    ErrorCode.FORBIDDEN: 403,
    ErrorCode.INVALID_TENANT: 404,
    ErrorCode.INVALID_REQUEST: 400,
    ErrorCode.INVALID_AGENT_TYPE: 400,
    ErrorCode.MESSAGE_TOO_LONG: 400,
    ErrorCode.AGENT_ERROR: 500,
    ErrorCode.AGENT_UNAVAILABLE: 503,
    ErrorCode.MODEL_ERROR: 502,
    ErrorCode.TOOL_ERROR: 500,
    ErrorCode.CONTEXT_OVERFLOW: 400,
    ErrorCode.INTERNAL_ERROR: 500,
    ErrorCode.SERVICE_UNAVAILABLE: 503,
    ErrorCode.DATABASE_ERROR: 500,
    ErrorCode.CANCELLED: 499,
    ErrorCode.USER_ABORT: 499,
    ErrorCode.HANDOFF_FAILED: 500,
    ErrorCode.ROUTING_FAILED: 500,
}


class AgentError(LightwaveBaseSchema):
    """
    Error details for agent execution failures.

    Provides structured error information including:
    - Machine-readable error code for programmatic handling
    - Human-readable message for user feedback
    - Recovery hints when applicable
    - Retry guidance for recoverable errors

    Example:
        error = AgentError(
            code=ErrorCode.TIMEOUT,
            message="Agent execution timed out",
            recoverable=True,
            retry_after_ms=5000,
        )

        if error.is_retryable():
            await asyncio.sleep(error.retry_after_ms / 1000)
            # retry...
    """

    code: ErrorCode = Field(
        ...,
        description="Machine-readable error code",
    )

    message: str = Field(
        ...,
        max_length=1000,
        description="Human-readable error message",
    )

    category: ErrorCategory | None = Field(
        None,
        description="Error category for grouping",
    )

    recoverable: bool = Field(
        False,
        description="Whether the error can be recovered from",
    )

    retry_after_ms: int | None = Field(
        None,
        ge=0,
        le=3600000,
        description="Suggested retry delay in milliseconds",
    )

    details: dict[str, Any] | None = Field(
        None,
        description="Additional error context",
    )

    stack_trace: str | None = Field(
        None,
        max_length=10000,
        description="Stack trace for debugging (internal only)",
    )

    correlation_id: str | None = Field(
        None,
        max_length=100,
        description="Correlation ID for log tracing",
    )

    recovery_hint: str | None = Field(
        None,
        max_length=500,
        description="Suggestion for how to recover",
    )

    documentation_url: str | None = Field(
        None,
        description="Link to relevant documentation",
    )

    @field_validator("category", mode="before")
    @classmethod
    def set_category_from_code(cls, v: ErrorCategory | None, info) -> ErrorCategory | None:
        """Auto-set category based on error code if not provided."""
        if v is None and "code" in info.data:
            return ERROR_CODE_CATEGORIES.get(info.data["code"])
        return v

    def model_post_init(self, __context: Any) -> None:
        """Set defaults based on error code after initialization."""
        # Set category if not provided
        if self.category is None:
            object.__setattr__(self, "category", ERROR_CODE_CATEGORIES.get(self.code))

        # Set recoverable based on code if using default
        if not self.recoverable and self.code not in NON_RETRYABLE_CODES:
            object.__setattr__(self, "recoverable", True)

        # Set default retry delay if recoverable and not provided
        if self.recoverable and self.retry_after_ms is None:
            default_delay = DEFAULT_RETRY_DELAYS.get(self.code)
            if default_delay:
                object.__setattr__(self, "retry_after_ms", default_delay)

    def is_retryable(self) -> bool:
        """Check if this error should be retried."""
        return self.recoverable and self.code not in NON_RETRYABLE_CODES

    def get_http_status(self) -> int:
        """Get the appropriate HTTP status code for this error."""
        return HTTP_STATUS_MAPPING.get(self.code, 500)

    @classmethod
    def from_exception(
        cls,
        exc: Exception,
        code: ErrorCode = ErrorCode.INTERNAL_ERROR,
        correlation_id: str | None = None,
    ) -> "AgentError":
        """
        Create an AgentError from an exception.

        Args:
            exc: The exception that occurred
            code: Error code to use (defaults to INTERNAL_ERROR)
            correlation_id: Optional correlation ID for tracing

        Returns:
            AgentError instance
        """
        import traceback

        return cls(
            code=code,
            message=str(exc),
            stack_trace=traceback.format_exc(),
            correlation_id=correlation_id,
        )
