import inspect
from enum import Enum
from typing import Optional, Type, Union, Tuple

from httpmessageprocessorutil.automapped import AutoMappedMessageProcessor
from messageprocessor import MessageProcessor
from simplehttprouter import Route

from ._builder_utils import (
    DEFAULT_CONTENT_TYPE,
    DEFAULT_SUCCESS_CODE,
    DEFAULT_RESPONSE_DESCRIPTION,
    OPENAPI_SCALAR_TYPES_DEFINITIONS,
    get_interactor_description,
    get_path_parameters,
)
from .._OpenApiModelBuilder import OpenApiModelBuilder
from ..descriptors import Descriptors
from ..dto import RouteBuildParams, build_params
from ..yaml import build_open_api_yaml
from ...model import (
    InfoModel,
    ObjectSchemaModel,
    OpenapiModel,
    PathContentModel,
    PathModel, PathParameterInEnum,
    PathParameterModel, PathResponseModel, PropertyModel,
    RequestBodyModel, SchemaArrayPropertyModel,
    SchemaPropertyModel,
    SchemaPropertyTypeEnum, BasePropertySchemaModel
)
from ...utils import (
    get_inner_optional_type,
    get_type_params,
    is_dict,
    is_list,
    is_optional,
    is_type, is_typing_alias,
)


class OpenApiModelBuilderImpl(OpenApiModelBuilder):
    def __init__(
        self,
        info_data: Optional[InfoModel] = None,
        descriptors: Optional[Descriptors] = None,
    ):
        self.info_data = info_data
        self.descriptors = descriptors or Descriptors()
        self.model = self.build_model()

    def get_model(self) -> OpenapiModel:
        return self.model

    def add_interactor_path(self, route_params: RouteBuildParams) -> PathModel:
        route = route_params["route"]
        route_descriptor = self.descriptors.get_route_descriptor(route)
        if route_descriptor is not None and route_descriptor.is_available():
            path_model = route_descriptor.create_path(self, parameters=route_params)
        else:
            path_model = self._inner_build_interactor_path(parameters=route_params)
        path_model = self.model.add_path(
            method=route.method,
            url=route.path,
            path=path_model,
        )

        if route_descriptor is not None and route_descriptor.is_available():
            path_model = route_descriptor.update_path(
                self,
                parameters=route_params,
                path_model=path_model,
            )
        return path_model

    def add_processor_path(
        self,
        route: Route,
        processor: Union[MessageProcessor, Type[MessageProcessor]],
    ) -> PathModel:
        parameters = self.build_processor_params(route=route, processor=processor)

        route_descriptor = self.descriptors.get_route_descriptor(route)
        processor_descriptor = self.descriptors.get_processor_descriptor(parameters["processor_type"])

        if route_descriptor is not None and route_descriptor.is_available():
            path_model = route_descriptor.create_path(self, parameters=parameters)
        else:
            if processor_descriptor is not None and processor_descriptor.is_available():
                path_model = processor_descriptor.create_path(self, parameters=parameters)
            else:
                path_model = self._inner_build_interactor_path(parameters=parameters)

        path_model = self.model.add_path(
            method=route.method,
            url=route.path,
            path=path_model,
        )

        if route_descriptor is not None and route_descriptor.is_available():
            path_model = route_descriptor.update_path(
                self,
                parameters=parameters,
                path_model=path_model,
            )
        if processor_descriptor is not None and processor_descriptor.is_available():
            path_model = processor_descriptor.update_path(
                self,
                parameters=parameters,
                path_model=path_model,
            )

        return path_model

    def build_open_api_yaml(self) -> str:
        return build_open_api_yaml(self.model)

    def build_model(self) -> OpenapiModel:
        info_data = self.info_data
        if info_data is None:
            info_data = InfoModel(
                title="Open API Definition",
                version="1.0.0",
            )

        return OpenapiModel(
            info=info_data,
        )

    def build_object_model(
        self,
        parameters: RouteBuildParams,
        request_type: Type,
        path_model: Optional[PathModel] = None
    ) -> Optional[BasePropertySchemaModel]:
        real_cls = request_type
        if is_optional(real_cls):
            real_cls = get_inner_optional_type(real_cls)

        if is_typing_alias(real_cls):
            params = get_type_params(real_cls)
            if len(params) > 0:
                real_cls = params[0]

        if is_list(real_cls):
            type_args = get_type_params(real_cls)
            if len(type_args) > 0:
                array_type = type_args[0]
                array_property_model, required = self.build_property_model(
                    parameters=parameters,
                    property_type=array_type
                )
                property_model = SchemaArrayPropertyModel(item=array_property_model)

                return property_model

        if is_dict(real_cls):
            return ObjectSchemaModel()

        payload_descriptor = self.descriptors.get_payload_descriptor(payload_type=real_cls)

        if payload_descriptor is not None and payload_descriptor.is_available():
            object_model = payload_descriptor.create_object(
                self,
                parameters=parameters,
                path_model=path_model
            )
        else:
            object_model = ObjectSchemaModel()

        keys_to_ignore = []
        if hasattr(real_cls, "_real_cls"):
            _cls_attrs = getattr(real_cls, "_cls_attrs")
            if _cls_attrs is not None:
                keys_to_ignore = _cls_attrs.keys()
            real_cls = getattr(real_cls, "_real_cls")

        signature = None
        if hasattr(real_cls, "__init__"):
            signature = inspect.signature(real_cls.__init__)

        if signature is not None:
            for parameter in signature.parameters.values():
                if parameter.name in ["self", "args", "kwargs"]:
                    continue

                if parameter.name in keys_to_ignore:
                    continue

                if path_model is not None and path_model.has_parameter(parameter.name):
                    continue

                property_model, required = self.build_property_model(
                    parameters=parameters,
                    property_type=parameter.annotation,
                    default=parameter.default
                )

                object_model.add_property(
                    name=parameter.name,
                    prop=property_model,
                    required=required,
                )
        if payload_descriptor is not None and payload_descriptor.is_available():
            object_model = payload_descriptor.update_object(
                self,
                parameters=parameters,
                object_model=object_model,
                path_model=path_model,
            )

        return object_model

    def build_property_model(
        self,
        parameters: RouteBuildParams,
        property_type: Type,
        default=inspect.Signature.empty
    ) -> Tuple[PropertyModel, bool]:
        real_type = property_type
        parameter_required = True
        parameter_default = None
        if hasattr(real_type, "_real_cls"):
            real_type = getattr(real_type, "_real_cls")

        if is_optional(real_type):
            parameter_required = False
            real_type = get_inner_optional_type(real_type)

        if default != inspect.Signature.empty:
            parameter_required = False
            parameter_default = default

        property_model = None
        if real_type in OPENAPI_SCALAR_TYPES_DEFINITIONS:
            (def_property_type, def_property_format) = OPENAPI_SCALAR_TYPES_DEFINITIONS[real_type]

            if def_property_type is None:
                def_property_type = SchemaPropertyTypeEnum.string

            property_model = SchemaPropertyModel(
                type=def_property_type,
                format=def_property_format,
                default=parameter_default,
            )
        elif is_list(real_type):
            type_args = get_type_params(real_type)
            if len(type_args) > 0:
                array_type = type_args[0]
                array_property_model, _ = self.build_property_model(
                    parameters=parameters,
                    property_type=array_type
                )
                property_model = SchemaArrayPropertyModel(item=array_property_model)
        elif is_dict(real_type):
            property_model = ObjectSchemaModel()
        elif is_type(real_type) and issubclass(real_type, Enum):
            property_model = SchemaPropertyModel(
                type=SchemaPropertyTypeEnum.string,
                enum=[e.value for e in real_type],
                default=parameter_default,
            )
        elif real_type is not None:
            property_model = self.build_object_model(
                parameters=parameters,
                request_type=real_type
            )

        if property_model is None:
            property_model = SchemaPropertyModel(
                type=SchemaPropertyTypeEnum.string,
                default=parameter_default,
            )

        return property_model, parameter_required

    def add_query_parameter(
        self,
        path_model: PathModel,
        parameter_type: type,
        parameter_name: str,
        required: bool = False,
        default=inspect.Signature.empty
    ):
        if is_list(parameter_type):
            return

        query_parameter_type = None
        query_parameter_format = None
        query_parameter_default = None

        if parameter_type in OPENAPI_SCALAR_TYPES_DEFINITIONS:
            (query_parameter_type, query_parameter_format) = OPENAPI_SCALAR_TYPES_DEFINITIONS[parameter_type]

        parameter_required = required
        if default != inspect.Signature.empty:
            parameter_required = False
            query_parameter_default = default

        path_model.add_parameter(
            parameter=PathParameterModel(
                path_in=PathParameterInEnum.query,
                name=parameter_name,
                required=parameter_required,
                schema=SchemaPropertyModel(
                    type=query_parameter_type or SchemaPropertyTypeEnum.string,
                    format=query_parameter_format,
                    default=query_parameter_default,
                ),
            )
        )

    def add_query_parameters(self, path_model: PathModel, request_type: Optional[Type] = None):
        real_cls = request_type
        if is_optional(real_cls):
            real_cls = get_inner_optional_type(real_cls)

        if is_list(real_cls):
            return

        keys_to_ignore = []
        if hasattr(real_cls, "_real_cls"):
            _cls_attrs = getattr(real_cls, "_cls_attrs")
            if _cls_attrs is not None:
                keys_to_ignore = _cls_attrs.keys()
            real_cls = getattr(real_cls, "_real_cls")

        if hasattr(real_cls, "__init__"):
            signature = inspect.signature(real_cls.__init__)
        else:
            return

        for parameter in signature.parameters.values():
            if parameter.name in ["self", "args", "kwargs"]:
                continue

            if parameter.name in keys_to_ignore:
                continue

            if path_model.has_parameter(parameter.name):
                continue

            parameter_required = True
            real_cls = parameter.annotation
            if is_optional(real_cls):
                parameter_required = False
                real_cls = get_inner_optional_type(real_cls)

            self.add_query_parameter(
                path_model=path_model,
                parameter_type=real_cls,
                parameter_name=parameter.name,
                required=parameter_required,
                default=parameter.default,
            )

    def add_request_body(
        self,
        parameters: RouteBuildParams,
        path_model: PathModel,
    ):

        request_type = parameters["request_type"]
        content_type = parameters["request_content_type"]

        if request_type is not None:
            request_body_model = RequestBodyModel()
            object_content_model = self.build_object_model(
                parameters=parameters,
                request_type=request_type,
                path_model=path_model,
            )
            request_body_model.add_content(
                content_type=content_type or DEFAULT_CONTENT_TYPE,
                content=PathContentModel(
                    schema=object_content_model,
                ),
            )
            path_model.request_body = request_body_model

    def add_path_parameters(self, route: Route, path_model: PathModel):
        for path_parameter in get_path_parameters(route.path):
            path_model.add_parameter(
                parameter=PathParameterModel(
                    path_in=PathParameterInEnum.path,
                    name=path_parameter,
                    schema=SchemaPropertyModel(type=SchemaPropertyTypeEnum.string),
                    required=True
                )
            )

    def build_processor_params(
        self,
        route: Route,
        processor: Union[MessageProcessor, Type[MessageProcessor]]
    ) -> RouteBuildParams:
        processor_type = processor
        if not is_type(processor_type):
            processor_type = type(processor_type)

        route_descriptor = self.descriptors.get_route_descriptor(route)
        processor_descriptor = self.descriptors.get_processor_descriptor(processor_type)

        if route_descriptor is not None and route_descriptor.is_available():
            return route_descriptor.build_params(
                self,
                route=route,
                processor=processor,
            )
        else:
            if processor_descriptor is not None and processor_descriptor.is_available():
                return processor_descriptor.build_params(
                    self,
                    route=route,
                    processor=processor,
                )

        parameters = build_params(route=route)
        if isinstance(processor, AutoMappedMessageProcessor):
            parameters["processor_instance"] = processor
            parameters["interactor_type"] = processor.interactor
            if not is_type(processor.interactor):
                parameters["interactor_instance"] = processor.interactor
                parameters["interactor_type"] = type(processor.interactor)
            if hasattr(processor, "parser") and hasattr(processor.parser, "request_type"):
                parameters["request_type"] = processor.parser.request_type

            parameters["response_content_type"] = processor.response_content_type
            parameters["success_status"] = processor.success_status
            parameters["processor_type"] = type(processor)
        else:
            processor_type = processor
            if not is_type(processor_type):
                parameters["processor_instance"] = processor_type
                processor_type = type(processor_type)
            parameters["processor_type"] = processor_type
            signature = inspect.signature(processor_type.__init__)
            for parameter in signature.parameters.values():
                if parameter.name in ["self", "args", "kwargs"]:
                    continue
                if "interactor" in parameter.name.lower():
                    parameters["interactor_type"] = parameter.annotation
                    break

        if parameters["interactor_type"] is not None:
            signature = inspect.signature(getattr(parameters["interactor_type"], "execute"))
            for parameter in signature.parameters.values():
                if parameter.name in ["self", "args", "kwargs"]:
                    continue
                parameters["request_type"] = parameter.annotation
                break
            parameters["response_type"] = signature.return_annotation

        return parameters

    def _inner_build_interactor_path(self, parameters: RouteBuildParams) -> PathModel:
        interactor_descriptor = self.descriptors.get_interactor_descriptor(parameters["interactor_type"])

        if interactor_descriptor is not None and interactor_descriptor.is_available():
            path_model = interactor_descriptor.create_path(self, parameters=parameters)
        else:
            path_model = self.build_interactor_path(parameters=parameters)

        if interactor_descriptor is not None and interactor_descriptor.is_available():
            path_model = interactor_descriptor.update_path(
                self,
                parameters=parameters,
                path_model=path_model,
            )

        return path_model

    def build_interactor_path(self, parameters: RouteBuildParams) -> PathModel:
        path_model = PathModel()

        route = parameters["route"]
        request_type = parameters["request_type"]

        self.add_path_parameters(
            route=route,
            path_model=path_model,
        )

        if route.method in ["GET", "DELETE"]:
            self.add_query_parameters(
                path_model=path_model,
                request_type=request_type,
            )
        else:
            self.add_request_body(
                parameters=parameters,
                path_model=path_model,
            )

        self.add_response_body(
            parameters=parameters,
            path_model=path_model,
        )

        return path_model

    def add_response_body(
        self,
        parameters: RouteBuildParams,
        path_model: PathModel,
    ):
        path_content_model = None
        response_type = parameters["response_type"]
        interactor_type = parameters["interactor_type"]

        if response_type is None:
            if interactor_type is not None:
                execute_method = parameters["interactor_method_name"] or "execute"
                execute_signature = inspect.signature(getattr(interactor_type, execute_method))
                response_type = execute_signature.return_annotation

        if response_type is not None:
            path_content_model = PathContentModel(
                schema=self.build_object_model(
                    parameters=parameters,
                    request_type=response_type
                ),
            )

        if path_content_model is None:
            path_content_model = PathContentModel()

        response_model = PathResponseModel(
            description=get_interactor_description(interactor_type) or DEFAULT_RESPONSE_DESCRIPTION
        )

        response_model.add_content(
            content_type=parameters.get("response_content_type") or DEFAULT_CONTENT_TYPE,
            content=path_content_model,
        )

        path_model.add_response(
            code=parameters.get("success_status") or DEFAULT_SUCCESS_CODE,
            response=response_model
        )
