from typing import Callable, Optional, Type, Union, Any
from fastapi import HTTPException, FastAPI
from fastapi.exceptions import RequestValidationError
from starlette.responses import JSONResponse
from starlette.exceptions import HTTPException as StarletteHTTPException
from pydantic import BaseModel

from tonilib.fastapi.exception.validation_converter import (
    convert_validation_errors,
    ValidationConvertedError,
)
from tonilib.fastapi.models import Failure, FailureData, ApiResponse


Jsonable = Union[BaseModel, dict]


def _to_dict(x: Jsonable) -> dict:
    return x.model_dump() if isinstance(x, BaseModel) else x


def setup_tonilib_exception_handler(
    app: FastAPI,
    *,
    response_model: Type[BaseModel] = ApiResponse,
    error_field: str = "error",
    response_factory: Optional[Callable[[FailureData, int], Jsonable]] = None,
) -> None:

    def build_response(failure_data: FailureData, status_code: int) -> dict:
        if response_factory is not None:
            return _to_dict(response_factory(failure_data, status_code))
        # fallback: costruisce con response_model(**{error_field: failure_data})
        payload: dict[str, Any] = {error_field: failure_data}
        return _to_dict(response_model(**payload))

    async def http_exception_handler(request, exc: HTTPException):
        failure = Failure(title="HttpException", msg=str(exc.detail)).failure_data
        return JSONResponse(build_response(failure, exc.status_code), status_code=exc.status_code)

    async def http_starlette_exception_handler(request, exc: StarletteHTTPException):
        failure = Failure(title="HttpException", msg=str(exc.detail)).failure_data
        return JSONResponse(build_response(failure, exc.status_code), status_code=exc.status_code)

    async def validation_exception_handler(request, exc: RequestValidationError):
        errs = convert_validation_errors(exc)
        err = errs[0] if errs else ValidationConvertedError(type="validation_error", loc="", msg=str(exc))
        # titolo = tipo, messaggio = dettaglio umano; loc dentro FailureData.code per non perdere info
        failure = Failure(title=err.type, msg=err.msg, code=err.loc).failure_data
        return JSONResponse(build_response(failure, 400), status_code=400)

    async def generic_exception_handler(request, exc: Exception):
        failure = Failure(title="Error", msg=str(exc)).failure_data
        return JSONResponse(build_response(failure, 500), status_code=500)

    async def failure_exception_handler(request, exc: Failure):
        return JSONResponse(build_response(exc.failure_data, 500), status_code=500)

    app.add_exception_handler(HTTPException, http_exception_handler)
    app.add_exception_handler(StarletteHTTPException, http_starlette_exception_handler)
    app.add_exception_handler(RequestValidationError, validation_exception_handler)
    app.add_exception_handler(Exception, generic_exception_handler)
    app.add_exception_handler(Failure, failure_exception_handler)