"""Utilities for easy adoption and incremental migration to structured error handling"""

# AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/utils/helpers.ipynb.

# %% auto 0
__all__ = ['error_boundary', 'with_error_handling', 'wrap_exception', 'chain_error']

# %% ../../nbs/utils/helpers.ipynb 3
from typing import Optional, Type, Callable, Any, Dict
from contextlib import contextmanager
from functools import wraps

from ..core.base import BaseError, ErrorContext, ErrorSeverity
from cjm_error_handling.core.errors import (
    ValidationError,
    ConfigurationError,
    ResourceError,
    PluginError,
    WorkerError
)

# %% ../../nbs/utils/helpers.ipynb 6
@contextmanager
def error_boundary(
    error_type: Type[BaseError] = BaseError,  # Error type to raise
    message: Optional[str] = None,  # User-facing message (uses original if None)
    context: Optional[ErrorContext] = None,  # Error context
    operation: Optional[str] = None,  # Operation name (added to context)
    job_id: Optional[str] = None,  # Job ID (added to context)
    plugin_id: Optional[str] = None,  # Plugin ID (added to context)
    worker_pid: Optional[int] = None,  # Worker PID (added to context)
    severity: ErrorSeverity = ErrorSeverity.ERROR,  # Error severity
    is_retryable: Optional[bool] = None,  # Override retryable flag
    catch: tuple = (Exception,),  # Exception types to catch
    **extra_context  # Additional context fields
):
    """
    Context manager that catches exceptions and wraps them in structured errors.
    
    This allows you to wrap existing code with minimal changes and get
    automatic error enrichment with context.
    
    Example:
        ```python
        with error_boundary(
            error_type=PluginError,
            operation="load_plugin",
            plugin_id="whisper_large"
        ):
            # Existing code that might fail
            plugin.initialize(config)
        ```
    """
    try:
        yield
    except catch as e:
        # Build context
        if context is None:
            ctx_kwargs = {}
            if operation:
                ctx_kwargs['operation'] = operation
            if job_id:
                ctx_kwargs['job_id'] = job_id
            if plugin_id:
                ctx_kwargs['plugin_id'] = plugin_id
            if worker_pid:
                ctx_kwargs['worker_pid'] = worker_pid
            if extra_context:
                ctx_kwargs['extra'] = extra_context
            context = ErrorContext(**ctx_kwargs)
        
        # Use provided message or create from exception
        error_message = message or str(e)
        
        # Use error_type's default retryable if not specified
        if is_retryable is None:
            # Create temporary instance to get default
            temp = error_type.__new__(error_type)
            BaseError.__init__(temp, "")
            is_retryable = temp.is_retryable
        
        # Create and raise wrapped error
        raise error_type(
            message=error_message,
            debug_info=f"Original error: {type(e).__name__}: {str(e)}",
            context=context,
            severity=severity,
            is_retryable=is_retryable,
            cause=e
        ) from e

# %% ../../nbs/utils/helpers.ipynb 11
def with_error_handling(
    error_type: Type[BaseError] = BaseError,  # Error type to raise
    message: Optional[str] = None,  # User-facing message
    operation: Optional[str] = None,  # Operation name (uses function name if None)
    severity: ErrorSeverity = ErrorSeverity.ERROR,  # Error severity
    is_retryable: Optional[bool] = None,  # Override retryable flag
    catch: tuple = (Exception,),  # Exception types to catch
    context_from_args: Optional[Dict[str, str]] = None  # Map arg names to context fields
):
    """
    Decorator that wraps function errors with structured error handling.
    
    This allows you to add error handling to functions without modifying
    their implementation.
    
    Args:
        error_type: Error class to raise
        message: User-facing error message
        operation: Operation name (defaults to function name)
        severity: Error severity level
        is_retryable: Whether error is retryable
        catch: Tuple of exception types to catch
        context_from_args: Map function arg names to context fields.
            Example: {'job_id': 'job_id', 'plugin': 'plugin_id'}
    
    Example:
        ```python
        @with_error_handling(
            error_type=WorkerError,
            operation="execute_job",
            context_from_args={'job_id': 'job_id'}
        )
        def run_job(job_id, data):
            # Function implementation
            pass
        ```
    """
    def decorator(func: Callable) -> Callable:
        @wraps(func)
        def wrapper(*args, **kwargs):
            # Determine operation name
            op_name = operation or func.__name__
            
            # Extract context from function arguments
            context_kwargs = {'operation': op_name}
            
            if context_from_args:
                # Get function signature
                import inspect
                sig = inspect.signature(func)
                bound_args = sig.bind(*args, **kwargs)
                bound_args.apply_defaults()
                
                # Map arguments to context fields
                for arg_name, context_field in context_from_args.items():
                    if arg_name in bound_args.arguments:
                        context_kwargs[context_field] = bound_args.arguments[arg_name]
            
            # Use error_boundary to wrap execution
            with error_boundary(
                error_type=error_type,
                message=message,
                context=ErrorContext(**context_kwargs),
                severity=severity,
                is_retryable=is_retryable,
                catch=catch
            ):
                return func(*args, **kwargs)
        
        return wrapper
    return decorator

# %% ../../nbs/utils/helpers.ipynb 16
def wrap_exception(
    exception: Exception,  # Original exception to wrap
    error_type: Type[BaseError] = BaseError,  # Error type to create
    message: Optional[str] = None,  # User-facing message (uses exception if None)
    context: Optional[ErrorContext] = None,  # Error context
    severity: ErrorSeverity = ErrorSeverity.ERROR,  # Error severity
    is_retryable: Optional[bool] = None,  # Override retryable flag
    **context_kwargs  # Additional context fields
) -> BaseError:  # Wrapped structured error
    """
    Wrap an existing exception in a structured error type.
    
    This is useful when you catch an exception and want to convert it
    to a structured error before re-raising or returning.
    
    Example:
        ```python
        try:
            load_model()
        except Exception as e:
            error = wrap_exception(
                e,
                error_type=PluginError,
                message="Failed to load model",
                plugin_id="whisper_large"
            )
            raise error
        ```
    """
    # Build context if not provided
    if context is None and context_kwargs:
        context = ErrorContext(**context_kwargs)
    elif context is None:
        context = ErrorContext()
    
    # Use exception message if no message provided
    error_message = message or str(exception)
    
    # Use error_type's default retryable if not specified
    if is_retryable is None:
        temp = error_type.__new__(error_type)
        BaseError.__init__(temp, "")
        is_retryable = temp.is_retryable
    
    # Create wrapped error
    return error_type(
        message=error_message,
        debug_info=f"Original: {type(exception).__name__}: {str(exception)}",
        context=context,
        severity=severity,
        is_retryable=is_retryable,
        cause=exception
    )

# %% ../../nbs/utils/helpers.ipynb 20
def chain_error(
    base_error: BaseError,  # Original structured error
    new_message: str,  # New user-facing message
    error_type: Optional[Type[BaseError]] = None,  # New error type (uses base type if None)
    additional_context: Optional[Dict[str, Any]] = None,  # Additional context to add
    operation: Optional[str] = None,  # Update operation name
    severity: Optional[ErrorSeverity] = None  # Override severity
) -> BaseError:  # New error with chained context
    """
    Chain a structured error with additional context.
    
    This is useful when an error propagates up through layers and you want
    to add context at each layer.
    
    Example:
        ```python
        try:
            load_plugin_config()
        except ConfigurationError as e:
            # Add higher-level context
            raise chain_error(
                e,
                new_message="Failed to initialize plugin",
                error_type=PluginError,
                operation="plugin_init"
            )
        ```
    """
    # Use same error type if not specified
    if error_type is None:
        error_type = type(base_error)
    
    # Copy and enrich context
    new_context = ErrorContext(
        job_id=base_error.context.job_id,
        plugin_id=base_error.context.plugin_id,
        worker_pid=base_error.context.worker_pid,
        session_id=base_error.context.session_id,
        user_id=base_error.context.user_id,
        operation=operation or base_error.context.operation,
        timestamp=base_error.context.timestamp,
        extra={**base_error.context.extra, **(additional_context or {})}
    )
    
    # Use base severity if not overridden
    new_severity = severity or base_error.severity
    
    # Create new error with chained cause
    return error_type(
        message=new_message,
        debug_info=base_error.get_debug_message(),
        context=new_context,
        severity=new_severity,
        is_retryable=base_error.is_retryable,
        cause=base_error
    )
