import inspect

from typing import Any, Optional, Iterable
from .fields import Field, FieldProto
from .index import Index, IndexKey


def snake_case(name: str) -> str:
    result = ""
    for i, l in enumerate(name):
        if 65 <= ord(l) <= 90:
            if i != 0:
                result += "_"
            result += l.lower()
        else:
            result += l
    return result


# TNamespace = dict[str, Any]


# class DefaultCollectionMeta(type):
#     @classmethod
#     def __prepare__(
#         mcs,
#         name: str,
#         bases: tuple[type[object]],
#         **kwargs: dict[str, Any]
#     ) -> TNamespace:
#         return {
#             "COLLECTION": snake_case(name)
#         }


class MetaModel:

    _fields: dict[str, FieldProto]
    _indexes: Iterable[Index]
    _cache_key_fields: set[str]

    collection: str

    # You will usually need INDEXES only for defining compound ones, as
    # simple single-field indexes including "unique" have a shortcut arg
    # in a field definition, i.e.
    #
    # username = StringField(unique=True)
    # owner_id = ReferenceField(index=True)
    INDEXES: Iterable[Index] | None = None

    COLLECTION: str = ""
    KEY_FIELD: str = "_id"
    CACHE_KEY_FIELDS: Optional[Iterable[str]] = None

    def __init_subclass__(cls) -> None:
        cls._fields = {
            name: obj
            for name, obj in inspect.getmembers(cls)
            if isinstance(obj, Field)
        }
        cls._indexes = cls.__get_indexes()
        cls._cache_key_fields = cls.__get_cache_key_fields()
        cls.collection = cls.COLLECTION if cls.COLLECTION else snake_case(cls.__name__)  # TODO: Remove one of the two

    @classmethod
    def __get_indexes(cls) -> Iterable[Index]:
        for field, descriptor in cls._fields.items():
            if descriptor.index is None:
                continue
            yield Index(
                keys=[IndexKey(key=field, spec=descriptor.index)],
                options=descriptor.index_options,
            )

        seen = set()
        for base in reversed(inspect.getmro(cls)):
            if not issubclass(base, MetaModel):
                continue
            if id(base.INDEXES) not in seen and base.INDEXES:
                seen.add(id(base.INDEXES))
                yield from base.INDEXES

    @classmethod
    def __get_cache_key_fields(cls) -> set[str]:
        cache_fields = set()
        for base in inspect.getmro(cls):
            if not issubclass(base, MetaModel):
                continue

            key_field = base.KEY_FIELD

            # _id field renaming. cache operates model props, not mongodb fields
            if key_field == "_id":
                key_field = "id"
            cache_fields.add(key_field)
            if base.CACHE_KEY_FIELDS:
                cache_fields.update(base.CACHE_KEY_FIELDS)
        return cache_fields
