"""Maps domain exceptions to HTTP responses. The only place that knows both.

Two surfaces share this mapping: the native ``/api/v1`` API answers with a JSON
error body and an HTTP status code, while the Subsonic ``/rest`` layer answers
with its own envelope and **always HTTP 200** (the status lives in the body). A
request is routed to the Subsonic renderer by path prefix.
"""

from fastapi import FastAPI, Request, Response, status
from fastapi.responses import JSONResponse

from app.api.rest.envelope import subsonic_error
from app.core.logging import get_logger
from app.domain.errors import (
    AlreadyExistsError,
    AuthenticationError,
    ConflictError,
    DependencyUnavailableError,
    DomainError,
    NotFoundError,
    PermissionDeniedError,
    RangeNotSatisfiableError,
    StorageError,
    ValidationError,
)

log = get_logger(__name__)

_STATUS_BY_ERROR: dict[type[DomainError], int] = {
    NotFoundError: status.HTTP_404_NOT_FOUND,
    AlreadyExistsError: status.HTTP_409_CONFLICT,
    ConflictError: status.HTTP_409_CONFLICT,
    ValidationError: status.HTTP_422_UNPROCESSABLE_CONTENT,
    AuthenticationError: status.HTTP_401_UNAUTHORIZED,
    PermissionDeniedError: status.HTTP_403_FORBIDDEN,
    DependencyUnavailableError: status.HTTP_503_SERVICE_UNAVAILABLE,
    StorageError: status.HTTP_500_INTERNAL_SERVER_ERROR,
}

# Subsonic error codes (subsonic.org/restapi): 10 missing param, 40 wrong
# credentials, 50 not authorized, 70 not found, 0 generic.
_SUBSONIC_CODE_BY_ERROR: dict[type[DomainError], int] = {
    ValidationError: 10,
    AuthenticationError: 40,
    PermissionDeniedError: 50,
    NotFoundError: 70,
}

_SUBSONIC_PREFIX = "/rest"


def _is_subsonic(request: Request) -> bool:
    return request.url.path.startswith(_SUBSONIC_PREFIX)


def _error_body(code: str, message: str) -> dict[str, dict[str, str]]:
    return {"error": {"code": code, "message": message}}


def register_exception_handlers(app: FastAPI) -> None:
    @app.exception_handler(RangeNotSatisfiableError)
    async def _handle_range_error(_request: Request, exc: RangeNotSatisfiableError) -> JSONResponse:
        return JSONResponse(
            status_code=status.HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE,
            content=_error_body(exc.code, exc.message),
            headers={"Content-Range": f"bytes */{exc.total_size}"},
        )

    @app.exception_handler(DomainError)
    async def _handle_domain_error(request: Request, exc: DomainError) -> Response:
        if _is_subsonic(request):
            code = _SUBSONIC_CODE_BY_ERROR.get(type(exc), 0)
            return subsonic_error(code, exc.message, fmt=request.query_params.get("f"))
        http_status = _STATUS_BY_ERROR.get(type(exc), status.HTTP_400_BAD_REQUEST)
        return JSONResponse(
            status_code=http_status,
            content=_error_body(exc.code, exc.message),
        )

    @app.exception_handler(Exception)
    async def _handle_unexpected(request: Request, exc: Exception) -> Response:
        log.error("unhandled_exception", exc_info=exc)
        if _is_subsonic(request):
            return subsonic_error(
                0, "An unexpected error occurred.", fmt=request.query_params.get("f")
            )
        return JSONResponse(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            content=_error_body("internal_error", "An unexpected error occurred."),
        )
