"""
Custom exceptions for pygeai-orchestration.

This module defines the exception hierarchy for the orchestration package,
providing specific error types for different failure scenarios.

Exception Hierarchy:
- OrchestrationError (base)
  - PatternExecutionError
  - PatternConfigurationError
  - AgentError
  - ToolExecutionError
  - StateError
  - ValidationError
"""

from typing import Any, Dict, Optional


class OrchestrationError(Exception):
    """
    Base class for all pygeai-orchestration exceptions.

    All custom exceptions in this package inherit from this base class,
    allowing for catching all orchestration-related errors with a single
    except clause.

    Example:
        >>> try:
        ...     pattern.execute(task)
        ... except OrchestrationError as e:
        ...     logger.error(f"Orchestration failed: {e}")
    """

    pass


class PatternExecutionError(OrchestrationError):
    """
    Raised when a pattern fails during execution.

    This exception is raised when a pattern encounters an error during
    its execution phase, such as agent failures, iteration limits exceeded,
    or unexpected runtime errors.

    :param message: str - The error message.
    :param pattern_name: Optional[str] - Name of the pattern that failed.
    :param iteration: Optional[int] - Iteration number where failure occurred.
    :param details: Optional[Dict[str, Any]] - Additional error context.

    Example:
        >>> raise PatternExecutionError(
        ...     "Reflection pattern failed to converge",
        ...     pattern_name="reflection",
        ...     iteration=5,
        ...     details={"max_iterations": 3}
        ... )
    """

    def __init__(
        self,
        message: str,
        pattern_name: Optional[str] = None,
        iteration: Optional[int] = None,
        details: Optional[Dict[str, Any]] = None,
    ):
        """
        Initialize a PatternExecutionError with detailed context.

        :param message: str - The main error message.
        :param pattern_name: Optional[str] - Name of the pattern that failed.
        :param iteration: Optional[int] - Iteration number where failure occurred.
        :param details: Optional[Dict[str, Any]] - Additional error context.
        """
        super().__init__(message)
        self.pattern_name = pattern_name
        self.iteration = iteration
        self.details = details or {}

    def __str__(self) -> str:
        """
        Format the error message with all available context.

        :return: str - Formatted error message with pattern details.
        """
        parts = [super().__str__()]

        if self.pattern_name:
            parts.append(f"  Pattern: {self.pattern_name}")
        if self.iteration is not None:
            parts.append(f"  Iteration: {self.iteration}")
        if self.details:
            parts.append(f"  Details: {self.details}")

        return "\n".join(parts)


class PatternConfigurationError(OrchestrationError):
    """
    Raised when a pattern is incorrectly configured.

    This exception is raised during pattern initialization or setup when
    the provided configuration is invalid, incomplete, or incompatible.

    :param message: str - The error message.
    :param config_field: Optional[str] - The configuration field that is invalid.
    :param expected: Optional[str] - Description of expected value.
    :param received: Optional[Any] - The actual value received.

    Example:
        >>> raise PatternConfigurationError(
        ...     "Invalid max_iterations value",
        ...     config_field="max_iterations",
        ...     expected="positive integer",
        ...     received=-1
        ... )
    """

    def __init__(
        self,
        message: str,
        config_field: Optional[str] = None,
        expected: Optional[str] = None,
        received: Optional[Any] = None,
    ):
        """
        Initialize a PatternConfigurationError with validation details.

        :param message: str - The main error message.
        :param config_field: Optional[str] - The configuration field that is invalid.
        :param expected: Optional[str] - Description of expected value.
        :param received: Optional[Any] - The actual value received.
        """
        super().__init__(message)
        self.config_field = config_field
        self.expected = expected
        self.received = received

    def __str__(self) -> str:
        """
        Format the error message with configuration context.

        :return: str - Formatted error message with config details.
        """
        parts = [super().__str__()]

        if self.config_field:
            parts.append(f"  Field: {self.config_field}")
        if self.expected:
            parts.append(f"  Expected: {self.expected}")
        if self.received is not None:
            parts.append(f"  Received: {self.received}")

        return "\n".join(parts)


class AgentError(OrchestrationError):
    """
    Raised when an agent operation fails.

    This exception is raised when agent initialization, execution, or
    communication fails. It can wrap underlying errors from the agent
    implementation.

    :param message: str - The error message.
    :param agent_name: Optional[str] - Name of the agent that failed.
    :param operation: Optional[str] - The operation that failed (e.g., "generate", "execute").
    :param details: Optional[Dict[str, Any]] - Additional error context.

    Example:
        >>> raise AgentError(
        ...     "Agent failed to generate response",
        ...     agent_name="research-agent",
        ...     operation="generate",
        ...     details={"timeout": True}
        ... )
    """

    def __init__(
        self,
        message: str,
        agent_name: Optional[str] = None,
        operation: Optional[str] = None,
        details: Optional[Dict[str, Any]] = None,
    ):
        """
        Initialize an AgentError with operation context.

        :param message: str - The main error message.
        :param agent_name: Optional[str] - Name of the agent that failed.
        :param operation: Optional[str] - The operation that failed.
        :param details: Optional[Dict[str, Any]] - Additional error context.
        """
        super().__init__(message)
        self.agent_name = agent_name
        self.operation = operation
        self.details = details or {}

    def __str__(self) -> str:
        """
        Format the error message with agent context.

        :return: str - Formatted error message with agent details.
        """
        parts = [super().__str__()]

        if self.agent_name:
            parts.append(f"  Agent: {self.agent_name}")
        if self.operation:
            parts.append(f"  Operation: {self.operation}")
        if self.details:
            parts.append(f"  Details: {self.details}")

        return "\n".join(parts)


class ToolExecutionError(OrchestrationError):
    """
    Raised when a tool fails during execution.

    This exception is raised when a tool encounters an error while
    executing its operation, such as invalid inputs, external API
    failures, or resource unavailability.

    :param message: str - The error message.
    :param tool_name: Optional[str] - Name of the tool that failed.
    :param input_data: Optional[Dict[str, Any]] - The input that caused the failure.
    :param details: Optional[Dict[str, Any]] - Additional error context.

    Example:
        >>> raise ToolExecutionError(
        ...     "Calculator tool received invalid expression",
        ...     tool_name="calculator",
        ...     input_data={"expression": "2 + + 2"},
        ...     details={"error": "SyntaxError"}
        ... )
    """

    def __init__(
        self,
        message: str,
        tool_name: Optional[str] = None,
        input_data: Optional[Dict[str, Any]] = None,
        details: Optional[Dict[str, Any]] = None,
    ):
        """
        Initialize a ToolExecutionError with execution context.

        :param message: str - The main error message.
        :param tool_name: Optional[str] - Name of the tool that failed.
        :param input_data: Optional[Dict[str, Any]] - The input that caused the failure.
        :param details: Optional[Dict[str, Any]] - Additional error context.
        """
        super().__init__(message)
        self.tool_name = tool_name
        self.input_data = input_data or {}
        self.details = details or {}

    def __str__(self) -> str:
        """
        Format the error message with tool context.

        :return: str - Formatted error message with tool details.
        """
        parts = [super().__str__()]

        if self.tool_name:
            parts.append(f"  Tool: {self.tool_name}")
        if self.input_data:
            parts.append(f"  Input: {self.input_data}")
        if self.details:
            parts.append(f"  Details: {self.details}")

        return "\n".join(parts)


class StateError(OrchestrationError):
    """
    Raised when state management operations fail.

    This exception is raised when there are errors in state initialization,
    updates, transitions, or persistence operations.

    :param message: str - The error message.
    :param current_state: Optional[str] - The current state when error occurred.
    :param attempted_state: Optional[str] - The state that was being set.
    :param details: Optional[Dict[str, Any]] - Additional error context.

    Example:
        >>> raise StateError(
        ...     "Invalid state transition",
        ...     current_state="running",
        ...     attempted_state="completed",
        ...     details={"reason": "Cannot transition while tasks pending"}
        ... )
    """

    def __init__(
        self,
        message: str,
        current_state: Optional[str] = None,
        attempted_state: Optional[str] = None,
        details: Optional[Dict[str, Any]] = None,
    ):
        """
        Initialize a StateError with state context.

        :param message: str - The main error message.
        :param current_state: Optional[str] - The current state when error occurred.
        :param attempted_state: Optional[str] - The state that was being set.
        :param details: Optional[Dict[str, Any]] - Additional error context.
        """
        super().__init__(message)
        self.current_state = current_state
        self.attempted_state = attempted_state
        self.details = details or {}

    def __str__(self) -> str:
        """
        Format the error message with state context.

        :return: str - Formatted error message with state details.
        """
        parts = [super().__str__()]

        if self.current_state:
            parts.append(f"  Current State: {self.current_state}")
        if self.attempted_state:
            parts.append(f"  Attempted State: {self.attempted_state}")
        if self.details:
            parts.append(f"  Details: {self.details}")

        return "\n".join(parts)


class ValidationError(OrchestrationError):
    """
    Raised when input validation fails.

    This exception is raised when user inputs, configuration values, or
    data structures fail validation checks. Provides detailed information
    about what was expected versus what was received.

    :param message: str - The error message.
    :param field: Optional[str] - Name of the field that failed validation.
    :param expected: Optional[str] - Description of what was expected.
    :param received: Optional[Any] - Description of what was received.
    :param example: Optional[str] - Example of valid input.

    Example:
        >>> raise ValidationError(
        ...     "Invalid task input",
        ...     field="task",
        ...     expected="non-empty string",
        ...     received="",
        ...     example="task='Summarize this document'"
        ... )
    """

    def __init__(
        self,
        message: str,
        field: Optional[str] = None,
        expected: Optional[str] = None,
        received: Optional[Any] = None,
        example: Optional[str] = None,
    ):
        """
        Initialize a ValidationError with detailed context.

        :param message: str - The main error message.
        :param field: Optional[str] - Name of the field that failed validation.
        :param expected: Optional[str] - Description of what was expected.
        :param received: Optional[Any] - Description of what was received.
        :param example: Optional[str] - Example of valid input.
        """
        super().__init__(message)
        self.field = field
        self.expected = expected
        self.received = received
        self.example = example

    def __str__(self) -> str:
        """
        Format the error message with all available context.

        :return: str - Formatted error message with validation details.
        """
        parts = [super().__str__()]

        if self.field:
            parts.append(f"  Field: {self.field}")
        if self.expected:
            parts.append(f"  Expected: {self.expected}")
        if self.received is not None:
            parts.append(f"  Received: {self.received}")
        if self.example:
            parts.append(f"  Example: {self.example}")

        return "\n".join(parts)
