import inspect
from typing import Optional, Type, Union, Set, Any

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

from ._builder_utils import (
    DEFAULT_CONTENT_TYPE,
    DEFAULT_RESPONSE_DESCRIPTION,
    DEFAULT_SUCCESS_CODE,
    OPENAPI_SCALAR_TYPES_DEFINITIONS,
    PARAMETER_LOCATION_MAP,
    get_interactor_description,
    get_path_parameters
)
from .._OpenApiModelBuilder import OpenApiModelBuilder
from ..descriptors import Descriptors
from ..dto import RouteBuildParams, build_params, get_type_definition, TypeDefinition, SchemaConflictStrategy, \
    BuilderConfig
from ..yaml import build_open_api_yaml
from ...model import (
    InfoModel,
    ObjectSchemaModel,
    OpenapiModel,
    PathContentModel,
    PathModel,
    PathParameterInEnum,
    PathParameterModel,
    PathResponseModel,
    RequestBodyModel,
    SchemaArrayPropertyModel,
    SchemaModel,
    SchemaPropertyModel,
    SchemaPropertyTypeEnum, ComponentsModel, RefSchemaModel
)
from ...utils import (
    is_dict,
    is_list,
    is_type,
    generate_uuid,
    is_scalar, get_type_name, is_enum
)

_PROCESSOR_TYPE = Union[MessageProcessor, Type[MessageProcessor]]

_GENERIC_MODEL = ObjectSchemaModel()
_GENERIC_MODEL.set_schema_uuid(schema_uuid="GenericObject")
_GENERIC_MODEL.set_schema_name(schema_name="GenericObject")


class OpenApiModelBuilderImpl(OpenApiModelBuilder):
    def __init__(self, config: Optional[BuilderConfig] = None, **kwargs):
        resolved = {**(config or {}), **kwargs}
        self.descriptors = resolved.get("descriptors") or Descriptors()
        self.conflict_strategy = resolved.get("conflict_strategy", SchemaConflictStrategy.readable)
        self.ref_array_types = resolved.get("ref_array_types", False)
        self._model = self._build_model(resolved.get("info_data"))

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

    def get_component_model(self) -> ComponentsModel:
        openapi_model = self.get_model()
        if openapi_model.components is None:
            openapi_model.components = ComponentsModel()
        return openapi_model.components

    @staticmethod
    def _build_model(info_data: Optional[InfoModel] = None) -> OpenapiModel:
        if info_data is None:
            info_data = InfoModel(
                title="Open API Definition",
                version="1.0.0",
            )

        return OpenapiModel(
            info=info_data,
        )

    def build_open_api_yaml(self) -> str:
        return build_open_api_yaml(self.get_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.get_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: _PROCESSOR_TYPE) -> 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.get_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_ref_schema(self, schema_model: SchemaModel) -> RefSchemaModel:
        component_model = self.get_component_model()
        component_model.add_schema(model=schema_model)
        return RefSchemaModel(
            schema=schema_model,
            path="components/schemas"
        )

    def _get_ref_schema(self, schema_uuid: str) -> Optional[RefSchemaModel]:
        component_model = self.get_component_model()
        schema_model = component_model.get_schema(schema_uuid=schema_uuid)

        if schema_model is None:
            return None

        return RefSchemaModel(
            schema=schema_model,
            path="components/schemas"
        )

    @staticmethod
    def _get_skipped_parameters(
        type_def: TypeDefinition,
        parameters: RouteBuildParams,
        path_model: Optional[PathModel] = None
    ) -> Set[str]:
        skipped_params = set()
        extra_params = parameters["extra_mapping"] or {}
        if path_model is not None:
            for parameter_def in type_def["parameters"]:
                parameter_name = parameter_def["name"]
                if path_model.has_parameter(parameter_name) or parameter_name in extra_params:
                    skipped_params.add(parameter_name)
        return skipped_params

    @staticmethod
    def _get_module_suffix(real_cls: Type) -> str:
        """Extract a readable suffix from the class module for disambiguation."""
        module = getattr(real_cls, "__module__", "")
        if not module:
            return ""
        parts = module.rsplit(".", 1)
        segment = parts[-1] if len(parts) > 1 else parts[0]
        segment = segment.lstrip("_")
        return ''.join(word.capitalize() for word in segment.split("_"))

    def _resolve_schema_identity(self, real_cls: Type, skipped_params: Set[str]) -> tuple:
        """Return (schema_uuid, schema_name) based on the conflict strategy."""
        if self.conflict_strategy == SchemaConflictStrategy.reuse:
            return generate_uuid(real_cls), get_type_name(real_cls)

        schema_uuid = generate_uuid(real_cls, salt=skipped_params)
        schema_name = get_type_name(real_cls)

        if self.conflict_strategy == SchemaConflictStrategy.readable and len(skipped_params) > 0:
            suffix = ''.join(
                ''.join(word.capitalize() for word in p.split('_'))
                for p in sorted(skipped_params)
            )
            schema_name = f"{schema_name}Without{suffix}"

        base_uuid = generate_uuid(real_cls)
        component_model = self.get_component_model()
        if component_model.has_schema_name(schema_name):
            existing = component_model.get_schema(schema_uuid)
            if existing is None and component_model.get_schema(base_uuid) is None:
                module_suffix = self._get_module_suffix(real_cls)
                if module_suffix:
                    schema_name = f"{module_suffix}{schema_name}"

        return schema_uuid, schema_name

    def build_schema_model(
        self,
        schema_type: Type,
        parameters: RouteBuildParams,
        path_model: Optional[PathModel] = None,
    ) -> SchemaModel:
        if self._is_property_schema(schema_type):
            return self.build_property_model(
                property_type=schema_type,
                parameters=parameters,
            )

        type_def = get_type_definition(schema_type)
        real_cls = type_def["origin_type"]
        payload_descriptor = self.descriptors.get_payload_descriptor(payload_type=real_cls)

        if payload_descriptor is not None and payload_descriptor.is_available():
            schema_model = payload_descriptor.create_object(self, parameters=parameters, path_model=path_model)
        else:
            if is_list(real_cls):
                array_type = dict
                if type_def["subtypes"] is not None and len(type_def["subtypes"]) > 0:
                    array_type = type_def["subtypes"][0]

                if self._is_property_schema(array_type):
                    array_property_model = self.build_property_model(
                        property_type=array_type,
                        parameters=parameters,
                    )
                    schema_model = SchemaArrayPropertyModel(array_property_model)
                    return schema_model

                array_property_model = self.build_schema_model(
                    schema_type=array_type,
                    parameters=parameters,
                    path_model=path_model,
                )

                if array_property_model == _GENERIC_MODEL:
                    schema_model = SchemaArrayPropertyModel(array_property_model)
                    return schema_model

                if not self.ref_array_types:
                    schema_model = SchemaArrayPropertyModel(items=array_property_model)
                    return schema_model

                schema_uuid = generate_uuid(f"ArrayOf{array_property_model.get_schema_uuid()}")
                schema_model = self._get_ref_schema(schema_uuid)
                if schema_model is None:
                    schema_model = SchemaArrayPropertyModel(items=array_property_model)
                    schema_model.set_schema_uuid(schema_uuid)
                    schema_model = self._build_ref_schema(schema_model)
                return schema_model

            type_params = type_def["parameters"]
            if is_dict(real_cls) or real_cls == Any:
                return ObjectSchemaModel()
            else:
                skipped_params = self._get_skipped_parameters(
                    type_def=type_def,
                    parameters=parameters,
                    path_model=path_model
                )
                schema_uuid, schema_name = self._resolve_schema_identity(real_cls, skipped_params)

                if len(skipped_params) > 0:
                    type_params = [
                        param_def
                        for param_def in type_def["parameters"]
                        if param_def["name"] not in skipped_params
                    ]

                if len(type_params) == 0:
                    return _GENERIC_MODEL

            schema_model = self._get_ref_schema(schema_uuid)
            if schema_model is not None:
                return schema_model

            schema_model = ObjectSchemaModel()
            schema_model.set_schema_uuid(schema_uuid=schema_uuid)
            schema_model.set_schema_name(schema_name=schema_name)

            if not is_dict(real_cls):
                for parameter_def in type_params:
                    property_model = self.build_property_model(
                        property_type=parameter_def["type"],
                        parameters=parameters,
                        default=parameter_def["default"],
                    )

                    schema_model.add_property(
                        name=parameter_def["name"],
                        prop=property_model,
                        required=not parameter_def["optional"],
                    )

        if payload_descriptor is not None and payload_descriptor.is_available():
            schema_model = payload_descriptor.update_object(
                self,
                parameters=parameters,
                schema_model=schema_model,
                path_model=path_model,
            )

        schema_model = self._build_ref_schema(schema_model)
        return schema_model

    @staticmethod
    def _is_property_schema(schema_type: type) -> bool:
        return is_scalar(schema_type) or is_enum(schema_type) or schema_type in OPENAPI_SCALAR_TYPES_DEFINITIONS

    def build_property_model(
        self,
        property_type: Type,
        parameters: RouteBuildParams,
        default=inspect.Signature.empty,
    ) -> SchemaModel:
        type_def = get_type_definition(property_type)
        real_type = type_def["origin_type"]

        parameter_default = None
        if default != inspect.Signature.empty:
            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 real_type == inspect.Signature.empty:
            property_model = SchemaPropertyModel(
                type=SchemaPropertyTypeEnum.string,
                default=parameter_default,
            )
        elif is_enum(real_type):
            property_model = SchemaPropertyModel(
                type=SchemaPropertyTypeEnum.string,
                enum=[e.value for e in real_type],
                default=parameter_default,
            )

        if property_model is not None:
            return property_model

        return self.build_schema_model(
            schema_type=property_type,
            parameters=parameters,
        )

    def add_query_parameter(
        self,
        path_model: PathModel,
        parameter_type: type,
        parameter_name: str,
        required: bool = False,
        default=inspect.Signature.empty,
    ):
        type_def = get_type_definition(parameter_type)
        resolved_type = type_def["origin_type"]

        if resolved_type is list:
            return

        query_parameter_type = None
        query_parameter_format = None
        query_parameter_default = None

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

        parameter_required = required and not type_def["optional"]
        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):
        type_def = get_type_definition(request_type)

        if type_def["origin_type"] is None or type_def["origin_type"] is list:
            return

        for param_def in type_def["parameters"]:
            if path_model.has_parameter(param_def["name"]):
                continue

            self.add_query_parameter(
                path_model=path_model,
                parameter_type=param_def["type"],
                parameter_name=param_def["name"],
                required=not param_def["optional"],
                default=param_def["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()
            request_body_model.add_content(
                content_type=content_type or DEFAULT_CONTENT_TYPE,
                content=PathContentModel(
                    schema=self.build_schema_model(
                        schema_type=request_type,
                        parameters=parameters,
                        path_model=path_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: _PROCESSOR_TYPE) -> 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:
            execute_method = getattr(parameters["interactor_type"], "execute", None)
            if execute_method is not None:
                signature = inspect.signature(execute_method)
                for parameter in signature.parameters.values():
                    if parameter.name in ["self", "args", "kwargs"]:
                        continue
                    interactor_request_type = parameter.annotation
                    if parameters["request_type"] is None:
                        parameters["request_type"] = interactor_request_type
                    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,
        )

        self.add_extra_mapping_parameters(
            parameters=parameters,
            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,
        )

        self.add_exception_responses(
            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_schema_model(
                    schema_type=response_type,
                    parameters=parameters,
                ),
            )

        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,
        )

    def add_exception_responses(
        self,
        parameters: RouteBuildParams,
        path_model: PathModel,
    ):
        exception_map = parameters.get("exception_map")
        if not exception_map:
            return

        for exception_type, status_def in exception_map.items():
            if isinstance(status_def, tuple):
                status_code, description = status_def
            else:
                status_code = status_def
                description = get_type_name(exception_type)

            response_model = PathResponseModel(description=description)
            path_model.add_response(code=status_code, response=response_model)

    def add_extra_mapping_parameters(
        self,
        parameters: RouteBuildParams,
        path_model: PathModel,
    ):
        extra_mapping = parameters.get("extra_mapping")
        if not extra_mapping:
            return

        for param_key, mapping_def in extra_mapping.items():
            param_name = mapping_def.get("name", param_key)
            location = mapping_def.get("location", "header")
            path_in = PARAMETER_LOCATION_MAP.get(location, PathParameterInEnum.header)

            path_model.add_parameter(
                parameter=PathParameterModel(
                    path_in=path_in,
                    name=param_name,
                    required=path_in != PathParameterInEnum.query,
                    schema=SchemaPropertyModel(type=SchemaPropertyTypeEnum.string),
                )
            )
