"""Typed exception hierarchy for Sika API errors.

Maps RFC 7807 Problem Details responses to typed Python exceptions.
Each exception carries the HTTP status code, machine-readable error
code, human-readable detail, and optional RFC 7807 ``type`` and
``instance`` URIs.

The ``parse_error_response`` function is the main entry point: given
a status code and response body (dict or raw string), it returns the
appropriate exception instance. The HTTP client calls this on every
non-2xx response.
"""

from __future__ import annotations

from typing import Any


class SikaError(Exception):
    """Base exception for all Sika SDK errors.

    All Sika exceptions carry RFC 7807 Problem Details fields when
    the API response includes them.

    Args:
        message: Human-readable error description.
        status_code: HTTP status code that triggered the error.
        code: Machine-readable error code (e.g. ``UNAUTHORIZED``).
        type: URI identifying the problem type (RFC 7807 ``type`` field).
        instance: URI identifying the specific occurrence.
    """

    def __init__(
        self,
        message: str,
        *,
        status_code: int = 0,
        code: str = "",
        type: str = "",
        instance: str = "",
    ) -> None:
        super().__init__(message)
        self.status_code = status_code
        self.detail = message
        self.code = code
        self.type = type
        self.instance = instance

    def __str__(self) -> str:
        parts: list[str] = []
        if self.status_code:
            if self.code:
                parts.append(f"[{self.status_code} {self.code}]")
            else:
                parts.append(f"[{self.status_code}]")
        parts.append(self.detail)
        return " ".join(parts)


class AuthenticationError(SikaError):
    """Raised when the API returns 401 Unauthorized."""


class ForbiddenError(SikaError):
    """Raised when the API returns 403 Forbidden.

    The request was authenticated but the user is not authorized
    to perform the requested action.
    """


class NotFoundError(SikaError):
    """Raised when the API returns 404 Not Found."""


class ValidationError(SikaError):
    """Raised when the API returns 400 Bad Request.

    Attributes:
        errors: List of field-level validation errors from the
            response body, if provided. Each entry is a dict with
            keys like ``field``, ``message``, ``code``.
    """

    def __init__(
        self,
        message: str,
        *,
        status_code: int = 400,
        code: str = "",
        type: str = "",
        instance: str = "",
        errors: list[dict[str, Any]] | None = None,
    ) -> None:
        super().__init__(
            message,
            status_code=status_code,
            code=code,
            type=type,
            instance=instance,
        )
        self.errors = errors or []


class ConflictError(SikaError):
    """Raised when the API returns 409 Conflict.

    The request conflicts with current server state (e.g. a resource
    with the same identifier already exists).
    """


class RateLimitError(SikaError):
    """Raised when the API returns 429 Too Many Requests.

    Attributes:
        retry_after: Seconds to wait before retrying, if provided
            by the server via the ``Retry-After`` header or response body.
    """

    def __init__(
        self,
        message: str,
        *,
        status_code: int = 429,
        code: str = "",
        type: str = "",
        instance: str = "",
        retry_after: float | None = None,
    ) -> None:
        super().__init__(
            message,
            status_code=status_code,
            code=code,
            type=type,
            instance=instance,
        )
        self.retry_after = retry_after


class QuotaExceededError(SikaError):
    """Raised when the API returns 429 with code ``QUOTA_EXCEEDED``.

    Indicates the user has exhausted their entitlement limit for a
    product dimension (e.g. monthly scan quota).

    Attributes:
        dimension: Which quota dimension was exceeded (e.g. ``scans``).
        current: Current usage count.
        limit: Maximum allowed for the billing period.
        period: Billing period (e.g. ``monthly``).
        upgrade_url: URL where the user can upgrade their plan.
    """

    def __init__(
        self,
        message: str,
        *,
        status_code: int = 429,
        code: str = "QUOTA_EXCEEDED",
        type: str = "",
        instance: str = "",
        retry_after: float | None = None,
        dimension: str = "",
        current: int = 0,
        limit: int = 0,
        period: str = "",
        upgrade_url: str = "",
    ) -> None:
        super().__init__(
            message,
            status_code=status_code,
            code=code,
            type=type,
            instance=instance,
        )
        self.retry_after = retry_after
        self.dimension = dimension
        self.current = current
        self.limit = limit
        self.period = period
        self.upgrade_url = upgrade_url


class ServerError(SikaError):
    """Raised when the API returns a 5xx server error."""


def parse_error_response(status_code: int, body: Any) -> SikaError:
    """Parse an API error response into a typed exception.

    Maps the HTTP status code and RFC 7807 response body to the
    appropriate ``SikaError`` subclass. Handles malformed or missing
    bodies gracefully by falling back to a generic ``SikaError``.

    Args:
        status_code: HTTP status code (non-2xx).
        body: Parsed JSON response body (dict) or raw response text
            (str). May also be ``None`` for empty responses.

    Returns:
        A ``SikaError`` (or subclass) instance ready to be raised.
    """
    # Normalise body to a dict. Malformed or non-dict bodies get
    # a generic fallback message.
    if not isinstance(body, dict):
        detail = str(body) if body else f"HTTP {status_code}"
        return _build_error(status_code, detail=detail)

    detail = str(body.get("detail") or body.get("message") or f"HTTP {status_code}")
    code = str(body.get("code", ""))
    type_uri = str(body.get("type", ""))
    instance = str(body.get("instance", ""))

    return _build_error(
        status_code,
        detail=detail,
        code=code,
        type=type_uri,
        instance=instance,
        body=body,
    )


def _build_error(
    status_code: int,
    *,
    detail: str,
    code: str = "",
    type: str = "",
    instance: str = "",
    body: dict[str, Any] | None = None,
) -> SikaError:
    """Construct the correct exception subclass for a status code."""
    if status_code == 400:
        errors = body.get("errors") if body else None
        errors_list = errors if isinstance(errors, list) else []
        return ValidationError(
            detail,
            status_code=status_code,
            code=code,
            type=type,
            instance=instance,
            errors=errors_list,
        )

    if status_code == 401:
        return AuthenticationError(
            detail, status_code=status_code, code=code, type=type, instance=instance
        )

    if status_code == 403:
        return ForbiddenError(
            detail, status_code=status_code, code=code, type=type, instance=instance
        )

    if status_code == 404:
        return NotFoundError(
            detail, status_code=status_code, code=code, type=type, instance=instance
        )

    if status_code == 409:
        return ConflictError(
            detail, status_code=status_code, code=code, type=type, instance=instance
        )

    if status_code == 429:
        if code == "QUOTA_EXCEEDED" or (
            body and body.get("code") == "QUOTA_EXCEEDED"
        ):
            return QuotaExceededError(
                detail,
                status_code=status_code,
                code=code,
                type=type,
                instance=instance,
                dimension=str(body.get("dimension", "")) if body else "",
                current=int(body.get("current", 0)) if body else 0,
                limit=int(body.get("limit", 0)) if body else 0,
                period=str(body.get("period", "")) if body else "",
                upgrade_url=str(body.get("upgrade_url", "")) if body else "",
            )
        return RateLimitError(
            detail, status_code=status_code, code=code, type=type, instance=instance
        )

    if status_code >= 500:
        return ServerError(
            detail, status_code=status_code, code=code, type=type, instance=instance
        )

    return SikaError(
        detail, status_code=status_code, code=code, type=type, instance=instance
    )
