"""AiCippy exception hierarchy and error handling utilities.

This module defines the standard exception classes and error codes used
throughout the AiCippy codebase to ensure consistent error reporting,
proper context preservation, and structured debugging information.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from enum import IntEnum
from typing import Any, Self


class ErrorCode(IntEnum):
    """Standardized error codes for AiCippy exceptions."""

    # General errors (1000-1099)
    UNKNOWN = 1000
    CONFIGURATION = 1001
    VALIDATION = 1002
    TIMEOUT = 1003

    # Authentication errors (1100-1199)
    AUTH_REQUIRED = 1100
    AUTH_EXPIRED = 1101
    AUTH_INVALID = 1102
    AUTH_REFRESH_FAILED = 1103
    AUTH_STORAGE_ERROR = 1104

    # Agent errors (1200-1299)
    AGENT_SPAWN_FAILED = 1200
    AGENT_TIMEOUT = 1201
    AGENT_EXECUTION_ERROR = 1202
    AGENT_QUOTA_EXCEEDED = 1204

    # Connector errors (1300-1399)
    CONNECTOR_UNAVAILABLE = 1300
    CONNECTOR_COMMAND_BLOCKED = 1301
    CONNECTOR_EXECUTION_FAILED = 1302
    CONNECTOR_TIMEOUT = 1303

    # Knowledge Base errors (1500-1599)
    KB_STORAGE_ERROR = 1503

    # Model/Bedrock errors (1700-1799)
    MODEL_INVOCATION_FAILED = 1700
    MODEL_RATE_LIMITED = 1701
    MODEL_CONTENT_FILTERED = 1702
    MODEL_CONTEXT_EXCEEDED = 1703

    # Billing/Plan errors (1800-1899)
    PLAN_VALIDATION_FAILED = 1800
    PLAN_NOT_FOUND = 1801
    PLAN_INACTIVE = 1802
    PLAN_EXPIRED = 1803
    PLAN_WRONG_TIER = 1804
    CREDITS_EXHAUSTED = 1805
    CREDIT_DEDUCTION_FAILED = 1806


@dataclass(frozen=True, slots=True)
class ErrorContext:
    """Immutable context information for error debugging.

    Attributes:
        correlation_id: Request correlation ID for tracing.
        operation: Name of the operation that failed.
        details: Additional context details.

    """

    correlation_id: str | None = None
    operation: str | None = None
    details: dict[str, Any] = field(default_factory=dict)

    def with_detail(self, key: str, value: object) -> Self:
        """Create new context with additional detail.

        Args:
            key: Detail key.
            value: Detail value.

        Returns:
            A new ErrorContext instance with the added detail.

        """
        new_details = {**self.details, key: value}
        return self.__class__(
            correlation_id=self.correlation_id,
            operation=self.operation,
            details=new_details,
        )


class AiCippyError(Exception):
    """Base exception for all AiCippy errors.

    Provides structured error information including error codes,
    context preservation, and proper exception chaining.

    Attributes:
        message: Human-readable error message.
        code: Standardized error code.
        context: Additional debugging context.
        recoverable: Whether the error is potentially recoverable.

    """

    __slots__ = ("code", "context", "message", "recoverable")

    def __init__(
        self,
        message: str,
        code: ErrorCode = ErrorCode.UNKNOWN,
        context: ErrorContext | None = None,
        recoverable: bool = False,
    ) -> None:
        """Initialize AiCippy error.

        Args:
            message: Human-readable error description.
            code: Standardized error code for programmatic handling.
            context: Additional context for debugging.
            recoverable: Indicates if retry/recovery is possible.

        """
        super().__init__(message)
        self.message = message
        self.code = code
        self.context = context or ErrorContext()
        self.recoverable = recoverable

    def __str__(self) -> str:
        """Format error for display."""
        parts = [f"[{self.code.name}] {self.message}"]
        if self.context.operation:
            parts.append(f"Operation: {self.context.operation}")
        if self.context.correlation_id:
            parts.append(f"Correlation ID: {self.context.correlation_id}")
        return " | ".join(parts)

    def __repr__(self) -> str:
        """Detailed representation for debugging."""
        return (
            f"{self.__class__.__name__}("
            f"message={self.message!r}, "
            f"code={self.code.name}, "
            f"operation={self.context.operation!r}"
            f")"
        )


class PlatformApiError(AiCippyError):
    """Exception raised for platform API errors."""

    def __init__(
        self,
        message: str,
        code: ErrorCode = ErrorCode.PLAN_VALIDATION_FAILED,
        context: ErrorContext | None = None,
    ) -> None:
        """Initialize platform API error.

        Args:
            message: Error message.
            code: Error code.
            context: Error context.

        """
        super().__init__(message, code, context)


class ConfigurationError(AiCippyError):
    """Exception raised for configuration-related errors."""

    def __init__(
        self,
        message: str,
        context: ErrorContext | None = None,
    ) -> None:
        """Initialize configuration error.

        Args:
            message: Error message.
            context: Error context.

        """
        super().__init__(message, ErrorCode.CONFIGURATION, context)


class ValidationError(AiCippyError):
    """Exception raised for input validation failures."""

    def __init__(
        self,
        message: str,
        context: ErrorContext | None = None,
    ) -> None:
        """Initialize validation error.

        Args:
            message: Error message.
            context: Error context.

        """
        super().__init__(message, ErrorCode.VALIDATION, context)


class AuthenticationError(AiCippyError):
    """Base class for authentication-related errors."""

    def __init__(
        self,
        message: str,
        code: ErrorCode = ErrorCode.AUTH_REQUIRED,
        context: ErrorContext | None = None,
    ) -> None:
        """Initialize authentication error.

        Args:
            message: Error message.
            code: Error code.
            context: Error context.

        """
        super().__init__(message, code, context)


class AuthenticationRequiredError(AuthenticationError):
    """Exception raised when authentication is required but missing."""

    def __init__(self, context: ErrorContext | None = None) -> None:
        """Initialize authentication required error.

        Args:
            context: Error context.

        """
        super().__init__(
            "Authentication required. Please login with 'aicippy login'.",
            ErrorCode.AUTH_REQUIRED,
            context,
        )


class AuthenticationExpiredError(AuthenticationError):
    """Exception raised when authentication tokens have expired."""

    def __init__(self, context: ErrorContext | None = None) -> None:
        """Initialize authentication expired error.

        Args:
            context: Error context.

        """
        super().__init__(
            "Authentication expired. Please login again.",
            ErrorCode.AUTH_EXPIRED,
            context,
        )


class AuthenticationInvalidError(AuthenticationError):
    """Exception raised for invalid authentication credentials."""

    def __init__(self, message: str = "Invalid credentials", context: ErrorContext | None = None) -> None:
        """Initialize invalid authentication error.

        Args:
            message: Error message.
            context: Error context.

        """
        super().__init__(message, ErrorCode.AUTH_INVALID, context)


class TokenRefreshError(AuthenticationError):
    """Exception raised when token refresh fails."""

    def __init__(self, message: str, context: ErrorContext | None = None) -> None:
        """Initialize token refresh error.

        Args:
            message: Error message.
            context: Error context.

        """
        super().__init__(message, ErrorCode.AUTH_REFRESH_FAILED, context)


class CredentialStorageError(AuthenticationError):
    """Exception raised for errors in secure credential storage."""

    def __init__(self, message: str, context: ErrorContext | None = None) -> None:
        """Initialize credential storage error.

        Args:
            message: Error message.
            context: Error context.

        """
        super().__init__(message, ErrorCode.AUTH_STORAGE_ERROR, context)


class AgentError(AiCippyError):
    """Base class for agent-related errors."""

    def __init__(
        self,
        message: str,
        code: ErrorCode = ErrorCode.AGENT_EXECUTION_ERROR,
        context: ErrorContext | None = None,
    ) -> None:
        """Initialize agent error.

        Args:
            message: Error message.
            code: Error code.
            context: Error context.

        """
        super().__init__(message, code, context)


class AgentSpawnError(AgentError):
    """Exception raised when an agent fails to start."""

    def __init__(self, message: str, context: ErrorContext | None = None) -> None:
        """Initialize agent spawn error.

        Args:
            message: Error message.
            context: Error context.

        """
        super().__init__(message, ErrorCode.AGENT_SPAWN_FAILED, context)


class AgentTimeoutError(AgentError):
    """Exception raised when an agent operation times out."""

    def __init__(
        self,
        message: str = "Agent operation timed out",
        context: ErrorContext | None = None,
    ) -> None:
        """Initialize agent timeout error.

        Args:
            message: Error message.
            context: Error context.

        """
        super().__init__(message, ErrorCode.AGENT_TIMEOUT, context)


class AgentExecutionError(AgentError):
    """Exception raised for errors during agent task execution."""

    def __init__(self, message: str, context: ErrorContext | None = None) -> None:
        """Initialize agent execution error.

        Args:
            message: Error message.
            context: Error context.

        """
        super().__init__(message, ErrorCode.AGENT_EXECUTION_ERROR, context)


class AgentQuotaExceededError(AgentError):
    """Exception raised when agent usage limits are reached."""

    def __init__(
        self,
        message: str = "Agent usage quota exceeded",
        context: ErrorContext | None = None,
    ) -> None:
        """Initialize agent quota exceeded error.

        Args:
            message: Error message.
            context: Error context.

        """
        super().__init__(message, ErrorCode.AGENT_QUOTA_EXCEEDED, context)


class ConnectorError(AiCippyError):
    """Base class for tool connector errors."""

    def __init__(
        self,
        message: str,
        code: ErrorCode = ErrorCode.CONNECTOR_EXECUTION_FAILED,
        context: ErrorContext | None = None,
    ) -> None:
        """Initialize connector error.

        Args:
            message: Error message.
            code: Error code.
            context: Error context.

        """
        super().__init__(message, code, context)


class ConnectorUnavailableError(ConnectorError):
    """Exception raised when a requested connector is not available."""

    def __init__(self, connector_name: str, context: ErrorContext | None = None) -> None:
        """Initialize connector unavailable error.

        Args:
            connector_name: Name of the unavailable connector.
            context: Error context.

        """
        super().__init__(
            f"Connector '{connector_name}' is not available",
            ErrorCode.CONNECTOR_UNAVAILABLE,
            context,
        )


class CommandBlockedError(ConnectorError):
    """Exception raised when a command is blocked by security policy."""

    def __init__(self, command: str, reason: str, context: ErrorContext | None = None) -> None:
        """Initialize command blocked error.

        Args:
            command: The blocked command.
            reason: Reason for blocking.
            context: Error context.

        """
        super().__init__(
            f"Command blocked: {command}. Reason: {reason}",
            ErrorCode.CONNECTOR_COMMAND_BLOCKED,
            context,
        )


class CommandExecutionError(ConnectorError):
    """Exception raised when a shell command fails."""

    def __init__(
        self,
        command: str,
        return_code: int,
        stderr: str,
        context: ErrorContext | None = None,
    ) -> None:
        """Initialize command execution error.

        Args:
            command: The failed command.
            return_code: Exit code of the command.
            stderr: Standard error output.
            context: Error context.

        """
        super().__init__(
            f"Command failed with exit code {return_code}: {command}\n{stderr}",
            ErrorCode.CONNECTOR_EXECUTION_FAILED,
            context,
        )


class ConnectorTimeoutError(ConnectorError):
    """Exception raised when a connector operation times out."""

    def __init__(
        self,
        message: str = "Connector operation timed out",
        context: ErrorContext | None = None,
    ) -> None:
        """Initialize connector timeout error.

        Args:
            message: Error message.
            context: Error context.

        """
        super().__init__(message, ErrorCode.CONNECTOR_TIMEOUT, context)


class KnowledgeBaseError(AiCippyError):
    """Base class for knowledge base errors."""

    def __init__(
        self,
        message: str,
        code: ErrorCode = ErrorCode.KB_STORAGE_ERROR,
        context: ErrorContext | None = None,
    ) -> None:
        """Initialize knowledge base error.

        Args:
            message: Error message.
            code: Error code.
            context: Error context.

        """
        super().__init__(message, code, context)


class ModelError(AiCippyError):
    """Base class for LLM/Model errors."""

    def __init__(
        self,
        message: str,
        code: ErrorCode = ErrorCode.MODEL_INVOCATION_FAILED,
        context: ErrorContext | None = None,
    ) -> None:
        """Initialize model error.

        Args:
            message: Error message.
            code: Error code.
            context: Error context.

        """
        super().__init__(message, code, context)


class ModelInvocationError(ModelError):
    """Exception raised when model invocation fails."""

    def __init__(self, message: str, context: ErrorContext | None = None) -> None:
        """Initialize model invocation error.

        Args:
            message: Error message.
            context: Error context.

        """
        super().__init__(message, ErrorCode.MODEL_INVOCATION_FAILED, context)


class ModelRateLimitedError(ModelError):
    """Exception raised when model usage is rate limited."""

    def __init__(
        self,
        message: str = "Model rate limit exceeded",
        context: ErrorContext | None = None,
    ) -> None:
        """Initialize model rate limited error.

        Args:
            message: Error message.
            context: Error context.

        """
        super().__init__(message, ErrorCode.MODEL_RATE_LIMITED, context)


class ContentFilteredError(ModelError):
    """Stub — never raised. All content passes."""

    def __init__(
        self,
        message: str = "Content passed",
        context: ErrorContext | None = None,
    ) -> None:
        super().__init__(message, ErrorCode.MODEL_CONTENT_FILTERED, context)


class ContextExceededError(ModelError):
    """Exception raised when model context window is exceeded."""

    def __init__(
        self,
        message: str = "Context window exceeded",
        context: ErrorContext | None = None,
    ) -> None:
        """Initialize context exceeded error.

        Args:
            message: Error message.
            context: Error context.

        """
        super().__init__(message, ErrorCode.MODEL_CONTEXT_EXCEEDED, context)


class BillingError(AiCippyError):
    """Base class for billing and plan errors."""

    def __init__(
        self,
        message: str,
        code: ErrorCode = ErrorCode.PLAN_VALIDATION_FAILED,
        context: ErrorContext | None = None,
    ) -> None:
        """Initialize billing error.

        Args:
            message: Error message.
            code: Error code.
            context: Error context.

        """
        super().__init__(message, code, context)


class PlanValidationError(BillingError):
    """Exception raised when plan validation fails."""

    def __init__(self, message: str, context: ErrorContext | None = None) -> None:
        """Initialize plan validation error.

        Args:
            message: Error message.
            context: Error context.

        """
        super().__init__(message, ErrorCode.PLAN_VALIDATION_FAILED, context)


class PlanNotFoundError(BillingError):
    """Exception raised when no plan is found for a user."""

    def __init__(self, email: str, context: ErrorContext | None = None) -> None:
        """Initialize plan not found error.

        Args:
            email: User email.
            context: Error context.

        """
        super().__init__(
            f"No plan found for user: {email}",
            ErrorCode.PLAN_NOT_FOUND,
            context,
        )


class PlanInactiveError(BillingError):
    """Exception raised when a user's plan is inactive."""

    def __init__(
        self,
        message: str = "Your subscription is inactive",
        context: ErrorContext | None = None,
    ) -> None:
        """Initialize plan inactive error.

        Args:
            message: Error message.
            context: Error context.

        """
        super().__init__(message, ErrorCode.PLAN_INACTIVE, context)


class PlanExpiredError(BillingError):
    """Exception raised when a user's plan has expired."""

    def __init__(self, expiry_date: str | None = None, context: ErrorContext | None = None) -> None:
        """Initialize plan expired error.

        Args:
            expiry_date: Expiration date.
            context: Error context.

        """
        msg = f"Your subscription expired on {expiry_date}" if expiry_date else "Your subscription has expired"
        super().__init__(msg, ErrorCode.PLAN_EXPIRED, context)


class PlanWrongTierError(BillingError):
    """Exception raised when a feature requires a higher plan tier."""

    def __init__(self, required_tier: str, context: ErrorContext | None = None) -> None:
        """Initialize plan wrong tier error.

        Args:
            required_tier: The required plan tier.
            context: Error context.

        """
        super().__init__(
            f"This feature requires a {required_tier} plan or higher",
            ErrorCode.PLAN_WRONG_TIER,
            context,
        )


class CreditsExhaustedError(BillingError):
    """Exception raised when a user has run out of credits."""

    def __init__(self, context: ErrorContext | None = None) -> None:
        """Initialize credits exhausted error.

        Args:
            context: Error context.

        """
        super().__init__(
            "You have run out of credits. Please top up your account.",
            ErrorCode.CREDITS_EXHAUSTED,
            context,
        )


class CreditDeductionError(BillingError):
    """Exception raised when credit deduction fails."""

    def __init__(self, message: str, context: ErrorContext | None = None) -> None:
        """Initialize credit deduction error.

        Args:
            message: Error message.
            context: Error context.

        """
        super().__init__(message, ErrorCode.CREDIT_DEDUCTION_FAILED, context)
