from enum import IntEnum  # noqa
from functools import partial
from os import linesep
from typing import *  # noqa
from typing import Any, Iterable, Type

from google.protobuf.descriptor import Descriptor, EnumDescriptor, FieldDescriptor
from google.protobuf.duration_pb2 import Duration  # noqa
from google.protobuf.reflection import GeneratedProtocolMessageType
from google.protobuf.struct_pb2 import Struct  # noqa
from google.protobuf.timestamp_pb2 import Timestamp  # noqa
from pydantic import BaseModel, Field  # noqa

from pydanticprotobuf.const import FIELD_DESCRIPTOR_TYPE_MAP, GETTER_KEY, ONELINE, TAB


class PydanticProtobuf:
    def _basemodel_statement(self, level: int, message: Descriptor) -> str:
        return f"{level * TAB}class {message.name}(BaseModel):"

    def _field_statements(self, level: int, message: Descriptor) -> Iterable:
        return map(partial(self.convert_protobuf_field, level), message.fields)

    def _basemodel_str(self, level: int, message: Descriptor) -> str:
        return linesep.join(
            [
                self._basemodel_statement(level=level, message=message),
                *self._field_statements(level=level, message=message),
            ]
        )

    def _getter_str(self, descriptor: Descriptor) -> str:
        return f"def {GETTER_KEY}(): return {descriptor.name}"

    def _compile_str(self, basemodel_str: str, getter_str: str) -> str:
        return basemodel_str + linesep + getter_str

    def _compile(self, compile_str: str) -> Any:
        return compile(compile_str, "<string>", "exec")

    def convert_protobuf(
        self, message: GeneratedProtocolMessageType
    ) -> Type[BaseModel]:
        descriptor = message.DESCRIPTOR  # type: ignore
        basemodel_str = self._basemodel_str(level=0, message=descriptor)
        compile_code = self._compile(
            compile_str=self._compile_str(
                basemodel_str=basemodel_str,
                getter_str=self._getter_str(descriptor=descriptor),
            )
        )
        _sub_namespace = {k: v for k, v in globals().items() if not k.startswith("__")}
        exec(compile_code, _sub_namespace)
        return _sub_namespace[GETTER_KEY]()

    def _type_statement(self, field: FieldDescriptor) -> str:
        return FIELD_DESCRIPTOR_TYPE_MAP[field.type].__name__

    def convert_protobuf_field(self, level: int, field: FieldDescriptor) -> str:
        level += 1
        _field_type = field.type
        _field_label = field.label
        extra = None

        if _field_type == FieldDescriptor.TYPE_ENUM:
            enum_type: EnumDescriptor = field.enum_type
            type_statement = enum_type.name
            class_statement = f"{TAB * level}class {enum_type.name}(IntEnum):"
            field_statements = map(
                lambda value: f"{TAB * (level + 1)}{value.name} = {value.index}",
                enum_type.values,
            )
            extra = linesep.join([class_statement, *field_statements])
            factory = "int"
        elif _field_type == FieldDescriptor.TYPE_MESSAGE:
            type_statement = field.message_type.name
            if type_statement.endswith("Entry"):
                key, value = field.message_type.fields
                type_statement = (
                    f"Dict[{self._type_statement(key)}, {self._type_statement(value)}]"
                )
                factory = "dict"
            elif type_statement == "Struct":
                type_statement = "Dict[str, Any]"
                factory = "dict"
            else:
                extra = self._basemodel_str(level=level, message=field.message_type)
                factory = type_statement
        else:
            type_statement = self._type_statement(field=field)
            factory = type_statement

        if _field_label == FieldDescriptor.LABEL_REPEATED:
            type_statement = f"List[{type_statement}]"
            factory = "list"

        default_statement = f" = Field(default_factory={factory})"
        if _field_label == FieldDescriptor.LABEL_REQUIRED:
            default_statement = ""

        field_statement = (
            f"{TAB * level}{field.name}: {type_statement}{default_statement}"
        )
        if not extra:
            return field_statement
        return linesep + extra + ONELINE + field_statement
