import http
from typing import Any

from pydantic import BaseModel, ConfigDict, Field, ValidationError, field_validator
from pydantic_core import InitErrorDetails
from pydantic_core.core_schema import ValidationInfo
from starlette.requests import Request
from starlette.responses import JSONResponse

import fbnconfig
from fbnconfig.exceptions import NotFoundError
from fbnconfig.resource_abc import RESOURCE_REGISTRY, Ref, Resource


async def get_metadata(_: Request):
    def is_resource(entry: dict) -> bool:
        cls = entry["class"]
        return issubclass(cls, Resource)

    def is_ref(entry: dict) -> bool:
        cls = entry["class"]
        return issubclass(cls, Ref)

    def has_method(cls: type, method_name: str) -> bool:
        return callable(getattr(cls, method_name, None))

    def has_ref(cls: type) -> bool:
        ref_name = cls.__name__.removesuffix("Resource") + "Ref"
        return ref_name in RESOURCE_REGISTRY and is_ref(RESOURCE_REGISTRY[ref_name])

    def get_resource_metadata(entry: dict) -> dict:
        cls = entry["class"]
        group = entry["group"]
        parent = entry["parent"]
        keys = entry["keys"]
        return {
            "resource_type": cls.resource_type,
            "group": group,
            "parent": parent.__name__ if parent else None,
            "list_keys": parent.keys if parent else [],
            "fetch_keys": keys,
            "filterable_columns": cls.filterable_fields,
            "supports_list": has_method(cls, "list"),
            "supports_fetch": has_method(cls, "fetch"),
            "supports_ref": has_ref(cls)
        }

    return JSONResponse([
        get_resource_metadata(entry)
        for entry in RESOURCE_REGISTRY.values()
        if is_resource(entry)
    ])


async def get_list(request: Request):
    class ListRequestParams(BaseModel):
        resource: str = Field(pattern=r"^[A-Za-z]+$")
        limit: int | None = Field(50, ge=1)
        page: str | None = None
        filter: str | None = None
        keys: dict[str, Any] | None = None
        model_config = ConfigDict(extra="ignore")

    try:
        body = await request.json()
        request_params = ListRequestParams(**(body | request.path_params))
    except ValidationError as e:
        return JSONResponse(
            status_code=http.HTTPStatus.UNPROCESSABLE_ENTITY,
            content=e.errors(),
        )

    resource = get_resource(request_params.resource)
    resource_class = resource["class"]
    if not has_method(resource_class, "list"):
        return JSONResponse(
            status_code=http.HTTPStatus.NOT_IMPLEMENTED,
            content={"detail": "Method not implemented for this resource"}
        )

    client = fbnconfig.create_client(request.state.lusid_env, request.state.token, is_async=True)
    data = await resource_class.list(
        client=client,
        query_params=request_params.model_dump(exclude_none=True, exclude={"resource", "keys"}),
        path_params=request_params.keys
    )

    return JSONResponse(data)


async def get_fetch(request: Request):
    class FetchRequestParams(BaseModel):
        resource: str = Field(pattern=r"^[A-Za-z]+$")

        model_config = ConfigDict(extra="ignore")

    class ResourceKeyParams(BaseModel):
        keys: dict[str, Any]

        @field_validator("keys")
        @classmethod
        def check_mandatory_keys(cls, keys: dict, info: ValidationInfo):
            if info.context is None or "mandatory_keys" not in info.context:
                raise ValueError("Validation context is missing")

            mandatory_keys = set(info.context["mandatory_keys"])
            provided_keys = set(keys.keys())

            missing_keys = mandatory_keys - provided_keys
            extra_keys = provided_keys - mandatory_keys

            errors = []

            for key in missing_keys:
                errors.append(
                    InitErrorDetails(type="missing", loc=(key,), input=keys,)
                )

            for key in extra_keys:
                errors.append(
                    InitErrorDetails(type="extra_forbidden", loc=(key,), input=key,)
                )

            if errors:
                raise ValidationError.from_exception_data(cls.__name__, errors)

            return keys

    try:
        request_params = FetchRequestParams(**request.path_params)
    except ValidationError as e:
        return JSONResponse(
            status_code=http.HTTPStatus.UNPROCESSABLE_ENTITY,
            content=e.errors(),
        )

    resource = get_resource(request_params.resource)
    resource_class = resource["class"]
    if not has_method(resource_class, "fetch"):
        return JSONResponse(
            status_code=http.HTTPStatus.NOT_IMPLEMENTED,
            content={"detail": "Method not implemented for this resource"}
        )

    try:
        body = await request.json()
        params = ResourceKeyParams.model_validate(
            body, context={"mandatory_keys": resource["keys"]}
        )
    except ValidationError as e:
        return JSONResponse(
            status_code=http.HTTPStatus.UNPROCESSABLE_ENTITY,
            content=e.errors(),
        )

    client = fbnconfig.create_client(request.state.lusid_env, request.state.token, is_async=True)
    data = await resource_class.fetch(client=client, keys=params.keys)

    return JSONResponse(data)


def get_resource(resource_type: str) -> dict[str, Any]:
    """
    Get the resource entry from the registry by its resource_type.
    """
    resource_object = RESOURCE_REGISTRY.get(resource_type)
    if resource_object is None:
        raise NotFoundError("resource")
    return resource_object


def has_method(cls: type, method_name: str) -> bool:
    return callable(getattr(cls, method_name, None))
