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(metaclass=DefaultCollectionMeta):

    _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  # 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
            cache_fields.add(base.KEY_FIELD)
            if base.CACHE_KEY_FIELDS:
                cache_fields.update(base.CACHE_KEY_FIELDS)
        return cache_fields
