from __future__ import annotations

from typing import Any

from django.db import models


def serialize_instance(instance: models.Model, depth: int = 0) -> dict[str, Any]:
    """Convert a Django model instance into a JSON-safe dictionary.

    Related fields are serialised by primary key only to avoid infinite
    recursion unless *depth* is greater than zero.

    Args:
        instance: The model instance to serialise.
        depth: How many levels of related objects to expand.

    Returns:
        A dictionary representation of the instance.
    """
    result: dict[str, Any] = {"pk": instance.pk}
    for field in instance._meta.concrete_fields:
        value = getattr(instance, field.attname)

        if isinstance(field, models.ForeignKey):
            if depth > 0 and value is not None:
                related = getattr(instance, field.name)
                result[field.name] = serialize_instance(related, depth - 1)
            else:
                result[field.name] = value
        elif isinstance(field, models.ManyToManyField):
            result[field.name] = list(
                getattr(instance, field.name).values_list("pk", flat=True)
            )
        elif isinstance(
            field, models.DateTimeField | models.DateField | models.TimeField
        ):
            result[field.name] = value.isoformat() if value else None
        elif hasattr(field, "choices") and field.choices:
            choices_dict = dict(field.flatchoices)
            result[field.name] = {
                "value": value,
                "display": choices_dict.get(value, value),
            }
        else:
            result[field.name] = value

    return result


def deserialize_data(model: type[models.Model], data: dict[str, Any]) -> dict[str, Any]:
    """Validate and clean incoming data for creating or updating a model instance.

    Read-only fields (auto_now, auto_now_add, non-editable primary keys) are
    stripped automatically.

    Args:
        model: The target Django model class.
        data: The raw data dictionary from the caller.

    Returns:
        A cleaned dictionary safe to pass to ``model.objects.create`` or
        ``instance.save``.

    Raises:
        ValueError: If an unknown field is provided.
    """
    cleaned: dict[str, Any] = {}
    valid_fields = {f.name for f in model._meta.concrete_fields}

    for key, value in data.items():
        if key not in valid_fields:
            msg = f"Unknown field '{key}' for {model._meta.label}."
            raise ValueError(msg)

        field = model._meta.get_field(key)

        if getattr(field, "auto_now", False) or getattr(field, "auto_now_add", False):
            continue

        if field.primary_key and not field.editable:
            continue

        # For ForeignKeys, pass raw PKs via the underlying attname so Django
        # doesn't try to resolve them through the descriptor.
        if isinstance(field, models.ForeignKey | models.OneToOneField):
            if not isinstance(value, models.Model):
                cleaned[field.attname] = value
                continue

        cleaned[key] = value

    return cleaned
