"""LLMHosts proxy error handling -- exceptions and format-aware error responses.

Provides a hierarchy of proxy exceptions and utilities to render errors in
either OpenAI or Anthropic wire format depending on the request path.
"""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING, Any

from fastapi.responses import JSONResponse

from llmhosts.constants import ANTHROPIC_MESSAGES_PATH

if TYPE_CHECKING:
    from fastapi import Request

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Exception hierarchy
# ---------------------------------------------------------------------------


class ProxyError(Exception):
    """Base exception for all LLMHosts proxy errors."""

    status_code: int = 500
    error_type: str = "internal_error"

    def __init__(self, message: str, *, status_code: int | None = None, error_type: str | None = None) -> None:
        super().__init__(message)
        self.message = message
        if status_code is not None:
            self.status_code = status_code
        if error_type is not None:
            self.error_type = error_type


class BackendUnavailableError(ProxyError):
    """Raised when no backend is reachable to serve the request."""

    status_code: int = 503
    error_type: str = "backend_unavailable"

    def __init__(self, message: str = "No backend available to serve this request") -> None:
        super().__init__(message)


class BackendTimeoutError(ProxyError):
    """Raised when a backend does not respond in time."""

    status_code: int = 504
    error_type: str = "timeout"

    def __init__(self, message: str = "Backend request timed out") -> None:
        super().__init__(message)


class KarmaTooLowError(ProxyError):
    """Raised when Hive member's karma is too low to consume from pool."""

    status_code: int = 429
    error_type: str = "karma_too_low"
    retry_after_seconds: int | None = None

    def __init__(self, message: str = "Karma too low", *, retry_after_seconds: int | None = None) -> None:
        super().__init__(message)
        self.retry_after_seconds = retry_after_seconds


class InvalidRequestError(ProxyError):
    """Raised for malformed or semantically invalid client requests."""

    status_code: int = 400
    error_type: str = "invalid_request_error"

    def __init__(self, message: str = "The request was invalid") -> None:
        super().__init__(message)


class AuthenticationError(ProxyError):
    """Raised when API key validation fails."""

    status_code: int = 401
    error_type: str = "authentication_error"

    def __init__(self, message: str = "Invalid or missing API key") -> None:
        super().__init__(message)


# ---------------------------------------------------------------------------
# OpenAI-format error builder
# ---------------------------------------------------------------------------


def openai_error_body(
    error_type: str, message: str, code: str | None = None, param: str | None = None
) -> dict[str, Any]:
    """Build an OpenAI-compatible error response body.

    Example output::

        {
            "error": {
                "message": "Model not found",
                "type": "invalid_request_error",
                "param": "model",
                "code": "model_not_found"
            }
        }
    """
    return {
        "error": {
            "message": message,
            "type": error_type,
            "param": param,
            "code": code,
        }
    }


# ---------------------------------------------------------------------------
# Anthropic-format error builder
# ---------------------------------------------------------------------------


def anthropic_error_body(error_type: str, message: str) -> dict[str, Any]:
    """Build an Anthropic-compatible error response body.

    Example output::

        {
            "type": "error",
            "error": {
                "type": "invalid_request_error",
                "message": "Model not found"
            }
        }
    """
    return {
        "type": "error",
        "error": {
            "type": error_type,
            "message": message,
        },
    }


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

_ANTHROPIC_ERROR_TYPE_MAP: dict[str, str] = {
    "internal_error": "api_error",
    "backend_unavailable": "overloaded_error",
    "timeout": "overloaded_error",
    "karma_too_low": "overloaded_error",
    "invalid_request_error": "invalid_request_error",
    "authentication_error": "authentication_error",
}


def _is_anthropic_path(path: str) -> bool:
    """Return True if *path* belongs to the Anthropic dialect."""
    return path.rstrip("/").endswith(ANTHROPIC_MESSAGES_PATH.rstrip("/"))


def _build_response(exc: ProxyError, *, anthropic: bool) -> JSONResponse:
    if anthropic:
        mapped_type = _ANTHROPIC_ERROR_TYPE_MAP.get(exc.error_type, "api_error")
        body = anthropic_error_body(mapped_type, exc.message)
    else:
        body = openai_error_body(exc.error_type, exc.message)
    return JSONResponse(status_code=exc.status_code, content=body)


# ---------------------------------------------------------------------------
# FastAPI exception handlers
# ---------------------------------------------------------------------------


async def proxy_error_handler(request: Request, exc: ProxyError) -> JSONResponse:
    """Handle :class:`ProxyError` and subclasses.

    Detects from the request path whether to emit an OpenAI or Anthropic
    error envelope. For :class:`KarmaTooLowError`, adds Retry-After header.
    """
    anthropic = _is_anthropic_path(request.url.path)
    logger.warning("ProxyError %s on %s %s: %s", exc.status_code, request.method, request.url.path, exc.message)
    response = _build_response(exc, anthropic=anthropic)
    retry_after = getattr(exc, "retry_after_seconds", None)
    if exc.status_code == 429 and retry_after is not None:
        response.headers["Retry-After"] = str(retry_after)
    return response


async def generic_error_handler(request: Request, exc: Exception) -> JSONResponse:
    """Catch-all for unhandled exceptions so callers always get structured JSON."""
    anthropic = _is_anthropic_path(request.url.path)
    logger.exception("Unhandled exception on %s %s", request.method, request.url.path)
    wrapped = ProxyError("Internal server error", status_code=500, error_type="internal_error")
    return _build_response(wrapped, anthropic=anthropic)
