from __future__ import annotations

from typing import Any

from django.db import models

from django_anyllm.registry import ModelRegistry


def _get_field_info(field: models.Field) -> dict[str, Any]:
    """Build a metadata dictionary for a single model field or relation."""
    info: dict[str, Any] = {
        "name": field.name,
        "type": field.__class__.__name__,
    }

    # Reverse relations (e.g. ManyToOneRel) have a smaller API.
    if hasattr(field, "null"):
        info["nullable"] = field.null
    if hasattr(field, "blank"):
        info["blank"] = field.blank
    if hasattr(field, "editable"):
        info["editable"] = field.editable
    if hasattr(field, "primary_key"):
        info["primary_key"] = field.primary_key
    if hasattr(field, "verbose_name"):
        info["verbose_name"] = str(field.verbose_name)

    if hasattr(field, "help_text") and field.help_text:
        info["help_text"] = str(field.help_text)

    if hasattr(field, "max_length") and field.max_length is not None:
        info["max_length"] = field.max_length

    if hasattr(field, "choices") and field.choices:
        info["choices"] = [
            {"value": value, "display": display} for value, display in field.choices
        ]

    if hasattr(field, "default") and field.default is not models.NOT_PROVIDED:
        info["default"] = field.default

    if isinstance(field, models.ForeignKey | models.OneToOneField):
        info["relationship"] = {
            "type": "ForeignKey"
            if isinstance(field, models.ForeignKey)
            else "OneToOneField",
            "to": field.remote_field.model._meta.label,
            "related_name": field.remote_field.name,
        }
    elif isinstance(field, models.ManyToManyField):
        info["relationship"] = {
            "type": "ManyToManyField",
            "to": field.remote_field.model._meta.label,
            "related_name": field.remote_field.name,
        }
    elif field.is_relation and hasattr(field, "remote_field"):
        # Reverse relations (ManyToOneRel, ManyToManyRel, OneToOneRel)
        info["relationship"] = {
            "type": field.__class__.__name__,
            "to": field.remote_field.model._meta.label,
            "related_name": field.remote_field.name,
        }

    return info


def list_registered_models(registry: ModelRegistry) -> list[dict[str, Any]]:
    """List all models registered with django-anyllm.

    Returns:
        A list of model metadata dictionaries containing app_label, model_name,
        verbose names, docstring, and allowed permissions.
    """
    return registry.list_models()


def describe_model(
    registry: ModelRegistry, app_label: str, model_name: str
) -> dict[str, Any]:
    """Describe the structure and metadata of a registered Django model.

    This includes every field with its type, constraints, choices, help_text,
    verbose_name, and relationship details.

    Args:
        app_label: The Django app label.
        model_name: The lower-case model name.

    Returns:
        A detailed metadata dictionary for the model.
    """
    model = registry.get_model(app_label, model_name)
    meta = model._meta

    fields = []
    relationships = []

    for field in meta.get_fields():
        if field.concrete:
            fields.append(_get_field_info(field))
        elif field.is_relation:
            relationships.append(_get_field_info(field))

    return {
        "app_label": meta.app_label,
        "model_name": meta.model_name,
        "verbose_name": str(meta.verbose_name),
        "verbose_name_plural": str(meta.verbose_name_plural),
        "docstring": (model.__doc__ or "").strip(),
        "db_table": meta.db_table,
        "ordering": list(meta.ordering) if meta.ordering else [],
        "fields": fields,
        "relationships": relationships,
        "permissions": sorted(
            registry.get_registration(app_label, model_name).permissions
        ),
    }


def _field_to_json_schema(field: models.Field) -> dict[str, Any]:
    """Map a Django field to a JSON-schema-like property dict."""
    schema: dict[str, Any] = {}

    description_parts: list[str] = []
    if field.verbose_name and field.verbose_name != field.name.replace("_", " "):
        description_parts.append(str(field.verbose_name))
    if field.help_text:
        description_parts.append(str(field.help_text))
    if description_parts:
        schema["description"] = " — ".join(description_parts)

    if field.choices:
        schema["enum"] = [choice[0] for choice in field.choices]
        schema["type"] = "string" if isinstance(field, models.CharField) else "integer"
        return schema

    type_map: dict[type[models.Field], str] = {
        models.CharField: "string",
        models.TextField: "string",
        models.SlugField: "string",
        models.URLField: "string",
        models.EmailField: "string",
        models.UUIDField: "string",
        models.IntegerField: "integer",
        models.PositiveIntegerField: "integer",
        models.PositiveSmallIntegerField: "integer",
        models.SmallIntegerField: "integer",
        models.BigIntegerField: "integer",
        models.BigAutoField: "integer",
        models.AutoField: "integer",
        models.FloatField: "number",
        models.DecimalField: "number",
        models.BooleanField: "boolean",
        models.DateTimeField: "string",
        models.DateField: "string",
        models.TimeField: "string",
        models.JSONField: "object",
    }

    for field_cls, json_type in type_map.items():
        if isinstance(field, field_cls):
            schema["type"] = json_type
            break
    else:
        schema["type"] = "string"

    if isinstance(field, models.DateTimeField):
        schema["format"] = "date-time"
    elif isinstance(field, models.DateField):
        schema["format"] = "date"
    elif isinstance(field, models.TimeField):
        schema["format"] = "time"

    if hasattr(field, "max_length") and field.max_length is not None:
        schema["maxLength"] = field.max_length

    if not field.null and field.default is models.NOT_PROVIDED:
        # We don't mark required here; that's handled at the schema root.
        pass

    return schema


def get_model_schema(
    registry: ModelRegistry, app_label: str, model_name: str
) -> dict[str, Any]:
    """Return a JSON-schema-like representation of a registered model.

    This is useful for structured LLM responses or JSON-mode completions.

    Args:
        app_label: The Django app label.
        model_name: The lower-case model name.

    Returns:
        A JSON Schema object describing the model's fields.
    """
    model = registry.get_model(app_label, model_name)
    meta = model._meta

    properties: dict[str, Any] = {}
    required: list[str] = []

    for field in meta.concrete_fields:
        properties[field.name] = _field_to_json_schema(field)
        if (
            not field.null
            and field.default is models.NOT_PROVIDED
            and not field.auto_created
            and not getattr(field, "auto_now", False)
            and not getattr(field, "auto_now_add", False)
        ):
            required.append(field.name)

    description_parts: list[str] = []
    if meta.verbose_name:
        description_parts.append(str(meta.verbose_name))
    if model.__doc__:
        description_parts.append(model.__doc__.strip())

    return {
        "type": "object",
        "title": meta.model_name,
        "description": " — ".join(description_parts) if description_parts else "",
        "properties": properties,
        "required": required,
    }
