from typing import Any, Iterable, List, Type, TypeVar, get_type_hints, Dict
from pydantic import BaseModel, ConfigDict
from django.db import models

SerializerType = TypeVar("SerializerType", bound="BaseSerializer")


class BaseSerializer(BaseModel):
    model_config = ConfigDict(from_attributes=True, arbitrary_types_allowed=True)

    @classmethod
    def serialize(
        cls: Type[SerializerType], instance: models.Model, **kwargs: Any
    ) -> Dict[str, Any]:
        """
        Automatically serialize Django model instance, including related fields
        if a serializer is defined for the field type.
        """
        data: Dict[str, Any] = {}
        hints = get_type_hints(cls)

        for field, field_type in hints.items():
            value = getattr(instance, field, None)

            # Nested serializer
            if isinstance(value, models.Model) and issubclass(
                field_type, BaseSerializer
            ):
                data[field] = field_type.serialize(value)
            # Handle ManyToMany with serializer hints
            elif (
                isinstance(value, models.Manager)
                and getattr(field_type, "__origin__", None) == list
            ):
                inner_type = field_type.__args__[0]
                if issubclass(inner_type, BaseSerializer):
                    data[field] = [inner_type.serialize(obj) for obj in value.all()]
                else:
                    data[field] = list(value.all())
            else:
                data[field] = value

        return data

    @classmethod
    def serializer_many(
        cls: Type[SerializerType], queryset: Iterable[models.Model], **kwargs: Any
    ) -> List[Dict[str, Any]]:
        """
        Serialize a list/queryset of Django model instances.
        """
        return [cls.serialize(obj, **kwargs) for obj in queryset]


class ModelSerializer(BaseSerializer):
    """
    ModelSerializer with optional fields, exclusions, and nested serializers.

    Example:

        class ArticleSerializer(ModelSerializer):
            author: UserSerializer  # type hint for nested serialization
            class Meta:
                model = Article
                fields = ['id', 'title', 'author']  # optional
                exclude = ['created_at']  # optional
    """

    @classmethod
    def get_meta(cls) -> Type:
        if not hasattr(cls, "Meta"):
            raise ValueError("ModelSerializer requires a `Meta` inner class.")
        meta = cls.Meta
        if not hasattr(meta, "model"):
            raise ValueError("Meta class requires a `model` attribute.")
        return meta

    @classmethod
    def serialize(
        cls: Type[SerializerType], instance: models.Model, **kwargs: Any
    ) -> Dict[str, Any]:
        meta = cls.get_meta()
        model = meta.model

        hints = get_type_hints(cls)
        data: Dict[str, Any] = {}

        include_fields: List[str] | None = getattr(meta, "fields", None)
        exclude_fields: List[str] = getattr(meta, "exclude", [])

        for field in model._meta.get_fields():
            name = field.name

            # Apply fields/exclude logic
            if include_fields and name not in include_fields:
                continue
            if exclude_fields and name in exclude_fields:
                continue

            value = getattr(instance, name, None)

            # Nested serializer via type hints
            if (
                name in hints
                and isinstance(value, models.Model)
                and issubclass(hints[name], BaseSerializer)
            ):
                data[name] = hints[name].serialize(value)
            # ForeignKey / OneToOne fallback
            elif isinstance(field, (models.ForeignKey, models.OneToOneField)):
                data[name] = value.pk if value else None
            # ManyToMany
            elif isinstance(field, models.ManyToManyField):
                if (
                    name in hints
                    and hasattr(hints[name], "__args__")
                    and issubclass(hints[name].__args__[0], BaseSerializer)
                ):
                    data[name] = [
                        hints[name].__args__[0].serialize(obj) for obj in value.all()
                    ]
                else:
                    data[name] = list(value.all())
            else:
                data[name] = value

        return data
