import inspect
import re
from typing import Dict, Callable, Type
from pydantic import BaseModel as PdModel

import sanic_ext
from sanic import Sanic
from sanic_ext.extensions.openapi.builders import SpecificationBuilder, OperationStore

from web_foundation.workers.io.http.routers.ext_router import ExtRouter, RouteMethodConf, RouteConf


def get_definition_with_correct_name(definitions: Dict) -> Dict:
    _definitions = {}
    for defin, _schema in definitions.copy().items():
        if "." in defin:
            _definitions[defin.split(".")[-2]] = _schema
        else:
            _definitions[defin] = _schema
    return _definitions


def get_model_schema(model: Type[PdModel]):
    """ Create schema from pydantic model and add all references to OpenAPI schemas """
    schema = model.schema(ref_template="#/components/schemas/{model}")
    components = {}
    if "definitions" in schema:
        definitions = get_definition_with_correct_name(schema.pop("definitions"))
        components.update(definitions)
    components.update({model.__name__: schema})
    spec = SpecificationBuilder()
    for component_name, component in components.items():
        spec._components["schemas"].update({component_name: component})
    return schema


def add_openapi_spec(uri: str, method_name: str, func: Callable, handler: Callable,
                     in_dto: Type[PdModel], out_dto: Type[PdModel]):
    # --- add query args ---#
    func_text = inspect.getsource(func)
    func_query_args = re.findall(r"request.args.get\(\"(\w*)\"\)", func_text)
    if func_query_args:
        for func_arg in func_query_args:
            handler = sanic_ext.openapi.parameter(func_arg)(handler)
    # --- add request body ---#
    if in_dto:
        if issubclass(in_dto, PdModel):
            schema = get_model_schema(in_dto)
            OperationStore()[handler].requestBody = {"content": {"application/json": {'schema': schema}}}
    # --- add response ---#
    if out_dto:
        if issubclass(out_dto, PdModel):
            schema = get_model_schema(out_dto)
            OperationStore()[handler].responses[200] = {"content": {"application/json": {'schema': schema}}}
    # --- add tag ---#
    handler = sanic_ext.openapi.tag(uri.split("/")[1].capitalize())(handler)  # first path word

    # --- decription --- #
    # f = sanic_ext.openapi.description("hujkiikasdf;awerfl;jkasdfklj;asdfjkl;")(handler)
    # f = sanic_ext.openapi.summary("l;kasdjllww")(handler)

    # --- set operation id --- #
    handler = sanic_ext.openapi.operation(f"{method_name}~{uri}")(handler)
    return handler


class DictRouter(ExtRouter):
    _router_conf: Dict
    chaining: Callable
    versioning: bool

    def __init__(self, routes_config: Dict, chaining: Callable, versioning: bool = True):
        super().__init__()
        self.chaining = chaining
        self._router_conf = routes_config
        self.chains = []
        self.versioning = versioning
        self._parse()

    def _parse(self):
        for app_route in self._router_conf.get("apps"):
            version_prefix = app_route.get("version_prefix")
            version_prefix = version_prefix if version_prefix else "/api/v"
            for endpoint, versions in app_route.get("endpoints").items():
                for version, params in versions.items():
                    methods_confs = []
                    endpoint_handler = params.pop("handler", None)
                    endpoint_protector = params.pop("protector", None)
                    endpoint_response_fabric = params.pop("response_fabric", None)
                    for method_name, method_params in params.items():
                        target_func = method_params.get('handler')
                        target_func = target_func if target_func else endpoint_handler
                        protector = method_params.get("protector")
                        protector = protector if protector else endpoint_protector
                        in_dto = method_params.get("in_dto")
                        out_dto = method_params.get("out_dto")
                        response_fabric = method_params.get("response_fabric")
                        response_fabric = response_fabric if response_fabric else endpoint_response_fabric
                        if response_fabric:
                            chain = self.chaining(
                                protector=protector,
                                in_struct=in_dto,
                                response_fabric=response_fabric)(target_func)
                        else:
                            chain = self.chaining(
                                protector=protector,
                                in_struct=in_dto)(target_func)

                        chain = add_openapi_spec(uri=endpoint, method_name=method_name, func=target_func,
                                                 handler=chain, in_dto=in_dto, out_dto=out_dto)

                        methods_confs.append(RouteMethodConf(method_name=method_name,
                                                             protector=protector,
                                                             in_dto=in_dto,
                                                             out_dto=out_dto,
                                                             handler=target_func,
                                                             chain=chain,
                                                             version_prefix=version_prefix,
                                                             version=version
                                                             ))

                    route = RouteConf(app_name=app_route.get("app_name"),
                                      path=endpoint,
                                      methods=methods_confs)
                    self.chains.append(route)

    def apply_routes(self, app: Sanic, **kwargs):
        """
        If you want use chaining, please use partial(chain,validation_fnc=...,response_fabric=...)
        :param **kwargs:
        :param chaining:
        :return:
        """
        for route_conf in self.chains:
            for method_conf in route_conf.methods:
                if self.versioning:
                    app.add_route(uri=route_conf.path, methods={method_conf.method_name.upper()},
                                  handler=method_conf.chain,
                                  version_prefix=method_conf.version_prefix, version=method_conf.version)
                else:
                    app.add_route(uri=route_conf.path, methods={method_conf.method_name.upper()},
                                  handler=method_conf.chain)

        # warn(f"Can't find dto or handler in imported module, suppressed exception: {e.__str__()}")
