import json

from typing import Any, Callable

from flask import Flask, request

from nsj_rest_lib.settings import APP_NAME, get_logger

from nsj_gcf_utils.rest_error_util import format_json_error

from nsj_multi_database_lib.decorator.multi_database import multi_database

from nsj_rest_lib.controller.controller_util import DEFAULT_RESP_HEADERS
from nsj_rest_lib.controller.delete_route import DeleteRoute
from nsj_rest_lib.controller.get_route import GetRoute
from nsj_rest_lib.controller.list_route import ListRoute
from nsj_rest_lib.controller.patch_route import PatchRoute
from nsj_rest_lib.controller.post_route import PostRoute
from nsj_rest_lib.controller.put_route import PutRoute

from nsj_rest_lib2.exception import (
    MissingDependencyConfigException,
    MissingEntityConfigException,
)
from nsj_rest_lib2.service.entity_loader import EntityLoader
from nsj_rest_lib2.util.entity_name_util import format_entity_name

logger = get_logger()


def _get_query_args() -> tuple[str | None, str | None, bool]:
    """
    Le tenant/grupo_empresarial/force_reload da query string e do body.

    Returns:
        Tupla com:
            - tenant (str | None)
            - grupo_empresarial (str | None)
            - force_reload (bool)
    """
    # Tentando ler do query args
    query_args = request.args
    tenant = query_args.get("tenant")
    grupo_empresarial = query_args.get("grupo_empresarial")
    force_reload = query_args.get("force_reload", "false").lower() == "true"

    # Tenta ler do corpo da requisicao quando nao vier por query string.
    try:
        body_str = request.data.decode("utf-8")
        body_json = json.loads(body_str)

        if not tenant:
            tenant = body_json.get("tenant")
        if not grupo_empresarial:
            grupo_empresarial = body_json.get("grupo_empresarial")
    except:
        pass

    # Preserva None quando nao houver dado para evitar "None" como string.
    tenant_value = str(tenant) if tenant is not None else None
    grupo_value = str(grupo_empresarial) if grupo_empresarial is not None else None
    return (tenant_value, grupo_value, force_reload)


def _endpoint_name(func: Any, multidb: bool, root: str) -> str:
    suffix = "_mb" if multidb else ""
    return f"{root}_{func.__name__}{suffix}"


def _route_handle(route: Any, *args: Any, **kwargs: Any) -> Any:
    if hasattr(route, "internal_handle_request"):
        return route.internal_handle_request(*args, **kwargs)
    return route.handle_request(*args, **kwargs)


def setup_dynamic_routes(
    flask_app: Flask,
    multidb: bool = True,
    dynamic_root_path: str = "edl1",
    injector_factory: Any = None,
    escopo_in_url: bool = False,
    edls_path: str | None = None,
    on_missing_entity_config: (
        Callable[[str, str, str | None, str | None], bool] | None
    ) = None,
    before_dynamic_request: (
        Callable[[dict[str, Any]], tuple[Any, int, dict[str, Any]] | None] | None
    ) = None,
) -> None:
    """
    Registra rotas dinamicas (GET/POST/PUT/PATCH/DELETE) para EDLs carregados.

    Args:
        flask_app: Instancia Flask onde as rotas serao registradas.
        multidb: Define se aplicara decorator de multi database por service account.
        dynamic_root_path: Prefixo de rota dinamica.
        injector_factory: Fabrica de injecao usada pelas rotas.
        escopo_in_url: Quando True, exige escopo na URL dinamica.
        edls_path: Caminho local de EDLs (fallback/disco).
        on_missing_entity_config: Callback para tentativa de recuperar/compilar
            config quando nao encontrada em cache/redis.
        before_dynamic_request: Callback executado antes da carga dinamica.
            Se retornar resposta HTTP, interrompe o fluxo da rota.

    Returns:
        None.
    """

    if not escopo_in_url:
        COLLECTION_DYNAMIC_ROUTE = f"/{APP_NAME}/{dynamic_root_path}/<entity_resource>"
        ONE_DYNAMIC_ROUTE = f"/{APP_NAME}/{dynamic_root_path}/<entity_resource>/<id>"
    else:
        COLLECTION_DYNAMIC_ROUTE = (
            f"/{APP_NAME}/{dynamic_root_path}/<entity_escopo>/<entity_resource>"
        )
        ONE_DYNAMIC_ROUTE = (
            f"/{APP_NAME}/{dynamic_root_path}/<entity_escopo>/<entity_resource>/<id>"
        )

    entity_loader = EntityLoader(edls_path=edls_path)

    def _dynamic_route_wrapper(
        route_builder: Callable[[tuple[Any, ...]], Callable[..., Any]],
        endpoint_suffix: str | None = None,
    ) -> Callable[..., Any]:
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            if "entity_resource" not in kwargs:
                msg = "Faltando parametro identificador da entidade na URL."
                return (format_json_error(msg), 400, {**DEFAULT_RESP_HEADERS})
            entity_resource = kwargs.pop("entity_resource")

            entity_escopo = kwargs.pop("entity_escopo", "")

            tenant, grupo_empresarial, force_reload = _get_query_args()
            entity_id = kwargs.get("id")

            if before_dynamic_request:
                # Permite bloquear o request dinamico antes do load/execucao da rota.
                guard_response = before_dynamic_request(
                    {
                        "method": request.method,
                        "path": request.path,
                        "host_url": request.host_url,
                        "entity_escopo": entity_escopo,
                        "entity_resource": entity_resource,
                        "entity_id": entity_id,
                        "tenant": tenant,
                        "grupo_empresarial": grupo_empresarial,
                        "force_reload": force_reload,
                    }
                )
                if guard_response is not None:
                    return guard_response

            try:
                entity_config = entity_loader.load_entity_source(
                    entity_resource,
                    tenant,
                    grupo_empresarial,
                    escopo=entity_escopo,
                    force_reload=force_reload,
                )
            except MissingEntityConfigException:
                recovered = False
                if on_missing_entity_config:
                    try:
                        recovered = bool(
                            on_missing_entity_config(
                                entity_resource,
                                entity_escopo,
                                tenant,
                                grupo_empresarial,
                            )
                        )
                    except Exception as exc:
                        logger.warning(
                            "Falha ao executar fallback de compilacao da entidade "
                            f"'{entity_escopo}/{entity_resource}': {exc}",
                            exc_info=True,
                        )
                if recovered:
                    try:
                        entity_config = entity_loader.load_entity_source(
                            entity_resource,
                            tenant,
                            grupo_empresarial,
                            escopo=entity_escopo,
                            force_reload=True,
                        )
                    except MissingEntityConfigException:
                        msg = (
                            f"Configuracao da entidade '{entity_resource}' nao encontrada."
                        )
                        return (
                            format_json_error(msg),
                            412,
                            {**DEFAULT_RESP_HEADERS},
                        )
                else:
                    msg = (
                        f"Configuracao da entidade '{entity_resource}' nao encontrada."
                    )
                    return (format_json_error(msg), 412, {**DEFAULT_RESP_HEADERS})
            except MissingDependencyConfigException as exc:
                # Mensagem funcional para diferenciar falta da entidade raiz
                # de falta de uma dependencia transitiva.
                entity_name = format_entity_name(exc.entity_resource)
                dependency_name = exc.dependency_key
                msg = (
                    f'Ocorreu um erro ao processar a entidade "{entity_name}" '
                    f'devido a ausencia de uma dependencia: "{dependency_name}"'
                )
                return (format_json_error(msg), 412, {**DEFAULT_RESP_HEADERS})

            (
                _,
                _,
                _,
                _,
                _,
                service_account,
                *rest,
            ) = entity_config

            route_handler = route_builder(entity_config)

            if multidb:
                route_handler = multi_database(service_account)(route_handler)

            return route_handler(*args, **kwargs)

        wrapper.__name__ = endpoint_suffix or f"{route_builder.__name__}_wrapper"
        return wrapper

    def list_dynamic_builder(entity_config: tuple[Any, ...]) -> Callable[..., Any]:
        (
            dto_class_name,
            entity_class_name,
            etities_dict,
            api_expose,
            api_verbs,
            _service_account,
            _insert_function_class_name,
            _update_function_class_name,
            _insert_function_name,
            _update_function_name,
            _get_function_name,
            list_function_name,
            _delete_function_name,
            _get_function_type_class_name,
            list_function_type_class_name,
            _delete_function_type_class_name,
            *_,
            _custom_json_get_response,
            custom_json_list_response,
            _custom_json_delete_response,
        ) = entity_config

        def list_dynamic(*args: Any, **kwargs: Any) -> Any:
            if not api_expose or "GET" not in api_verbs:
                return ("", 405, {})

            route = ListRoute(
                url=COLLECTION_DYNAMIC_ROUTE,
                http_method="GET",
                dto_class=etities_dict[dto_class_name],
                entity_class=etities_dict[entity_class_name],
                injector_factory=injector_factory,
                list_function_name=list_function_name,
                list_function_type_class=etities_dict.get(
                    list_function_type_class_name
                ),
                custom_json_response=bool(custom_json_list_response),
            )

            return _route_handle(route, *args, **kwargs)

        return list_dynamic

    def get_dynamic_builder(entity_config: tuple[Any, ...]) -> Callable[..., Any]:
        (
            dto_class_name,
            entity_class_name,
            etities_dict,
            api_expose,
            api_verbs,
            _service_account,
            _insert_function_class_name,
            _update_function_class_name,
            _insert_function_name,
            _update_function_name,
            get_function_name,
            _list_function_name,
            _delete_function_name,
            get_function_type_class_name,
            _list_function_type_class_name,
            _delete_function_type_class_name,
            *_,
            custom_json_get_response,
            _custom_json_list_response,
            _custom_json_delete_response,
        ) = entity_config

        def get_dynamic(*args: Any, **kwargs: Any) -> Any:
            if not api_expose or "GET" not in api_verbs:
                return ("", 405, {})

            route = GetRoute(
                url=ONE_DYNAMIC_ROUTE,
                http_method="GET",
                dto_class=etities_dict[dto_class_name],
                entity_class=etities_dict[entity_class_name],
                injector_factory=injector_factory,
                get_function_name=get_function_name,
                get_function_type_class=etities_dict.get(get_function_type_class_name),
                custom_json_response=bool(custom_json_get_response),
            )

            return _route_handle(route, *args, **kwargs)

        return get_dynamic

    def post_dynamic_builder(entity_config: tuple[Any, ...]) -> Callable[..., Any]:
        (
            dto_class_name,
            entity_class_name,
            etities_dict,
            api_expose,
            api_verbs,
            _service_account,
            insert_function_class_name,
            _update_function_class_name,
            insert_function_name,
            _update_function_name,
            _get_function_name,
            _list_function_name,
            _delete_function_name,
            _get_function_type_class_name,
            _list_function_type_class_name,
            _delete_function_type_class_name,
            retrieve_after_insert,
            _retrieve_after_update,
            _retrieve_after_partial_update,
            post_response_dto_class_name,
            _put_response_dto_class_name,
            _patch_response_dto_class_name,
            custom_json_post_response,
            _custom_json_put_response,
            _custom_json_patch_response,
            _custom_json_get_response,
            _custom_json_list_response,
            _custom_json_delete_response,
        ) = entity_config

        def post_dynamic(*args: Any, **kwargs: Any) -> Any:
            if not api_expose or "POST" not in api_verbs:
                return ("", 405, {})

            insert_function_type_class = (
                etities_dict.get(insert_function_class_name)
                if insert_function_class_name
                else None
            )

            dto_response_class = (
                etities_dict.get(post_response_dto_class_name)
                if post_response_dto_class_name
                else None
            )

            route = PostRoute(
                url=COLLECTION_DYNAMIC_ROUTE,
                http_method="POST",
                dto_class=etities_dict[dto_class_name],
                entity_class=etities_dict[entity_class_name],
                injector_factory=injector_factory,
                insert_function_type_class=insert_function_type_class,
                insert_function_name=insert_function_name,
                dto_response_class=dto_response_class,
                retrieve_after_insert=retrieve_after_insert,
                custom_json_response=bool(custom_json_post_response),
            )

            return _route_handle(route, *args, **kwargs)

        return post_dynamic

    def put_dynamic_builder(entity_config: tuple[Any, ...]) -> Callable[..., Any]:
        (
            dto_class_name,
            entity_class_name,
            etities_dict,
            api_expose,
            api_verbs,
            _service_account,
            _insert_function_class_name,
            update_function_class_name,
            _insert_function_name,
            update_function_name,
            _get_function_name,
            _list_function_name,
            _delete_function_name,
            _get_function_type_class_name,
            _list_function_type_class_name,
            _delete_function_type_class_name,
            _retrieve_after_insert,
            retrieve_after_update,
            _retrieve_after_partial_update,
            _post_response_dto_class_name,
            put_response_dto_class_name,
            _patch_response_dto_class_name,
            _custom_json_post_response,
            custom_json_put_response,
            _custom_json_patch_response,
            _custom_json_get_response,
            _custom_json_list_response,
            _custom_json_delete_response,
        ) = entity_config

        dto_response_class = (
            etities_dict.get(put_response_dto_class_name)
            if put_response_dto_class_name
            else None
        )

        def put_dynamic(*args: Any, **kwargs: Any) -> Any:
            if not api_expose or "PUT" not in api_verbs:
                return ("", 405, {})

            update_function_type_class = (
                etities_dict.get(update_function_class_name)
                if update_function_class_name
                else None
            )

            route = PutRoute(
                url=ONE_DYNAMIC_ROUTE,
                http_method="PUT",
                dto_class=etities_dict[dto_class_name],
                entity_class=etities_dict[entity_class_name],
                injector_factory=injector_factory,
                update_function_type_class=update_function_type_class,
                update_function_name=update_function_name,
                dto_response_class=dto_response_class,
                retrieve_after_update=retrieve_after_update,
                custom_json_response=bool(custom_json_put_response),
            )

            return _route_handle(route, *args, **kwargs)

        return put_dynamic

    def patch_dynamic_builder(entity_config: tuple[Any, ...]) -> Callable[..., Any]:
        (
            dto_class_name,
            entity_class_name,
            etities_dict,
            api_expose,
            api_verbs,
            _service_account,
            _insert_function_class_name,
            _update_function_class_name,
            _insert_function_name,
            _update_function_name,
            _get_function_name,
            _list_function_name,
            _delete_function_name,
            _get_function_type_class_name,
            _list_function_type_class_name,
            _delete_function_type_class_name,
            _retrieve_after_insert,
            _retrieve_after_update,
            retrieve_after_partial_update,
            _post_response_dto_class_name,
            _put_response_dto_class_name,
            patch_response_dto_class_name,
            _custom_json_post_response,
            _custom_json_put_response,
            custom_json_patch_response,
            _custom_json_get_response,
            _custom_json_list_response,
            _custom_json_delete_response,
        ) = entity_config

        def patch_dynamic(*args: Any, **kwargs: Any) -> Any:
            if not api_expose or "PATCH" not in api_verbs:
                return ("", 405, {})

            dto_response_class = (
                etities_dict.get(patch_response_dto_class_name)
                if patch_response_dto_class_name
                else None
            )

            route = PatchRoute(
                url=ONE_DYNAMIC_ROUTE,
                http_method="PATCH",
                dto_class=etities_dict[dto_class_name],
                entity_class=etities_dict[entity_class_name],
                injector_factory=injector_factory,
                dto_response_class=dto_response_class,
                retrieve_after_partial_update=retrieve_after_partial_update,
                custom_json_response=bool(custom_json_patch_response),
            )

            return _route_handle(route, *args, **kwargs)

        return patch_dynamic

    def delete_dynamic_builder(entity_config: tuple[Any, ...]) -> Callable[..., Any]:
        (
            dto_class_name,
            entity_class_name,
            etities_dict,
            api_expose,
            api_verbs,
            _service_account,
            _insert_function_class_name,
            _update_function_class_name,
            _insert_function_name,
            _update_function_name,
            _get_function_name,
            _list_function_name,
            delete_function_name,
            _get_function_type_class_name,
            _list_function_type_class_name,
            delete_function_type_class_name,
            *_,
            _custom_json_get_response,
            _custom_json_list_response,
            custom_json_delete_response,
        ) = entity_config

        def delete_dynamic(*args: Any, **kwargs: Any) -> Any:
            if not api_expose or "DELETE" not in api_verbs:
                return ("", 405, {})

            route = DeleteRoute(
                url=ONE_DYNAMIC_ROUTE,
                http_method="DELETE",
                dto_class=etities_dict[dto_class_name],
                entity_class=etities_dict[entity_class_name],
                injector_factory=injector_factory,
                delete_function_name=delete_function_name,
                delete_function_type_class=etities_dict.get(
                    delete_function_type_class_name
                ),
                custom_json_response=bool(custom_json_delete_response),
            )

            return _route_handle(route, *args, **kwargs)

        return delete_dynamic

    list_dynamic = _dynamic_route_wrapper(
        list_dynamic_builder, endpoint_suffix="list_dynamic"
    )
    get_dynamic = _dynamic_route_wrapper(
        get_dynamic_builder, endpoint_suffix="get_dynamic"
    )
    post_dynamic = _dynamic_route_wrapper(
        post_dynamic_builder, endpoint_suffix="post_dynamic"
    )
    put_dynamic = _dynamic_route_wrapper(
        put_dynamic_builder, endpoint_suffix="put_dynamic"
    )
    patch_dynamic = _dynamic_route_wrapper(
        patch_dynamic_builder, endpoint_suffix="patch_dynamic"
    )
    delete_dynamic = _dynamic_route_wrapper(
        delete_dynamic_builder, endpoint_suffix="delete_dynamic"
    )

    # Registrando as rotas no flask
    flask_app.add_url_rule(
        COLLECTION_DYNAMIC_ROUTE,
        endpoint=_endpoint_name(list_dynamic, multidb, dynamic_root_path),
        view_func=list_dynamic,
        methods=["GET"],
    )
    flask_app.add_url_rule(
        ONE_DYNAMIC_ROUTE,
        endpoint=_endpoint_name(get_dynamic, multidb, dynamic_root_path),
        view_func=get_dynamic,
        methods=["GET"],
    )
    flask_app.add_url_rule(
        COLLECTION_DYNAMIC_ROUTE,
        endpoint=_endpoint_name(post_dynamic, multidb, dynamic_root_path),
        view_func=post_dynamic,
        methods=["POST"],
    )
    flask_app.add_url_rule(
        ONE_DYNAMIC_ROUTE,
        endpoint=_endpoint_name(put_dynamic, multidb, dynamic_root_path),
        view_func=put_dynamic,
        methods=["PUT"],
    )
    flask_app.add_url_rule(
        ONE_DYNAMIC_ROUTE,
        endpoint=_endpoint_name(patch_dynamic, multidb, dynamic_root_path),
        view_func=patch_dynamic,
        methods=["PATCH"],
    )
    flask_app.add_url_rule(
        ONE_DYNAMIC_ROUTE,
        endpoint=_endpoint_name(delete_dynamic, multidb, dynamic_root_path),
        view_func=delete_dynamic,
        methods=["DELETE"],
    )
