"""
Error handling utilities for pygeai-orchestration.

This module provides centralized error handling functionality following
the PyGEAI pattern. The ErrorHandler class offers consistent error detection,
extraction, and processing across the orchestration package.
"""

from typing import Any, Dict, Optional
import logging

from .exceptions import OrchestrationError


logger = logging.getLogger("pygeai-orchestration")


class ErrorHandler:
    """
    Centralized error handler for orchestration operations.

    This class provides static methods for detecting, extracting, and handling
    errors in a consistent manner across the package. It follows the PyGEAI
    ErrorHandler pattern for uniform error processing.

    The ErrorHandler can process various response formats and extract error
    information, making it easier to handle errors from different sources
    (agents, tools, external APIs, etc.).

    Example:
        >>> response = {"error": "Agent timeout", "code": 500}
        >>> if ErrorHandler.has_errors(response):
        ...     error = ErrorHandler.extract_error(response)
        ...     ErrorHandler.handle_error(error, context={"operation": "generate"})
    """

    @staticmethod
    def has_errors(response: Any) -> bool:
        """
        Check if a response contains error information.

        Detects errors in various response formats including:
        - Dictionary with 'error' or 'errors' key
        - Dictionary with 'success': False
        - Dictionary with 'status' indicating error
        - Exception objects

        :param response: Any - The response to check for errors.
        :return: bool - True if errors are detected, False otherwise.

        Example:
            >>> ErrorHandler.has_errors({"error": "Failed"})
            True
            >>> ErrorHandler.has_errors({"success": True})
            False
        """
        if response is None:
            return False

        if isinstance(response, Exception):
            return True

        if isinstance(response, dict):
            # Check for explicit error keys
            if "error" in response and response["error"]:
                return True
            if "errors" in response and response["errors"]:
                return True

            # Check for success flag
            if "success" in response and response["success"] is False:
                return True

            # Check for error status codes
            if "status" in response:
                status = response["status"]
                if isinstance(status, int) and status >= 400:
                    return True
                if isinstance(status, str) and status.lower() in ["error", "failed", "failure"]:
                    return True

            # Check for code indicating error
            if "code" in response:
                code = response["code"]
                if isinstance(code, int) and code >= 400:
                    return True

        return False

    @staticmethod
    def extract_error(response: Any) -> str:
        """
        Extract error message from a response.

        Attempts to extract a meaningful error message from various response
        formats. Returns a descriptive error message or a generic message if
        the specific error cannot be determined.

        :param response: Any - The response containing error information.
        :return: str - The extracted error message.

        Example:
            >>> response = {"error": "Connection timeout", "code": 504}
            >>> ErrorHandler.extract_error(response)
            'Connection timeout'
        """
        if response is None:
            return "Unknown error: None response"

        if isinstance(response, Exception):
            return str(response)

        if isinstance(response, str):
            return response

        if isinstance(response, dict):
            # Try to extract from common error keys
            if "error" in response:
                error = response["error"]
                if isinstance(error, dict):
                    # Handle nested error object
                    return error.get("message", str(error))
                return str(error)

            if "errors" in response:
                errors = response["errors"]
                if isinstance(errors, list) and errors:
                    # Return first error or join multiple
                    if len(errors) == 1:
                        return str(errors[0])
                    return "; ".join(str(e) for e in errors)
                return str(errors)

            if "message" in response:
                return str(response["message"])

            # Try to construct message from status/code
            if "status" in response or "code" in response:
                parts = []
                if "status" in response:
                    parts.append(f"Status: {response['status']}")
                if "code" in response:
                    parts.append(f"Code: {response['code']}")
                if parts:
                    return " | ".join(parts)

            # Last resort: stringify the entire response
            return f"Error in response: {response}"

        return f"Unknown error: {type(response).__name__}"

    @staticmethod
    def handle_error(
        error: Any, context: Optional[Dict[str, Any]] = None, log_level: int = logging.ERROR
    ) -> str:
        """
        Handle an error with logging and context.

        Processes an error by extracting the error message, logging it with
        context, and returning a formatted error message. This provides a
        centralized point for error processing across the package.

        :param error: Any - The error to handle (Exception, dict, or string).
        :param context: Optional[Dict[str, Any]] - Additional context for the error.
        :param log_level: int - Logging level to use. Defaults to logging.ERROR.
        :return: str - The formatted error message.

        Example:
            >>> error = {"error": "Pattern failed", "iteration": 5}
            >>> msg = ErrorHandler.handle_error(
            ...     error,
            ...     context={"pattern": "reflection", "task": "summarize"},
            ...     log_level=logging.WARNING
            ... )
        """
        # Extract error message
        error_message = ErrorHandler.extract_error(error)

        # Build context string
        context_str = ""
        if context:
            context_parts = [f"{k}={v}" for k, v in context.items()]
            context_str = f" [{', '.join(context_parts)}]"

        # Format full message
        full_message = f"{error_message}{context_str}"

        # Log the error
        logger.log(log_level, full_message)

        return full_message

    @staticmethod
    def wrap_exception(
        error: Exception,
        exception_class: type = OrchestrationError,
        message_prefix: Optional[str] = None,
        **kwargs,
    ) -> OrchestrationError:
        """
        Wrap an exception in a custom orchestration exception.

        Converts generic exceptions into specific orchestration exceptions
        while preserving the original error message and adding context.

        :param error: Exception - The original exception to wrap.
        :param exception_class: type - The exception class to wrap with. Defaults to OrchestrationError.
        :param message_prefix: Optional[str] - Prefix to add to error message.
        :param kwargs: Additional keyword arguments for the exception class.
        :return: OrchestrationError - The wrapped exception.

        Example:
            >>> try:
            ...     risky_operation()
            ... except Exception as e:
            ...     raise ErrorHandler.wrap_exception(
            ...         e,
            ...         PatternExecutionError,
            ...         message_prefix="Pattern execution failed",
            ...         pattern_name="reflection",
            ...         iteration=3
            ...     )
        """
        original_message = str(error)

        if message_prefix:
            message = f"{message_prefix}: {original_message}"
        else:
            message = original_message

        # Create the wrapped exception
        wrapped = exception_class(message, **kwargs)

        # Preserve the original exception as context
        wrapped.__cause__ = error

        return wrapped

    @staticmethod
    def is_recoverable(error: Any) -> bool:
        """
        Determine if an error is potentially recoverable.

        Analyzes an error to determine if the operation could potentially
        succeed if retried. This helps in implementing retry logic.

        :param error: Any - The error to analyze.
        :return: bool - True if the error might be recoverable, False otherwise.

        Example:
            >>> error = {"error": "Timeout", "code": 504}
            >>> if ErrorHandler.is_recoverable(error):
            ...     retry_operation()
        """
        if isinstance(error, dict):
            # Check status/code for recoverable errors
            code = error.get("code", 0)
            if isinstance(code, int):
                # Timeout and rate limit errors are often recoverable
                if code in [408, 429, 503, 504]:
                    return True

            # Check for specific error types
            error_msg = str(ErrorHandler.extract_error(error)).lower()
            recoverable_keywords = [
                "timeout",
                "rate limit",
                "too many requests",
                "service unavailable",
                "connection",
                "temporary",
            ]

            return any(keyword in error_msg for keyword in recoverable_keywords)

        if isinstance(error, Exception):
            error_str = str(error).lower()
            recoverable_types = [
                "timeout",
                "connection",
                "temporary",
            ]
            return any(err_type in error_str for err_type in recoverable_types)

        return False

    @staticmethod
    def format_error_response(error: Any, include_details: bool = True) -> Dict[str, Any]:
        """
        Format an error as a standardized response dictionary.

        Converts various error formats into a consistent dictionary structure
        that can be used across the package.

        :param error: Any - The error to format.
        :param include_details: bool - Whether to include detailed error information.
        :return: Dict[str, Any] - Standardized error response.

        Example:
            >>> error = ValueError("Invalid input")
            >>> response = ErrorHandler.format_error_response(error)
            >>> print(response)
            {'success': False, 'error': 'Invalid input', 'error_type': 'ValueError'}
        """
        response = {
            "success": False,
            "error": ErrorHandler.extract_error(error),
        }

        if include_details:
            if isinstance(error, Exception):
                response["error_type"] = type(error).__name__
                if hasattr(error, "__cause__") and error.__cause__:
                    response["original_error"] = str(error.__cause__)

            if isinstance(error, dict):
                # Include additional fields from error dict
                for key in ["code", "status", "details"]:
                    if key in error:
                        response[key] = error[key]

        return response


class RetryHandler:
    """
    Handler for implementing retry logic with error handling.

    This class provides utilities for retrying operations that may fail
    transiently, with configurable retry strategies and error handling.

    Example:
        >>> handler = RetryHandler(max_retries=3, delay=1.0)
        >>> result = await handler.execute_with_retry(risky_operation, arg1, arg2)
    """

    def __init__(
        self,
        max_retries: int = 3,
        delay: float = 1.0,
        backoff_factor: float = 2.0,
        recoverable_only: bool = True,
    ):
        """
        Initialize the RetryHandler.

        :param max_retries: int - Maximum number of retry attempts. Defaults to 3.
        :param delay: float - Initial delay between retries in seconds. Defaults to 1.0.
        :param backoff_factor: float - Multiplier for delay on each retry. Defaults to 2.0.
        :param recoverable_only: bool - Only retry recoverable errors. Defaults to True.
        """
        self.max_retries = max_retries
        self.delay = delay
        self.backoff_factor = backoff_factor
        self.recoverable_only = recoverable_only

    def should_retry(self, error: Any, attempt: int) -> bool:
        """
        Determine if an operation should be retried.

        :param error: Any - The error that occurred.
        :param attempt: int - Current attempt number (0-indexed).
        :return: bool - True if should retry, False otherwise.
        """
        if attempt >= self.max_retries:
            return False

        if self.recoverable_only:
            return ErrorHandler.is_recoverable(error)

        return True

    def get_delay(self, attempt: int) -> float:
        """
        Calculate delay before next retry.

        :param attempt: int - Current attempt number (0-indexed).
        :return: float - Delay in seconds.
        """
        return self.delay * (self.backoff_factor**attempt)
