"""Foundation classes for structured error handling with context propagation and dual messaging"""

# AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/core/base.ipynb.

# %% auto 0
__all__ = ['ErrorSeverity', 'ErrorContext', 'BaseError']

# %% ../../nbs/core/base.ipynb 3
from enum import Enum
from dataclasses import dataclass, field, asdict
from typing import Dict, Any, Optional
from datetime import datetime

# %% ../../nbs/core/base.ipynb 5
class ErrorSeverity(Enum):
    """Error severity levels for categorization and handling."""
    DEBUG = "debug"          # Development/debugging information
    INFO = "info"            # Informational, not really an error
    WARNING = "warning"      # Warning, operation continues
    ERROR = "error"          # Error, operation failed but recoverable
    CRITICAL = "critical"    # Critical error, system/application may be unstable

# %% ../../nbs/core/base.ipynb 7
@dataclass
class ErrorContext:
    """
    Structured context information for errors.
    
    This dataclass provides a standard way to attach contextual metadata
    to errors. Use specific fields for common context (job_id, plugin_id, etc.)
    and the `extra` dict for domain-specific information.
    """
    
    # Common context fields
    job_id: Optional[str] = None              # Job identifier
    plugin_id: Optional[str] = None           # Plugin identifier  
    worker_pid: Optional[int] = None          # Worker process ID
    session_id: Optional[str] = None          # Session identifier
    user_id: Optional[str] = None             # User identifier
    operation: Optional[str] = None           # Operation being performed
    timestamp: str = field(default_factory=lambda: datetime.now().isoformat())  # When error occurred
    
    # Flexible additional context
    extra: Dict[str, Any] = field(default_factory=dict)  # Domain-specific context
    
    def to_dict(self) -> Dict[str, Any]:  # Dictionary representation
        """Convert context to dictionary for serialization."""
        result = asdict(self)
        # Remove None values for cleaner serialization
        return {k: v for k, v in result.items() if v is not None}
    
    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> 'ErrorContext':  # ErrorContext instance
        """Create ErrorContext from dictionary."""
        # Extract known fields
        known_fields = {'job_id', 'plugin_id', 'worker_pid', 'session_id', 
                       'user_id', 'operation', 'timestamp'}
        kwargs = {k: v for k, v in data.items() if k in known_fields}
        
        # Put unknown fields in extra
        extra = {k: v for k, v in data.items() if k not in known_fields and k != 'extra'}
        if 'extra' in data:
            extra.update(data['extra'])
        
        if extra:
            kwargs['extra'] = extra
            
        return cls(**kwargs)

# %% ../../nbs/core/base.ipynb 13
class BaseError(Exception):
    """
    Base exception class with rich context and dual messaging.
    
    This class provides a standardized way to create errors with:
    - User-friendly messages for end users
    - Debug information for developers
    - Structured context for logging and monitoring
    - Serialization support for crossing process boundaries
    
    All custom errors in the library ecosystem should inherit from this.
    """
    
    def __init__(
        self,
        message: str,                              # User-friendly error message
        debug_info: Optional[str] = None,          # Optional developer details
        context: Optional[ErrorContext] = None,    # Structured error context
        severity: ErrorSeverity = ErrorSeverity.ERROR,  # Error severity level
        is_retryable: bool = False,                # Whether error is transient/retryable
        cause: Optional[Exception] = None          # Original exception if chaining
    ):
        """Initialize error with message, context, and metadata."""
        super().__init__(message)
        self.message = message
        self.debug_info = debug_info
        self.context = context or ErrorContext()
        self.severity = severity
        self.is_retryable = is_retryable
        self.cause = cause
        
        # Set the __cause__ for proper exception chaining
        if cause:
            self.__cause__ = cause
    
    def get_user_message(self) -> str:  # User-friendly message
        """Get the user-friendly error message."""
        return self.message
    
    def get_debug_message(self) -> str:  # Debug message with details
        """Get detailed debug information."""
        parts = [self.message]
        
        if self.debug_info:
            parts.append(f"Debug: {self.debug_info}")
        
        if self.cause:
            parts.append(f"Caused by: {type(self.cause).__name__}: {str(self.cause)}")
        
        return " | ".join(parts)
    
    def to_dict(self) -> Dict[str, Any]:  # Dictionary representation
        """
        Serialize error to dictionary for transmission across process boundaries.
        
        Returns a dictionary that can be JSON-serialized and used to reconstruct
        the error on the other side.
        """
        result = {
            'error_type': self.__class__.__name__,
            'message': self.message,
            'severity': self.severity.value,
            'is_retryable': self.is_retryable,
            'context': self.context.to_dict()
        }
        
        if self.debug_info:
            result['debug_info'] = self.debug_info
        
        if self.cause:
            result['cause'] = {
                'type': type(self.cause).__name__,
                'message': str(self.cause)
            }
        
        return result
    
    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> 'BaseError':  # Reconstructed error
        """
        Reconstruct error from dictionary representation.
        
        This is useful when receiving errors from worker processes or
        other boundaries where the original exception can't be passed directly.
        """
        # Reconstruct context
        context = ErrorContext.from_dict(data.get('context', {}))
        
        # Reconstruct severity
        severity_str = data.get('severity', 'error')
        severity = ErrorSeverity(severity_str)
        
        # Note: We can't reconstruct the original cause exception,
        # but we can preserve the information
        cause_info = data.get('cause')
        debug_info = data.get('debug_info', '')
        if cause_info:
            cause_str = f"{cause_info['type']}: {cause_info['message']}"
            if debug_info:
                debug_info = f"{debug_info} | Original cause: {cause_str}"
            else:
                debug_info = f"Original cause: {cause_str}"
        
        return cls(
            message=data['message'],
            debug_info=debug_info,
            context=context,
            severity=severity,
            is_retryable=data.get('is_retryable', False)
        )
    
    def __str__(self) -> str:
        """String representation shows user message."""
        return self.message
    
    def __repr__(self) -> str:
        """Detailed representation for debugging."""
        return f"{self.__class__.__name__}(message='{self.message}', severity={self.severity.value})"
