import http
from collections.abc import Awaitable, Callable
from typing import ClassVar, Self

from fastapi import Request, Response
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse, PlainTextResponse
from pydantic import ValidationError
from starlette.exceptions import HTTPException

from ..exceptions import (
    AuthorizationError,
    DoesNotExistError,
    DomainValidationError,
    NotAllowedError,
)
from ..schema import Error, InvalidParam

type ExceptionHandler[E: Exception] = Callable[[Request, E], Response | Awaitable[Response]]


class ExceptionHandlerInfo:
    default_response_cls: ClassVar[type[Response]] = JSONResponse
    default_error_schema_cls: ClassVar[type[Error]] = Error

    def __init__(
        self, exc_class_or_status_code: int | type[Exception], handler: ExceptionHandler
    ) -> None:
        self.exc_class_or_status_code = exc_class_or_status_code
        self.handler = handler

    @classmethod
    def for_status_code(
        cls,
        status_code: http.HTTPStatus,
        error_type: str | None = None,
        response_cls: type[Response] | None = None,
        error_schema_cls: type[Error] | None = None,
    ) -> Self:
        return cls(
            exc_class_or_status_code=status_code,
            handler=create_exception_handler(
                status_code=status_code,
                error_type=error_type or status_code.name.lower(),
                response_cls=response_cls or cls.default_response_cls,
                error_schema_cls=error_schema_cls or cls.default_error_schema_cls,
            ),
        )

    @classmethod
    def for_unauthorized_error(
        cls,
        response_cls: type[Response] | None = None,
        error_schema_cls: type[Error] | None = None,
    ) -> Self:
        return cls(
            exc_class_or_status_code=http.HTTPStatus.UNAUTHORIZED,
            handler=create_unauthorized_error_handler(
                response_cls=response_cls or cls.default_response_cls,
                error_schema_cls=error_schema_cls or cls.default_error_schema_cls,
            ),
        )

    @classmethod
    def for_exc(
        cls,
        exc_cls: type[Exception],
        status_code: int,
        error_type: str | None = None,
        response_cls: type[Response] | None = None,
        error_schema_cls: type[Error] | None = None,
    ) -> Self:
        return cls(
            exc_class_or_status_code=exc_cls,
            handler=create_exception_handler(
                status_code=status_code,
                error_type=error_type or http.HTTPStatus(status_code).name.lower(),
                response_cls=response_cls or cls.default_response_cls,
                error_schema_cls=error_schema_cls or cls.default_error_schema_cls,
            ),
        )

    @classmethod
    def default(
        cls,
        error_schema_cls: type[Error] | None = None,
    ) -> Self:
        return cls(
            exc_class_or_status_code=Exception,
            handler=create_exception_handler(
                status_code=http.HTTPStatus.INTERNAL_SERVER_ERROR,
                error_type=http.HTTPStatus.INTERNAL_SERVER_ERROR.name.lower(),
                response_cls=cls.default_response_cls,
                error_schema_cls=error_schema_cls or cls.default_error_schema_cls,
            ),
        )

    @classmethod
    def for_http_exception(
        cls,
        response_cls: type[Response] | None = None,
        error_schema_cls: type[Error] | None = None,
    ) -> Self:
        return cls(
            exc_class_or_status_code=HTTPException,
            handler=create_http_exception_handler(
                response_cls=response_cls or cls.default_response_cls,
                error_schema_cls=error_schema_cls or cls.default_error_schema_cls,
            ),
        )

    @classmethod
    def for_request_validation_error(
        cls,
        exception_handler: ExceptionHandler | None = None,
        response_cls: type[Response] | None = None,
        error_schema_cls: type[Error] | None = None,
    ) -> Self:
        return cls(
            exc_class_or_status_code=RequestValidationError,
            handler=(
                exception_handler
                or create_default_validation_error_handler(
                    response_cls=response_cls or cls.default_response_cls,
                    error_schema_cls=error_schema_cls or cls.default_error_schema_cls,
                )
            ),
        )

    @classmethod
    def for_pydantic_validation_error(
        cls,
        exception_handler: ExceptionHandler | None = None,
        response_cls: type[Response] | None = None,
        error_schema_cls: type[Error] | None = None,
    ) -> Self:
        return cls(
            exc_class_or_status_code=ValidationError,
            handler=(
                exception_handler
                or create_default_validation_error_handler(
                    response_cls=response_cls or cls.default_response_cls,
                    error_schema_cls=error_schema_cls or cls.default_error_schema_cls,
                )
            ),
        )


def default_exception_handlers(
    *extras: *tuple[ExceptionHandlerInfo, ...],
    handler_info_cls: type[ExceptionHandlerInfo] = ExceptionHandlerInfo,
) -> tuple[ExceptionHandlerInfo, ...]:
    return (
        handler_info_cls.for_exc(DoesNotExistError, http.HTTPStatus.NOT_FOUND),
        handler_info_cls.for_exc(NotAllowedError, http.HTTPStatus.FORBIDDEN),
        handler_info_cls.for_exc(DomainValidationError, http.HTTPStatus.UNPROCESSABLE_ENTITY),
        handler_info_cls.for_exc(AuthorizationError, http.HTTPStatus.UNAUTHORIZED),
        *extras,
        handler_info_cls.for_request_validation_error(),
        handler_info_cls.for_pydantic_validation_error(),
        handler_info_cls.default(),
    )


def create_exception_handler(
    status_code: int,
    error_type: str,
    response_cls: type[Response],
    error_schema_cls: type[Error] = Error,
) -> ExceptionHandler:
    def exception_handler(request: Request, exc: Exception) -> Response:
        response = exc_to_error_schema(
            exc=exc, error_type=error_type, error_schema_cls=error_schema_cls
        )
        return response_cls(content=jsonable_encoder(response), status_code=status_code)

    return exception_handler


def create_unauthorized_error_handler(
    response_cls: type[Response],
    error_schema_cls: type[Error] = Error,
) -> ExceptionHandler:
    def exception_handler(request: Request, exc: Exception) -> Response:
        if (
            isinstance(exc, HTTPException)
            and exc.headers is not None
            and exc.headers.get("WWW-Authenticate", "").lower().startswith("basic")
        ):
            return PlainTextResponse(
                content=exc.detail,
                status_code=exc.status_code,
                headers=exc.headers,
            )

        response = exc_to_error_schema(
            exc=exc,
            error_type=(status_code := http.HTTPStatus.UNAUTHORIZED).name.lower(),
            error_schema_cls=error_schema_cls,
        )
        return response_cls(content=jsonable_encoder(response), status_code=status_code)

    return exception_handler


def exc_to_error_schema[T: Error](
    exc: Exception,
    error_type: str,
    error_schema_cls: type[T],
) -> T:
    return error_schema_cls(
        title=str(exc),
        type=error_type,
        detail=getattr(exc, "detail", None),
        invalid_params=(
            (invalid_params := getattr(exc, "invalid_params", None))
            and [InvalidParam.model_validate(obj=invalid_param) for invalid_param in invalid_params]
        ),
    )


def create_http_exception_handler(
    response_cls: type[Response],
    error_schema_cls: type[Error] = Error,
) -> ExceptionHandler:
    def http_exception_handler(request: Request, exc: HTTPException) -> Response:
        status_code = http.HTTPStatus(exc.status_code)
        return response_cls(
            content=jsonable_encoder(
                error_schema_cls(
                    title=exc.detail,
                    type=status_code.name.lower(),
                )
            ),
            status_code=status_code,
            headers=exc.headers,
        )

    return http_exception_handler


def create_default_validation_error_handler(
    response_cls: type[Response],
    error_schema_cls: type[Error] = Error,
) -> ExceptionHandler:
    def validation_error_handler(
        request: Request, exc: RequestValidationError | ValidationError
    ) -> Response:
        return response_cls(
            content=jsonable_encoder(
                error_schema_cls.from_validation_error(exc=exc, title="Validation failed"),
            ),
            status_code=http.HTTPStatus.UNPROCESSABLE_ENTITY,
        )

    return validation_error_handler
