"""Typed exception hierarchy for the Rulebook SDK.

Hierarchy::

    RulebookError
    └── APIError
        ├── APIConnectionError
        │   └── APITimeoutError
        └── APIStatusError
            ├── BadRequestError          (400)
            ├── AuthenticationError      (401)
            ├── PermissionDeniedError    (403)
            ├── NotFoundError            (404)
            ├── UnprocessableEntityError (422)
            ├── RateLimitError           (429)
            └── InternalServerError      (5xx)
"""

from __future__ import annotations

from typing import Optional

import httpx
from typing_extensions import Literal

__all__ = [
    "RulebookError",
    "APIError",
    "APIConnectionError",
    "APITimeoutError",
    "APIStatusError",
    "BadRequestError",
    "AuthenticationError",
    "PermissionDeniedError",
    "NotFoundError",
    "UnprocessableEntityError",
    "RateLimitError",
    "InternalServerError",
]


class RulebookError(Exception):
    """Base exception for all Rulebook SDK errors (non-HTTP, e.g., missing API key)."""

    pass


class APIError(RulebookError):
    """Base exception for all HTTP-related errors."""

    message: str
    request: httpx.Request

    body: object | None
    """The API response body. Parsed JSON if valid, raw text otherwise, None if no response."""

    def __init__(self, message: str, request: httpx.Request, *, body: object | None) -> None:
        super().__init__(message)
        self.message = message
        self.request = request
        self.body = body


class APIConnectionError(APIError):
    """Raised on network-level failures (DNS, socket, connection refused)."""

    def __init__(
        self,
        *,
        message: str = "Connection error.",
        request: httpx.Request,
        cause: BaseException | None = None,
    ) -> None:
        if cause is not None and not message.startswith("Request timed out"):
            url = str(request.url)
            message = f"Connection error: {cause} (URL: {url})"
        super().__init__(message, request, body=None)
        self.__cause__ = cause


class APITimeoutError(APIConnectionError):
    """Raised when a request times out."""

    def __init__(
        self,
        request: httpx.Request,
        *,
        cause: BaseException | None = None,
    ) -> None:
        url = str(request.url)
        if cause is not None:
            message = f"Request timed out: {cause} (URL: {url})"
        else:
            message = f"Request timed out. (URL: {url})"
        super().__init__(message=message, request=request, cause=cause)


class APIStatusError(APIError):
    """Raised when an API response has a status code of 4xx or 5xx."""

    response: httpx.Response
    status_code: int
    error_code: str | None
    """The API error code (e.g., ``"UNAUTHORIZED"``, ``"VALIDATION_ERROR"``)."""
    details: list[object] | None
    """Validation error details (populated for 422 responses)."""

    def __init__(
        self,
        message: str,
        *,
        response: httpx.Response,
        body: object | None,
        error_code: str | None = None,
        details: list[object] | None = None,
    ) -> None:
        super().__init__(message, response.request, body=body)
        self.response = response
        self.status_code = response.status_code
        self.error_code = error_code
        self.details = details


class BadRequestError(APIStatusError):
    """Raised on HTTP 400."""

    status_code: Literal[400] = 400  # type: ignore[assignment]


class AuthenticationError(APIStatusError):
    """Raised on HTTP 401 — missing or invalid API key."""

    status_code: Literal[401] = 401  # type: ignore[assignment]


class PermissionDeniedError(APIStatusError):
    """Raised on HTTP 403 — no access to the requested resource."""

    status_code: Literal[403] = 403  # type: ignore[assignment]


class NotFoundError(APIStatusError):
    """Raised on HTTP 404 — resource does not exist."""

    status_code: Literal[404] = 404  # type: ignore[assignment]


class UnprocessableEntityError(APIStatusError):
    """Raised on HTTP 422 — invalid query parameters."""

    status_code: Literal[422] = 422  # type: ignore[assignment]


class RateLimitError(APIStatusError):
    """Raised on HTTP 429 — rate limit exceeded."""

    status_code: Literal[429] = 429  # type: ignore[assignment]


class InternalServerError(APIStatusError):
    """Raised on HTTP 5xx — server-side error."""

    pass
