from dataclasses import dataclass, field as dataclass_field
from typing import Optional

from django.db import models


@dataclass
class FieldInfo:
    name: str
    column: str
    ts_type: str
    descriptor_class: str
    is_nullable: bool = False
    has_default: bool = False
    is_primary_key: bool = False
    is_relation: bool = False
    is_reverse_relation: bool = False
    choices: Optional[list[str]] = None
    choice_type_name: Optional[str] = None
    related_model: Optional[str] = None
    related_pk_type: Optional[str] = None
    descriptor_extra_args: list[str] = dataclass_field(default_factory=list)


# Maps Django field class names to (ts_type, descriptor_class)
FIELD_TYPE_MAP: dict[str, tuple[str, str]] = {
    "CharField": ("string", "StringField"),
    "TextField": ("string", "StringField"),
    "SlugField": ("string", "StringField"),
    "EmailField": ("string", "StringField"),
    "URLField": ("string", "StringField"),
    "FilePathField": ("string", "StringField"),
    "IntegerField": ("number", "NumberField"),
    "BigIntegerField": ("number", "NumberField"),
    "SmallIntegerField": ("number", "NumberField"),
    "PositiveIntegerField": ("number", "NumberField"),
    "PositiveSmallIntegerField": ("number", "NumberField"),
    "PositiveBigIntegerField": ("number", "NumberField"),
    "FloatField": ("number", "NumberField"),
    "DecimalField": ("string", "NumberField"),
    "BooleanField": ("boolean", "BooleanField"),
    "NullBooleanField": ("boolean", "BooleanField"),
    "DateField": ("string", "DateField"),
    "DateTimeField": ("string", "DateTimeField"),
    "TimeField": ("string", "TimeField"),
    "UUIDField": ("string", "UUIDField"),
    "JSONField": ("unknown", "UUIDField"),
    "FileField": ("string", "StringField"),
    "ImageField": ("string", "StringField"),
    "AutoField": ("number", "NumberField"),
    "BigAutoField": ("number", "NumberField"),
    "SmallAutoField": ("number", "NumberField"),
    "BinaryField": ("string", "StringField"),
    "IPAddressField": ("string", "StringField"),
    "GenericIPAddressField": ("string", "StringField"),
    "DurationField": ("string", "StringField"),
}


def _get_field_class_name(field) -> str:
    """Walk up the MRO to find a class name we recognize."""
    for cls in type(field).__mro__:
        if cls.__name__ in FIELD_TYPE_MAP:
            return cls.__name__
    return "CharField"


def _make_choice_type_name(model_name: str, field_name: str) -> str:
    """Generate a TypeScript type name for a choice field's union type."""
    return f"{model_name}{field_name[0].upper()}{field_name[1:]}"


def introspect_field(field, model_name: str) -> Optional[FieldInfo]:
    """Extract type-relevant information from a Django model field."""
    # Skip reverse relations (ForeignKey reverse, M2M reverse)
    if isinstance(field, (models.fields.related.ForeignObjectRel,)):
        return None

    # ForeignKey
    if isinstance(field, models.ForeignKey):
        related_model_key = (
            f"{field.related_model._meta.app_label}.{field.related_model.__name__}"
        )
        related_pk_type = _get_pk_ts_type(field.related_model)
        return FieldInfo(
            name=field.name,
            column=field.column,
            ts_type=related_pk_type,
            descriptor_class="ForeignKeyField",
            is_nullable=field.null,
            has_default=field.has_default(),
            is_primary_key=False,
            is_relation=True,
            related_model=related_model_key,
            related_pk_type=related_pk_type,
            descriptor_extra_args=[f"'{related_model_key}'"],
        )

    # ManyToManyField
    if isinstance(field, models.ManyToManyField):
        related_model_key = (
            f"{field.related_model._meta.app_label}.{field.related_model.__name__}"
        )
        return FieldInfo(
            name=field.name,
            column=field.name,
            ts_type=f"{_get_pk_ts_type(field.related_model)}[]",
            descriptor_class="ManyToManyField",
            is_relation=True,
            related_model=related_model_key,
            descriptor_extra_args=[f"'{related_model_key}'"],
        )

    class_name = _get_field_class_name(field)
    ts_type, descriptor_class = FIELD_TYPE_MAP.get(class_name, ("string", "StringField"))

    info = FieldInfo(
        name=field.name,
        column=getattr(field, "column", field.name),
        ts_type=ts_type,
        descriptor_class=descriptor_class,
        is_nullable=getattr(field, "null", False),
        has_default=getattr(field, "has_default", lambda: False)(),
        is_primary_key=getattr(field, "primary_key", False),
    )

    # Handle choices
    if getattr(field, "choices", None):
        info.choices = [choice[0] for choice in field.flatchoices]
        info.choice_type_name = _make_choice_type_name(model_name, field.name)
        info.descriptor_class = "StringChoiceField"

    # Handle nullable
    if info.is_nullable:
        info.ts_type = f"{info.ts_type} | null"

    return info


def _get_pk_ts_type(model) -> str:
    """Get the TypeScript type for a model's primary key."""
    pk = model._meta.pk
    if pk is None:
        return "number"
    class_name = _get_field_class_name(pk)
    ts_type, _ = FIELD_TYPE_MAP.get(class_name, ("number", "NumberField"))
    return ts_type
