"""Exception hierarchy for the DexFather SDK.

All SDK exceptions inherit from :class:`DexFatherError`. API-level errors
(HTTP 4xx/5xx) inherit from :class:`APIError`, while transport-layer
problems inherit from :class:`NetworkError`.

Hierarchy::

    DexFatherError
    ├── APIError
    │   ├── BadRequest        (400)
    │   ├── Unauthorized      (401)
    │   ├── Forbidden         (403)
    │   ├── NotFound          (404)
    │   └── RateLimited       (429)
    └── NetworkError
        ├── TimeoutError
        └── WebSocketError
"""

from __future__ import annotations

import re
from typing import Optional


class DexFatherError(Exception):
    """Base exception for all DexFather SDK errors."""

    def __init__(self, message: str = "An error occurred") -> None:
        self.message = message
        super().__init__(message)


class APIError(DexFatherError):
    """Raised when the API returns an error response (HTTP 4xx/5xx).

    Attributes:
        error_code: The HTTP status code.
        description: Human-readable error description from the server.
    """

    def __init__(self, error_code: int, description: str) -> None:
        self.error_code = error_code
        self.description = description
        super().__init__(f"[{error_code}] {description}")


class BadRequest(APIError):
    """HTTP 400 — the request was malformed or invalid."""

    def __init__(self, description: str = "Bad Request") -> None:
        super().__init__(400, description)


class Unauthorized(APIError):
    """HTTP 401 — authentication is required or has failed."""

    def __init__(self, description: str = "Unauthorized") -> None:
        super().__init__(401, description)


class Forbidden(APIError):
    """HTTP 403 — the authenticated user lacks permission."""

    def __init__(self, description: str = "Forbidden") -> None:
        super().__init__(403, description)


class NotFound(APIError):
    """HTTP 404 — the requested resource does not exist."""

    def __init__(self, description: str = "Not Found") -> None:
        super().__init__(404, description)


class RateLimited(APIError):
    """HTTP 429 — too many requests; retry after the indicated delay.

    Attributes:
        retry_after: Seconds to wait before the next request.
    """

    def __init__(
        self,
        retry_after: float = 1.0,
        description: str = "Rate limited",
    ) -> None:
        self.retry_after = retry_after
        super().__init__(429, description)


class NetworkError(DexFatherError):
    """Raised when a request fails due to a transport-level problem."""

    pass


class TimeoutError(NetworkError):  # noqa: A003 — intentionally shadows builtin
    """Raised when a request exceeds the configured timeout."""

    pass


class WebSocketError(NetworkError):
    """Raised when the WebSocket connection encounters an error."""

    pass


# ---------------------------------------------------------------------------
# Factory
# ---------------------------------------------------------------------------

_RETRY_AFTER_RE = re.compile(r"Retry after (\d+(?:\.\d+)?)s", re.IGNORECASE)
MAX_RETRY_AFTER = 300  # 5 minutes -- prevents server-controlled DoS

_STATUS_MAP: dict[int, type[APIError]] = {
    400: BadRequest,
    401: Unauthorized,
    403: Forbidden,
    404: NotFound,
}


def _map_error(code: int, description: str) -> APIError:
    """Map an HTTP status code + description to the appropriate exception.

    For 429 responses the ``retry_after`` value is parsed from the
    description if it matches the pattern ``"Retry after Ns"``.  Falls
    back to 1.0 seconds when the pattern is absent.
    """
    if code == 429:
        retry_after = 1.0
        match = _RETRY_AFTER_RE.search(description)
        if match:
            retry_after = min(float(match.group(1)), MAX_RETRY_AFTER)
        return RateLimited(retry_after=retry_after, description=description)

    cls = _STATUS_MAP.get(code)
    if cls is not None:
        return cls(description)

    return APIError(code, description)
