"""
MnemoCore Domain-Specific Exceptions
=====================================

This module defines a hierarchy of exceptions for consistent error handling
across the MnemoCore system.

Exception Hierarchy:
    MnemoCoreError (base)
    ├── RecoverableError (transient, retry possible)
    │   ├── StorageConnectionError
    │   ├── StorageTimeoutError
    │   └── CircuitOpenError
    ├── IrrecoverableError (permanent, requires intervention)
    │   ├── ConfigurationError
    │   ├── DataCorruptionError
    │   ├── ValidationError
    │   ├── NotFoundError
    │   └── UnsupportedOperationError
    └── Domain Errors (mixed recoverability)
        ├── StorageError
        ├── VectorError
        │   ├── DimensionMismatchError
        │   └── VectorOperationError
        └── MemoryOperationError

Usage Guidelines:
    - Return None for "not found" scenarios (expected case, not an error)
    - Raise exceptions for actual errors (connection failures, validation, corruption)
    - Always include context in error messages
    - Use error_code for API responses
"""

from typing import Optional, Any
from enum import Enum
import os


class ErrorCategory(Enum):
    """Categories for error classification."""
    STORAGE = "STORAGE"
    VECTOR = "VECTOR"
    CONFIG = "CONFIG"
    VALIDATION = "VALIDATION"
    MEMORY = "MEMORY"
    AGENT = "AGENT"
    PROVIDER = "PROVIDER"
    SYSTEM = "SYSTEM"


class MnemoCoreError(Exception):
    """
    Base exception for all MnemoCore errors.

    Attributes:
        message: Human-readable error message
        error_code: Machine-readable error code for API responses
        context: Additional context about the error
        recoverable: Whether the error is potentially recoverable
    """

    error_code: str = "MNEMO_CORE_ERROR"
    recoverable: bool = True
    category: ErrorCategory = ErrorCategory.SYSTEM

    def __init__(
        self,
        message: str,
        context: Optional[dict] = None,
        error_code: Optional[str] = None,
        recoverable: Optional[bool] = None
    ):
        super().__init__(message)
        self.message = message
        self.context = context or {}
        if error_code is not None:
            self.error_code = error_code
        if recoverable is not None:
            self.recoverable = recoverable

    def __str__(self) -> str:
        if self.context:
            return f"{self.message} | context={self.context}"
        return self.message

    def to_dict(self, include_traceback: bool = False) -> dict:
        """
        Convert exception to dictionary for JSON response.

        Args:
            include_traceback: Whether to include stack trace (only in DEBUG mode)
        """
        result = {
            "error": self.message,
            "code": self.error_code,
            "recoverable": self.recoverable,
        }

        if include_traceback:
            import traceback
            result["traceback"] = traceback.format_exc()

        if self.context:
            result["context"] = self.context

        return result


# =============================================================================
# Base Categories: Recoverable vs Irrecoverable
# =============================================================================

class RecoverableError(MnemoCoreError):
    """
    Base class for recoverable errors.

    These are transient errors that may succeed on retry:
    - Connection failures
    - Timeouts
    - Circuit breaker open
    - Rate limiting
    """
    recoverable = True


class IrrecoverableError(MnemoCoreError):
    """
    Base class for irrecoverable errors.

    These are permanent errors that require intervention:
    - Invalid configuration
    - Data corruption
    - Validation failures
    - Resource not found
    """
    recoverable = False


# =============================================================================
# Storage Errors
# =============================================================================

class StorageError(MnemoCoreError):
    """Base exception for storage-related errors."""
    error_code = "STORAGE_ERROR"
    category = ErrorCategory.STORAGE


class StorageConnectionError(RecoverableError, StorageError):
    """Raised when connection to storage backend fails."""
    error_code = "STORAGE_CONNECTION_ERROR"

    def __init__(self, backend: str, message: str = "Connection failed", context: Optional[dict] = None):
        ctx = {"backend": backend}
        if context:
            ctx.update(context)
        super().__init__(f"[{backend}] {message}", ctx)
        self.backend = backend


class StorageTimeoutError(RecoverableError, StorageError):
    """Raised when a storage operation times out."""
    error_code = "STORAGE_TIMEOUT_ERROR"

    def __init__(self, backend: str, operation: str, timeout_ms: Optional[int] = None, context: Optional[dict] = None):
        msg = f"[{backend}] Operation '{operation}' timed out"
        ctx = {"backend": backend, "operation": operation}
        if timeout_ms is not None:
            ctx["timeout_ms"] = timeout_ms
        if context:
            ctx.update(context)
        super().__init__(msg, ctx)
        self.backend = backend
        self.operation = operation


class DataCorruptionError(IrrecoverableError, StorageError):
    """Raised when stored data is corrupt or cannot be deserialized."""
    error_code = "DATA_CORRUPTION_ERROR"

    def __init__(self, resource_id: str, reason: str = "Data corruption detected", context: Optional[dict] = None):
        ctx = {"resource_id": resource_id}
        if context:
            ctx.update(context)
        super().__init__(f"{reason} for resource '{resource_id}'", ctx)
        self.resource_id = resource_id


# =============================================================================
# Vector Errors
# =============================================================================

class VectorError(MnemoCoreError):
    """Base exception for vector/hyperdimensional operations."""
    error_code = "VECTOR_ERROR"
    category = ErrorCategory.VECTOR


class DimensionMismatchError(IrrecoverableError, VectorError):
    """Raised when vector dimensions do not match."""
    error_code = "DIMENSION_MISMATCH_ERROR"

    def __init__(self, expected: int, actual: int, operation: str = "operation", context: Optional[dict] = None):
        ctx = {"expected": expected, "actual": actual, "operation": operation}
        if context:
            ctx.update(context)
        super().__init__(
            f"Dimension mismatch in {operation}: expected {expected}, got {actual}",
            ctx
        )
        self.expected = expected
        self.actual = actual
        self.operation = operation


class VectorOperationError(IrrecoverableError, VectorError):
    """Raised when a vector operation fails."""
    error_code = "VECTOR_OPERATION_ERROR"

    def __init__(self, operation: str, reason: str, context: Optional[dict] = None):
        ctx = {"operation": operation}
        if context:
            ctx.update(context)
        super().__init__(f"Vector operation '{operation}' failed: {reason}", ctx)
        self.operation = operation


# =============================================================================
# Configuration Errors
# =============================================================================

class ConfigurationError(IrrecoverableError):
    """Raised when configuration is invalid or missing."""
    error_code = "CONFIGURATION_ERROR"
    category = ErrorCategory.CONFIG

    def __init__(self, config_key: str, reason: str, context: Optional[dict] = None):
        ctx = {"config_key": config_key}
        if context:
            ctx.update(context)
        super().__init__(f"Configuration error for '{config_key}': {reason}", ctx)
        self.config_key = config_key


# =============================================================================
# Circuit Breaker Errors
# =============================================================================

class CircuitOpenError(RecoverableError):
    """Raised when a circuit breaker is open and blocking requests."""
    error_code = "CIRCUIT_OPEN_ERROR"
    category = ErrorCategory.SYSTEM

    def __init__(self, breaker_name: str, failures: int, context: Optional[dict] = None):
        ctx = {"breaker_name": breaker_name, "failures": failures}
        if context:
            ctx.update(context)
        super().__init__(
            f"Circuit breaker '{breaker_name}' is OPEN after {failures} failures",
            ctx
        )
        self.breaker_name = breaker_name
        self.failures = failures


# =============================================================================
# Memory Operation Errors
# =============================================================================

class MemoryOperationError(MnemoCoreError):
    """Raised when a memory operation (store, retrieve, delete) fails."""
    error_code = "MEMORY_OPERATION_ERROR"
    category = ErrorCategory.MEMORY

    def __init__(self, operation: str, node_id: Optional[str], reason: str, context: Optional[dict] = None):
        ctx = {"operation": operation}
        if node_id:
            ctx["node_id"] = node_id
        if context:
            ctx.update(context)
        super().__init__(f"Memory {operation} failed for '{node_id}': {reason}", ctx)
        self.operation = operation
        self.node_id = node_id


# =============================================================================
# Validation Errors
# =============================================================================

class ValidationError(IrrecoverableError):
    """Raised when input validation fails."""
    error_code = "VALIDATION_ERROR"
    category = ErrorCategory.VALIDATION

    def __init__(self, field: str, reason: str, value: Any = None, context: Optional[dict] = None):
        ctx = {"field": field}
        if value is not None:
            # Truncate large values
            value_str = str(value)
            if len(value_str) > 100:
                value_str = value_str[:100] + "..."
            ctx["value"] = value_str
        if context:
            ctx.update(context)
        super().__init__(f"Validation error for '{field}': {reason}", ctx)
        self.field = field
        self.reason = reason


class MetadataValidationError(ValidationError):
    """Raised when metadata validation fails."""
    error_code = "METADATA_VALIDATION_ERROR"


class AttributeValidationError(ValidationError):
    """Raised when attribute validation fails."""
    error_code = "ATTRIBUTE_VALIDATION_ERROR"


# =============================================================================
# Not Found Errors
# =============================================================================

class NotFoundError(IrrecoverableError):
    """Raised when a requested resource is not found."""
    error_code = "NOT_FOUND_ERROR"
    category = ErrorCategory.SYSTEM

    def __init__(self, resource_type: str, resource_id: str, context: Optional[dict] = None):
        ctx = {"resource_type": resource_type, "resource_id": resource_id}
        if context:
            ctx.update(context)
        super().__init__(f"{resource_type} '{resource_id}' not found", ctx)
        self.resource_type = resource_type
        self.resource_id = resource_id


class AgentNotFoundError(NotFoundError):
    """Raised when an agent is not found."""
    error_code = "AGENT_NOT_FOUND_ERROR"
    category = ErrorCategory.AGENT

    def __init__(self, agent_id: str, context: Optional[dict] = None):
        super().__init__("Agent", agent_id, context)
        self.agent_id = agent_id


class MemoryNotFoundError(NotFoundError):
    """Raised when a memory is not found."""
    error_code = "MEMORY_NOT_FOUND_ERROR"
    category = ErrorCategory.MEMORY

    def __init__(self, memory_id: str, context: Optional[dict] = None):
        super().__init__("Memory", memory_id, context)
        self.memory_id = memory_id


# =============================================================================
# Provider Errors
# =============================================================================

class ProviderError(MnemoCoreError):
    """Base exception for provider-related errors."""
    error_code = "PROVIDER_ERROR"
    category = ErrorCategory.PROVIDER


class UnsupportedProviderError(IrrecoverableError, ProviderError):
    """Raised when an unsupported provider is requested."""
    error_code = "UNSUPPORTED_PROVIDER_ERROR"

    def __init__(self, provider: str, supported_providers: Optional[list] = None, context: Optional[dict] = None):
        ctx = {"provider": provider}
        if supported_providers:
            ctx["supported_providers"] = supported_providers
        if context:
            ctx.update(context)
        msg = f"Unsupported provider: {provider}"
        if supported_providers:
            msg += f". Supported: {', '.join(supported_providers)}"
        super().__init__(msg, ctx)
        self.provider = provider


class UnsupportedTransportError(IrrecoverableError, ValueError):
    """Raised when an unsupported transport is requested."""
    error_code = "UNSUPPORTED_TRANSPORT_ERROR"
    category = ErrorCategory.CONFIG

    def __init__(self, transport: str, supported_transports: Optional[list] = None, context: Optional[dict] = None):
        ctx = {"transport": transport}
        if supported_transports:
            ctx["supported_transports"] = supported_transports
        if context:
            ctx.update(context)
        msg = f"Unsupported transport: {transport}"
        if supported_transports:
            msg += f". Supported: {', '.join(supported_transports)}"
        super().__init__(msg, ctx)
        self.transport = transport


class DependencyMissingError(IrrecoverableError):
    """Raised when a required dependency is missing."""
    error_code = "DEPENDENCY_MISSING_ERROR"
    category = ErrorCategory.SYSTEM

    def __init__(self, dependency: str, message: str = "", context: Optional[dict] = None):
        ctx = {"dependency": dependency}
        if context:
            ctx.update(context)
        msg = f"Missing dependency: {dependency}"
        if message:
            msg += f". {message}"
        super().__init__(msg, ctx)
        self.dependency = dependency


# =============================================================================
# Utility Functions
# =============================================================================

def wrap_storage_exception(backend: str, operation: str, exc: Exception) -> StorageError:
    """
    Wrap a generic exception into an appropriate StorageError.

    Args:
        backend: Name of the storage backend (e.g., 'redis', 'qdrant')
        operation: Name of the operation that failed
        exc: The original exception

    Returns:
        An appropriate StorageError subclass
    """
    exc_name = type(exc).__name__
    exc_msg = str(exc)

    # Timeout detection
    if 'timeout' in exc_msg.lower() or 'Timeout' in exc_name:
        return StorageTimeoutError(backend, operation)

    # Connection error detection
    if any(x in exc_name.lower() for x in ['connection', 'connect', 'network']):
        return StorageConnectionError(backend, exc_msg)

    # Default to generic storage error
    return StorageError(
        f"[{backend}] {operation} failed: {exc_msg}",
        {"backend": backend, "operation": operation, "original_exception": exc_name}
    )


def is_debug_mode() -> bool:
    """Check if debug mode is enabled via environment variable."""
    return os.environ.get("MNEMO_DEBUG", "").lower() in ("true", "1", "yes")


# =============================================================================
# Convenience Exports
# =============================================================================

__all__ = [
    # Base
    "MnemoCoreError",
    "RecoverableError",
    "IrrecoverableError",
    "ErrorCategory",
    # Storage
    "StorageError",
    "StorageConnectionError",
    "StorageTimeoutError",
    "DataCorruptionError",
    # Vector
    "VectorError",
    "DimensionMismatchError",
    "VectorOperationError",
    # Config
    "ConfigurationError",
    # Circuit Breaker
    "CircuitOpenError",
    # Memory
    "MemoryOperationError",
    # Validation
    "ValidationError",
    "MetadataValidationError",
    "AttributeValidationError",
    # Not Found
    "NotFoundError",
    "AgentNotFoundError",
    "MemoryNotFoundError",
    # Provider
    "ProviderError",
    "UnsupportedProviderError",
    "UnsupportedTransportError",
    "DependencyMissingError",
    # Utilities
    "wrap_storage_exception",
    "is_debug_mode",
]
