"""
Boring-OS SDK exceptions.

Hierarchical exception classes for handling different error scenarios
from the Boring-OS API.
"""

from typing import Any


class BoringOSError(Exception):
    """
    Base exception for all Boring-OS SDK errors.
    
    Attributes:
        message: Human-readable error description
        code: Machine-readable error code (if available)
        details: Additional error context
        retryable: Whether the request can be retried
    """
    
    def __init__(
        self,
        message: str,
        code: str | None = None,
        details: dict[str, Any] | None = None,
        retryable: bool = False,
    ):
        super().__init__(message)
        self.message = message
        self.code = code
        self.details = details or {}
        self.retryable = retryable
    
    def __str__(self) -> str:
        if self.code:
            return f"[{self.code}] {self.message}"
        return self.message


class AuthenticationError(BoringOSError):
    """
    Raised when API authentication fails.
    
    Common causes:
    - Invalid or expired API key
    - Missing API key header
    """
    
    def __init__(self, message: str = "Authentication failed", **kwargs: Any):
        super().__init__(message, code="AUTHENTICATION_ERROR", **kwargs)


class AuthorizationError(BoringOSError):
    """
    Raised when the authenticated user lacks permission.
    
    Common causes:
    - Accessing tools outside tenant scope
    - Insufficient permissions for operation
    """
    
    def __init__(self, message: str = "Authorization failed", **kwargs: Any):
        super().__init__(message, code="AUTHORIZATION_ERROR", **kwargs)


class RateLimitError(BoringOSError):
    """
    Raised when rate limit is exceeded.
    
    The request can be retried after waiting.
    
    Attributes:
        retry_after: Seconds to wait before retrying (if provided)
    """
    
    def __init__(
        self,
        message: str = "Rate limit exceeded",
        retry_after: int | None = None,
        **kwargs: Any,
    ):
        super().__init__(message, code="RATE_LIMIT_EXCEEDED", retryable=True, **kwargs)
        self.retry_after = retry_after


class ValidationError(BoringOSError):
    """
    Raised when request validation fails.
    
    Common causes:
    - Invalid parameter types
    - Missing required parameters
    - Parameter value out of range
    """
    
    def __init__(self, message: str = "Validation failed", **kwargs: Any):
        super().__init__(message, code="VALIDATION_ERROR", **kwargs)


class ToolError(BoringOSError):
    """Base exception for tool-related errors."""
    pass


class ToolNotFoundError(ToolError):
    """
    Raised when the requested tool does not exist.
    
    Common causes:
    - Typo in tool name
    - Tool was removed or disabled
    - Version not found
    """
    
    def __init__(
        self,
        tool_name: str,
        message: str | None = None,
        **kwargs: Any,
    ):
        msg = message or f"Tool not found: {tool_name}"
        super().__init__(msg, code="TOOL_NOT_FOUND", **kwargs)
        self.tool_name = tool_name


class ToolDisabledError(ToolError):
    """
    Raised when attempting to use a disabled tool.
    """
    
    def __init__(
        self,
        tool_name: str,
        message: str | None = None,
        **kwargs: Any,
    ):
        msg = message or f"Tool is disabled: {tool_name}"
        super().__init__(msg, code="TOOL_DISABLED", **kwargs)
        self.tool_name = tool_name


class ToolTimeoutError(ToolError):
    """
    Raised when tool execution times out.
    
    The request may be retryable with a longer timeout.
    """
    
    def __init__(
        self,
        tool_name: str,
        timeout_seconds: int | None = None,
        message: str | None = None,
        **kwargs: Any,
    ):
        msg = message or f"Tool execution timed out: {tool_name}"
        super().__init__(msg, code="TOOL_TIMEOUT", retryable=True, **kwargs)
        self.tool_name = tool_name
        self.timeout_seconds = timeout_seconds


class ToolExecutionError(ToolError):
    """
    Raised when tool execution fails.
    
    The underlying cause may be in the adapter or vendor API.
    """
    
    def __init__(
        self,
        tool_name: str,
        message: str | None = None,
        **kwargs: Any,
    ):
        msg = message or f"Tool execution failed: {tool_name}"
        super().__init__(msg, code="TOOL_EXECUTION_ERROR", **kwargs)
        self.tool_name = tool_name


class AdapterError(ToolError):
    """
    Raised when an adapter fails to process the request.
    
    This typically indicates an issue with the external service
    that backs the tool.
    """
    
    def __init__(
        self,
        adapter_id: str | None = None,
        message: str = "Adapter error",
        **kwargs: Any,
    ):
        super().__init__(message, code="ADAPTER_ERROR", retryable=True, **kwargs)
        self.adapter_id = adapter_id


class ConnectionError(BoringOSError):
    """
    Raised when unable to connect to the Boring-OS API.
    
    Common causes:
    - Network issues
    - Service is down
    - Invalid base URL
    """
    
    def __init__(self, message: str = "Connection failed", **kwargs: Any):
        super().__init__(message, retryable=True, **kwargs)


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

class AgentError(BoringOSError):
    """Base exception for agent-related errors."""
    pass


class AgentNotFoundError(AgentError):
    """
    Raised when the requested agent does not exist.
    
    Common causes:
    - Typo in agent name
    - Agent was deprecated or removed
    """
    
    def __init__(
        self,
        agent_name: str,
        message: str | None = None,
        **kwargs: Any,
    ):
        msg = message or f"Agent not found: {agent_name}"
        super().__init__(msg, code="AGENT_NOT_FOUND", **kwargs)
        self.agent_name = agent_name


class AgentTimeoutError(AgentError):
    """
    Raised when agent execution times out.
    
    The agent loop exceeded the configured timeout.
    """
    
    def __init__(
        self,
        agent_name: str = "unknown",
        message: str | None = None,
        **kwargs: Any,
    ):
        msg = message or f"Agent execution timed out: {agent_name}"
        super().__init__(msg, code="AGENT_TIMEOUT", retryable=True, **kwargs)
        self.agent_name = agent_name


# =============================================================================
# Flow Errors
# =============================================================================

class FlowError(BoringOSError):
    """Base exception for flow-related errors."""
    pass


class FlowNotFoundError(FlowError):
    """
    Raised when the requested flow does not exist.
    """
    
    def __init__(
        self,
        flow_name: str,
        message: str | None = None,
        **kwargs: Any,
    ):
        msg = message or f"Flow not found: {flow_name}"
        super().__init__(msg, code="FLOW_NOT_FOUND", **kwargs)
        self.flow_name = flow_name


class FlowTimeoutError(FlowError):
    """
    Raised when flow execution times out.
    """
    
    def __init__(
        self,
        flow_name: str = "unknown",
        message: str | None = None,
        **kwargs: Any,
    ):
        msg = message or f"Flow execution timed out: {flow_name}"
        super().__init__(msg, code="FLOW_TIMEOUT", retryable=True, **kwargs)
        self.flow_name = flow_name


# =============================================================================
# Model Errors
# =============================================================================

class ModelError(BoringOSError):
    """
    Raised when a direct model completion fails.
    
    Common causes:
    - Provider unavailable
    - Invalid model name
    - Token limit exceeded
    """
    
    def __init__(self, message: str = "Model completion failed", **kwargs: Any):
        super().__init__(message, code="MODEL_ERROR", **kwargs)


# =============================================================================
# Session Errors
# =============================================================================

class SessionError(BoringOSError):
    """
    Raised when a session operation fails.
    
    Common causes:
    - Session not found (expired or invalid ID)
    - Session already closed
    """
    
    def __init__(self, message: str = "Session error", **kwargs: Any):
        super().__init__(message, code="SESSION_ERROR", **kwargs)
