"""
Exceptions - Custom exception hierarchy for the Agent Orchestrator.

This module provides a structured exception hierarchy to replace
broad `except Exception` blocks with specific, meaningful errors.

Exception Hierarchy:
    OrchestratorError (base)
    ├── AgentError
    │   ├── AgentNotFoundError
    │   ├── AgentUnavailableError
    │   ├── AgentAuthenticationError
    │   ├── AgentTimeoutError
    │   └── AgentExecutionError
    ├── TaskError
    │   ├── TaskNotFoundError
    │   ├── TaskAssignmentError
    │   └── TaskExecutionError
    ├── RiskError
    │   ├── RiskBlockedError
    │   └── ApprovalRequiredError
    ├── BudgetError
    │   ├── BudgetExceededError
    │   └── RateLimitError
    ├── MemoryError
    │   ├── MemoryWriteError
    │   └── MemoryReadError
    └── ConfigurationError

Usage:
    from agent_orchestrator.exceptions import AgentUnavailableError

    try:
        await agent.execute(task)
    except AgentUnavailableError as e:
        logger.warning(f"Agent unavailable: {e.agent_id} - {e.reason}")
        # Handle gracefully
"""

from typing import Optional, Any, Dict


class OrchestratorError(Exception):
    """
    Base exception for all orchestrator errors.

    All custom exceptions inherit from this class, making it easy
    to catch any orchestrator-related error.
    """

    def __init__(
        self,
        message: str,
        *,
        details: Optional[Dict[str, Any]] = None,
        cause: Optional[Exception] = None,
    ):
        """
        Initialize the exception.

        Args:
            message: Human-readable error message
            details: Additional context as key-value pairs
            cause: Original exception that caused this error
        """
        super().__init__(message)
        self.message = message
        self.details = details or {}
        self.cause = cause

    def __str__(self) -> str:
        result = self.message
        if self.details:
            detail_str = ", ".join(f"{k}={v}" for k, v in self.details.items())
            result = f"{result} ({detail_str})"
        if self.cause:
            result = f"{result} [caused by: {self.cause}]"
        return result


# =============================================================================
# Agent Errors
# =============================================================================


class AgentError(OrchestratorError):
    """Base class for agent-related errors."""

    def __init__(
        self,
        message: str,
        agent_id: Optional[str] = None,
        **kwargs,
    ):
        super().__init__(message, **kwargs)
        self.agent_id = agent_id
        if agent_id:
            self.details["agent_id"] = agent_id


class AgentNotFoundError(AgentError):
    """Raised when a requested agent does not exist."""

    def __init__(self, agent_id: str, **kwargs):
        super().__init__(
            f"Agent not found: {agent_id}",
            agent_id=agent_id,
            **kwargs,
        )


class AgentUnavailableError(AgentError):
    """Raised when an agent exists but is not available for tasks."""

    def __init__(
        self,
        agent_id: str,
        reason: str = "unavailable",
        **kwargs,
    ):
        super().__init__(
            f"Agent unavailable: {agent_id} ({reason})",
            agent_id=agent_id,
            **kwargs,
        )
        self.reason = reason
        self.details["reason"] = reason


class AgentAuthenticationError(AgentError):
    """Raised when agent authentication fails."""

    def __init__(
        self,
        agent_id: str,
        auth_method: str = "unknown",
        **kwargs,
    ):
        super().__init__(
            f"Authentication failed for agent: {agent_id}",
            agent_id=agent_id,
            **kwargs,
        )
        self.auth_method = auth_method
        self.details["auth_method"] = auth_method


class AgentTimeoutError(AgentError):
    """Raised when an agent operation times out."""

    def __init__(
        self,
        agent_id: str,
        operation: str = "operation",
        timeout_seconds: float = 0,
        **kwargs,
    ):
        super().__init__(
            f"Agent timeout: {agent_id} ({operation} exceeded {timeout_seconds}s)",
            agent_id=agent_id,
            **kwargs,
        )
        self.operation = operation
        self.timeout_seconds = timeout_seconds
        self.details["operation"] = operation
        self.details["timeout_seconds"] = timeout_seconds


class AgentExecutionError(AgentError):
    """Raised when an agent fails to execute a task."""

    def __init__(
        self,
        agent_id: str,
        task_id: Optional[str] = None,
        error_type: str = "execution_failed",
        **kwargs,
    ):
        msg = f"Agent execution failed: {agent_id}"
        if task_id:
            msg = f"{msg} (task: {task_id})"
        super().__init__(msg, agent_id=agent_id, **kwargs)
        self.task_id = task_id
        self.error_type = error_type
        if task_id:
            self.details["task_id"] = task_id
        self.details["error_type"] = error_type


# =============================================================================
# Task Errors
# =============================================================================


class TaskError(OrchestratorError):
    """Base class for task-related errors."""

    def __init__(
        self,
        message: str,
        task_id: Optional[str] = None,
        **kwargs,
    ):
        super().__init__(message, **kwargs)
        self.task_id = task_id
        if task_id:
            self.details["task_id"] = task_id


class TaskNotFoundError(TaskError):
    """Raised when a requested task does not exist."""

    def __init__(self, task_id: str, **kwargs):
        super().__init__(
            f"Task not found: {task_id}",
            task_id=task_id,
            **kwargs,
        )


class TaskAssignmentError(TaskError):
    """Raised when a task cannot be assigned to an agent."""

    def __init__(
        self,
        task_id: str,
        reason: str = "assignment failed",
        target_agent: Optional[str] = None,
        **kwargs,
    ):
        msg = f"Cannot assign task {task_id}: {reason}"
        super().__init__(msg, task_id=task_id, **kwargs)
        self.reason = reason
        self.target_agent = target_agent
        self.details["reason"] = reason
        if target_agent:
            self.details["target_agent"] = target_agent


class TaskExecutionError(TaskError):
    """Raised when task execution fails."""

    def __init__(
        self,
        task_id: str,
        stage: str = "execution",
        **kwargs,
    ):
        super().__init__(
            f"Task execution failed at {stage}: {task_id}",
            task_id=task_id,
            **kwargs,
        )
        self.stage = stage
        self.details["stage"] = stage


# =============================================================================
# Risk & Approval Errors
# =============================================================================


class RiskError(OrchestratorError):
    """Base class for risk-related errors."""
    pass


class RiskBlockedError(RiskError):
    """Raised when an action is blocked due to risk classification."""

    def __init__(
        self,
        action: str,
        risk_level: str = "critical",
        reason: str = "action blocked",
        **kwargs,
    ):
        super().__init__(
            f"Action blocked ({risk_level}): {action} - {reason}",
            **kwargs,
        )
        self.action = action
        self.risk_level = risk_level
        self.reason = reason
        self.details["action"] = action
        self.details["risk_level"] = risk_level
        self.details["reason"] = reason


class ApprovalRequiredError(RiskError):
    """Raised when an action requires human approval."""

    def __init__(
        self,
        action: str,
        risk_level: str = "high",
        approval_id: Optional[str] = None,
        **kwargs,
    ):
        super().__init__(
            f"Approval required for {risk_level} risk action: {action}",
            **kwargs,
        )
        self.action = action
        self.risk_level = risk_level
        self.approval_id = approval_id
        self.details["action"] = action
        self.details["risk_level"] = risk_level
        if approval_id:
            self.details["approval_id"] = approval_id


# =============================================================================
# Budget & Rate Limit Errors
# =============================================================================


class BudgetError(OrchestratorError):
    """Base class for budget-related errors."""

    def __init__(
        self,
        message: str,
        agent_id: Optional[str] = None,
        **kwargs,
    ):
        super().__init__(message, **kwargs)
        self.agent_id = agent_id
        if agent_id:
            self.details["agent_id"] = agent_id


class BudgetExceededError(BudgetError):
    """Raised when an agent exceeds its budget."""

    def __init__(
        self,
        agent_id: str,
        budget_type: str = "daily",
        current_usage: float = 0,
        limit: float = 0,
        **kwargs,
    ):
        pct = (current_usage / limit * 100) if limit > 0 else 0
        super().__init__(
            f"Budget exceeded for {agent_id}: {budget_type} usage at {pct:.0f}%",
            agent_id=agent_id,
            **kwargs,
        )
        self.budget_type = budget_type
        self.current_usage = current_usage
        self.limit = limit
        self.details["budget_type"] = budget_type
        self.details["current_usage"] = current_usage
        self.details["limit"] = limit


class RateLimitError(BudgetError):
    """Raised when rate limits are hit."""

    def __init__(
        self,
        agent_id: Optional[str] = None,
        provider: Optional[str] = None,
        retry_after: Optional[float] = None,
        **kwargs,
    ):
        msg = "Rate limit exceeded"
        if provider:
            msg = f"{msg} for {provider}"
        if agent_id:
            msg = f"{msg} (agent: {agent_id})"
        super().__init__(msg, agent_id=agent_id, **kwargs)
        self.provider = provider
        self.retry_after = retry_after
        if provider:
            self.details["provider"] = provider
        if retry_after:
            self.details["retry_after"] = retry_after


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


class MemoryError(OrchestratorError):
    """Base class for memory-related errors."""
    pass


class MemoryWriteError(MemoryError):
    """Raised when writing to memory fails."""

    def __init__(
        self,
        memory_type: str = "operational",
        key: Optional[str] = None,
        **kwargs,
    ):
        msg = f"Failed to write to {memory_type} memory"
        if key:
            msg = f"{msg} (key: {key})"
        super().__init__(msg, **kwargs)
        self.memory_type = memory_type
        self.key = key
        self.details["memory_type"] = memory_type
        if key:
            self.details["key"] = key


class MemoryReadError(MemoryError):
    """Raised when reading from memory fails."""

    def __init__(
        self,
        memory_type: str = "operational",
        key: Optional[str] = None,
        **kwargs,
    ):
        msg = f"Failed to read from {memory_type} memory"
        if key:
            msg = f"{msg} (key: {key})"
        super().__init__(msg, **kwargs)
        self.memory_type = memory_type
        self.key = key
        self.details["memory_type"] = memory_type
        if key:
            self.details["key"] = key


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


class ConfigurationError(OrchestratorError):
    """Raised when there's a configuration problem."""

    def __init__(
        self,
        config_key: str,
        message: str = "invalid configuration",
        **kwargs,
    ):
        super().__init__(
            f"Configuration error for '{config_key}': {message}",
            **kwargs,
        )
        self.config_key = config_key
        self.details["config_key"] = config_key


# =============================================================================
# Convenience Functions
# =============================================================================


def wrap_exception(
    exc: Exception,
    wrapper_class: type = OrchestratorError,
    message: Optional[str] = None,
    **kwargs,
) -> OrchestratorError:
    """
    Wrap a generic exception in an OrchestratorError.

    Args:
        exc: The original exception
        wrapper_class: The OrchestratorError subclass to use
        message: Optional custom message (defaults to str(exc))
        **kwargs: Additional arguments for the wrapper

    Returns:
        Wrapped exception
    """
    return wrapper_class(
        message or str(exc),
        cause=exc,
        **kwargs,
    )
