from collections import defaultdict

from django.apps import apps

DEFAULT_EXCLUDED_APPS = frozenset({"contenttypes", "sessions", "admin", "auth", "django_lumen"})


def get_app_models(excluded=DEFAULT_EXCLUDED_APPS):
    """Return all models whose app_label is not in *excluded*."""
    excluded = frozenset(excluded)
    return [
        model
        for model in apps.get_models()
        if model._meta.app_label not in excluded
    ]


def _collect_choices(field) -> list[dict] | None:
    choices = getattr(field, 'choices', None)
    if not choices:
        return None
    result = []
    for value, label in choices:
        if isinstance(label, (list, tuple)):
            # Grouped choices: value is the group name, label is [(val, display), ...]
            for sub_value, sub_label in label:
                result.append({"value": str(sub_value), "label": str(sub_label)})
        else:
            result.append({"value": str(value), "label": str(label)})
    return result or None


def _collect_indexes(meta) -> list[dict]:
    indexes = []
    if meta.pk:
        indexes.append({"type": "pk", "fields": [meta.pk.name], "name": "", "unique": True})
    for field in meta.local_fields:
        if field.primary_key:
            continue
        if field.unique:
            indexes.append({"type": "unique", "fields": [field.name], "name": "", "unique": True})
        elif field.db_index:
            indexes.append({"type": "index", "fields": [field.name], "name": "", "unique": False})
    for fields in meta.unique_together:
        indexes.append({"type": "unique", "fields": list(fields), "name": "", "unique": True})
    for idx in meta.indexes:
        indexes.append({"type": "index", "fields": list(idx.fields), "name": idx.name or "", "unique": False})
    for c in meta.constraints:
        if hasattr(c, "fields") and c.fields:
            indexes.append({"type": "unique", "fields": list(c.fields), "name": c.name or "", "unique": True})
    return indexes


def get_diagram_data(models) -> dict:
    """
    Return structured diagram data ready to be JSON-serialised and passed to
    the template.  Each field carries relation metadata when applicable.
    """
    by_app: dict[str, list] = defaultdict(list)
    for model in models:
        by_app[model._meta.app_label].append(model)

    result: list[dict] = []

    for app_label in sorted(by_app):
        for model in by_app[app_label]:
            meta = model._meta
            fields = []

            # local_fields + local_many_to_many contains only fields whose
            # columns belong to this model's own DB table, excluding anything
            # pulled in via inheritance.
            for field in [*meta.local_fields, *meta.local_many_to_many]:
                field_data: dict = {
                    "name": field.name,
                    "type": field.get_internal_type(),
                    "relation": None,
                    "verbose_name": str(field.verbose_name),
                    "help_text": str(field.help_text),
                    "choices": _collect_choices(field),
                }

                if field.is_relation and field.related_model:
                    rm = field.related_model._meta
                    field_data["relation"] = {
                        "to": f"{rm.app_label}.{rm.object_name}",
                        "kind": field.get_internal_type(),
                    }

                fields.append(field_data)

            result.append({
                "key":     f"{app_label}.{meta.object_name}",
                "name":    meta.object_name,
                "app":     app_label,
                "managed": meta.managed,
                "fields":  fields,
                "indexes": _collect_indexes(meta),
            })

    return {"models": result}
