from ast import TypeVar
import inspect
import re
from datetime import date, datetime
from enum import Enum
from typing import Any, Type, Optional, List, Union, Tuple

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

from ..model import (
    PathResponseModel,
    PathContentModel,
    RequestBodyModel,
    PathModel,
    PropertyModel,
    SchemaModel,
    SchemaPropertyTypeEnum,
    SchemaPropertyFormatEnum,
    ObjectSchemaModel,
    SchemaArrayPropertyModel,
    SchemaPropertyModel,
    PathParameterModel,
    PathParameterInEnum,
    OpenapiModel,
    InfoModel,
)
from ..utils import is_optional, is_list, get_inner_optional_type, get_type_params, is_dict, is_type

AnyType = TypeVar('AnyType')

URL_PATH_PARAMETER_PATTERN = re.compile(r'{(.*?)}')
INTERACTOR_REPLACE_PATTERN = re.compile("Interactor", re.IGNORECASE)
DEFAULT_RESPONSE_DESCRIPTION = "Response description"
OPENAPI_SCALAR_TYPES_DEFINITIONS = {
    int: (SchemaPropertyTypeEnum.integer, None),
    float: (SchemaPropertyTypeEnum.number, None),
    bool: (SchemaPropertyTypeEnum.boolean, None),

    # String data
    str: (SchemaPropertyTypeEnum.string, None),
    date: (SchemaPropertyTypeEnum.string, SchemaPropertyFormatEnum.date),
    datetime: (SchemaPropertyTypeEnum.string, SchemaPropertyFormatEnum.datetime),
}


class OpenApiModelBuilder:
    def __init__(self, info_data: InfoModel | None = None) -> None:
        self.info_data = info_data

    def build_model(self):
        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,
        )


    @staticmethod
    def _get_path_parameters(url: str) -> List[str]:
        return URL_PATH_PARAMETER_PATTERN.findall(url)

    @staticmethod
    def _get_description(value: str | Type | None) -> Optional[str]:
        if value is None:
            return None
        name = str(value)
        if is_type(value):
            name = getattr(value, '__name__', name)
        name = INTERACTOR_REPLACE_PATTERN.sub(repl='', string=name)
        out = ''
        for i in range(len(name)):
            char = name[i]
            if char.isupper() and len(out) > 0:
                out += ' '
            out += char
        return out

    def _build_object_model(
        self,
        request_type,
        path_model: Optional[PathModel] = None,
    ) -> Optional[SchemaModel]:
        real_cls = request_type
        if is_optional(real_cls):
            real_cls = get_inner_optional_type(real_cls)

        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(property_type=array_type)
                property_model = SchemaArrayPropertyModel(
                    item=array_property_model
                )

                return property_model

        object_model = ObjectSchemaModel()
        if is_dict(real_cls):
            return object_model

        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 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(
                property_type=parameter.annotation,
                default=parameter.default
            )

            object_model.add_property(
                name=parameter.name,
                prop=property_model,
                required=required,
            )

        return object_model

    @staticmethod
    def _add_query_parameters(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)

            if is_list(real_cls):
                continue

            query_parameter_type = None
            query_parameter_format = None
            query_parameter_default = None

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

            if parameter.default != inspect.Signature.empty:
                parameter_required = False
                query_parameter_default = parameter.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_request_body(
        self,
        path_model: PathModel,
        request_type: Optional[Type] = None,
        content_type: Optional[str] = None,
    ):
        if request_type is not None:
            request_body_model = RequestBodyModel()
            object_content_model = self._build_object_model(
                request_type=request_type,
                path_model=path_model,
            )
            request_body_model.add_content(
                content_type=content_type or 'application/json',
                content=PathContentModel(
                    schema=object_content_model,
                )
            )
            path_model.request_body = request_body_model

    def _build_property_model(
        self,
        property_type: Any,
        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: PropertyModel | None = None
        if real_type is None:
            property_model = SchemaPropertyModel(
                type=SchemaPropertyTypeEnum.string,
                default=parameter_default,
            )
        elif 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, required = self._build_property_model(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_schema = self._build_object_model(request_type=real_type)
            if isinstance(property_schema, PropertyModel):
                property_model = property_schema

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

        return property_model, parameter_required

    def add_interactor_path(
        self,
        model: OpenapiModel,
        route: Route,
        interactor_type: Optional[Type] = None,
        request_type: Optional[Type] = None,
        response_content_type: Optional[str] = None
    ):
        model.add_path(
            method=route.method,
            url=route.path,
            path=self.build_interactor_path(
                route=route,
                interactor_type=interactor_type,
                request_type=request_type,
                response_content_type=response_content_type,
            )
        )

    def add_processor_path(
        self,
        model: OpenapiModel,
        route: Route,
        processor: Union[MessageProcessor, Type[MessageProcessor]],
    ):
        interactor_type = None
        request_type = None
        response_content_type = None

        if isinstance(processor, AutoMappedMessageProcessor):
            interactor_type = processor.interactor
            if not is_type(interactor_type):
                interactor_type = type(interactor_type)
            if hasattr(processor, 'parser') and hasattr(processor.parser, 'request_type'):
                request_type = processor.parser.request_type

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

            response_content_type = processor.response_content_type
        else:
            processor_type = processor
            if not is_type(processor_type):
                processor_type = 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():
                    interactor_type = parameter.annotation
                    break

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

        model.add_path(
            method=route.method,
            url=route.path,
            path=self.build_interactor_path(
                route=route,
                interactor_type=interactor_type,
                request_type=request_type,
                response_content_type=response_content_type,
            )
        )

    def build_interactor_path(
        self,
        route: Route,
        interactor_type: Optional[Type] = None,
        request_type: Optional[Type] = None,
        response_content_type: Optional[str] = None
    ):
        path_model = PathModel()
        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(
                path_model=path_model,
                request_type=request_type,
            )

        response_model = PathResponseModel(
            description=self._get_description(interactor_type) or DEFAULT_RESPONSE_DESCRIPTION
        )

        path_content_model = None
        if interactor_type is not None:
            execute_signature = inspect.signature(getattr(interactor_type, 'execute'))
            path_content_model = PathContentModel(
                schema=self._build_object_model(execute_signature.return_annotation)
            )
        if path_content_model is None:
            path_content_model = PathContentModel()

        response_model.add_content(
            content_type=response_content_type or "application/json",
            content=path_content_model
        )
        path_model.add_response(
            code="200",
            response=response_model
        )
        return path_model

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