"""
Custom exceptions for fabias.

Provides a hierarchy of exceptions for different error scenarios:
- FabiasError: Base exception for all fabias errors
- AuthenticationError: OAuth/authentication failures
- ApiError: HTTP/API request failures
- NotFoundError: Resource not found (404) errors
"""

from typing import Any, Optional


class FabiasError(Exception):
    """
    Base exception for all fabias errors.

    Provides exception chaining support similar to Java's exception chaining,
    allowing nested exceptions to preserve the full error context.

    Attributes:
        message (str): Human-readable error description
        cause (Exception): Original exception that caused this error, if any

    Examples:
        >>> try:
        ...     risky_operation()
        ... except SomeError as e:
        ...     raise FabiasError("Operation failed", cause=e)
    """

    def __init__(self, message: str, cause: Optional[Exception] = None):
        """
        Initialize the exception with a message and optional cause.

        Args:
            message: Human-readable error description
            cause: Original exception that caused this error
        """
        self.message = message
        self.cause = cause

        # Build full message including cause
        full_message = message
        if cause:
            full_message = f"{message} -> {type(cause).__name__}: {str(cause)}"

        super().__init__(full_message)

        # Set the __cause__ for Python's built-in exception chaining
        if cause:
            self.__cause__ = cause


class AuthenticationError(FabiasError):
    """
    Exception raised when authentication fails.

    This exception is raised when:
    - OAuth2 token acquisition fails
    - Credentials are invalid or expired
    - Service principal authentication fails
    - notebookutils credentials are unavailable

    Examples:
        >>> try:
        ...     token = auth.getToken()
        ... except AuthenticationError as e:
        ...     print(f"Auth failed: {e.message}")
    """

    pass


class ApiError(FabiasError):
    """
    Exception raised when an API request fails.

    This exception is raised when:
    - HTTP request returns an error status code (4xx, 5xx)
    - Response cannot be parsed
    - Request times out
    - Network errors occur

    Attributes:
        status_code (int): HTTP status code, if available
        response_body (Any): Response body content, if available

    Examples:
        >>> try:
        ...     response = client.get("/workspaces")
        ... except ApiError as e:
        ...     print(f"API error {e.status_code}: {e.message}")
    """

    def __init__(
        self,
        message: str,
        cause: Optional[Exception] = None,
        status_code: Optional[int] = None,
        response_body: Any = None,
    ):
        """
        Initialize the API error with details.

        Args:
            message: Human-readable error description
            cause: Original exception that caused this error
            status_code: HTTP status code from the response
            response_body: Response body content for debugging
        """
        super().__init__(message, cause)
        self.status_code = status_code
        self.response_body = response_body


class NotFoundError(ApiError):
    """
    Exception raised when a requested resource is not found.

    This is a specific case of ApiError for 404 responses. It provides
    clearer semantics when handling "not found" scenarios separately
    from other API errors.

    Attributes:
        resource_type (str): Type of resource that wasn't found
        resource_id (str): Identifier of the missing resource

    Examples:
        >>> try:
        ...     workspace = client.workspace("nonexistent")
        ... except NotFoundError as e:
        ...     print(f"{e.resource_type} '{e.resource_id}' not found")
    """

    def __init__(
        self,
        message: str,
        resource_type: Optional[str] = None,
        resource_id: Optional[str] = None,
        cause: Optional[Exception] = None,
    ):
        """
        Initialize the not found error with resource details.

        Args:
            message: Human-readable error description
            resource_type: Type of resource (e.g., "Workspace", "Pipeline")
            resource_id: Identifier that was searched for
            cause: Original exception that caused this error
        """
        super().__init__(message, cause=cause, status_code=404)
        self.resource_type = resource_type
        self.resource_id = resource_id


class OperationError(FabiasError):
    """
    Exception raised when a long-running operation fails.

    This exception is raised when:
    - Async operations (like git sync or publish) fail
    - Operation polling times out
    - Operation returns a failed status

    Attributes:
        operation_id (str): ID of the failed operation
        operation_status (str): Final status of the operation

    Examples:
        >>> try:
        ...     operation.wait()
        ... except OperationError as e:
        ...     print(f"Operation {e.operation_id} failed: {e.operation_status}")
    """

    def __init__(
        self,
        message: str,
        operation_id: Optional[str] = None,
        operation_status: Optional[str] = None,
        cause: Optional[Exception] = None,
    ):
        """
        Initialize the operation error with details.

        Args:
            message: Human-readable error description
            operation_id: ID of the operation that failed
            operation_status: Final status string from the API
            cause: Original exception that caused this error
        """
        super().__init__(message, cause)
        self.operation_id = operation_id
        self.operation_status = operation_status
