import json
import re
from typing import Any, Dict, Type
from dataclasses import is_dataclass

from dataclasses_avroschema import AvroModel

from wunderkafka.logger import logger
from wunderkafka.serdes.avromodel.pydantic import derive_from_pydantic, exclude_pydantic_class


def derive(model_type: Type[object], topic: str, *, is_key: bool = False) -> str:
    pydantic_model = derive_from_pydantic(model_type)
    if pydantic_model is None:
        if is_dataclass(model_type):
            # https://github.com/python/mypy/issues/14941
            model_schema = model_type.avro_schema_to_python()                                             # type: ignore
        else:
            # Non-dataclasses objects may allow mixing defaults and non-default fields order,
            # so to still reuse dataclasses_avroschema, we extract fields and reorder them to satisfy dataclasses
            # restrictions, than reorder them back.
            # All these hacks will work only for flat schemas:
            #   we avoid describing objects via nested types for HDFS's sake.
            attributes = _extract_attributes(model_type)
            ordering = list(attributes['__annotations__'])
            crafted_model: Type[AvroModel] = type(model_type.__name__, (AvroModel,), attributes)
            model_schema = crafted_model.avro_schema_to_python()
            fields_map = {field_data['name']: field_data for field_data in model_schema['fields']}
            reordered_fields = [fields_map[attr] for attr in ordering]
            model_schema['fields'] = reordered_fields
    else:
        model_schema = pydantic_model.avro_schema_to_python()
    model_schema.pop('doc', None)
    suffix = 'key' if is_key else 'value'
    if hasattr(model_type, 'Meta') and hasattr(model_type.Meta, 'name'):
        model_schema['name'] = model_type.Meta.name
    else:
        model_schema['name'] = '{0}_{1}'.format(re.sub('[^\w_]','_', topic), suffix)
    if model_schema['name'].startswith('_'):
        logger.warning('Topic name {0} starts with underscore, which is not allowed for Avro schema names.')
        model_schema['name'] = model_schema['name'][1:]
    if pydantic_model:
        # I would not prefer to just override PydanticParser because it always may be broken or thrown-out
        model_schema = exclude_pydantic_class(model_schema)
    return json.dumps(model_schema)


def _extract_attributes(type_: Type[object]) -> Dict[str, Any]:
    fields = vars(type_).get('__annotations__', {})
    _, *parents = type_.mro()
    for base in parents:
        new_fields = {**vars(base).get('__annotations__', {})}
        # Currently it is `model_config: ClassVar[SettingsConfigDict]`
        # https://github.com/pydantic/pydantic-settings/blob/919a20b77527ecc1cd6eeb0a09ca22cc21486fb8/pydantic_settings/main.py#L166
        for field in ['__slots__', 'klass', 'metadata', 'schema_def', '__config__', 'model_config']:
            new_fields.pop(field, None)
        fields = {**new_fields, **fields}

    attributes = {}
    attributes.update({'__annotations__': fields})
    meta = vars(type_).get('Meta', None)
    if meta is not None:
        attributes.update({'Meta': meta})
    return attributes
