"""
Rendering utils for admin forms;

This makes sure that admin fieldsets/layout settings are exported to the template.
"""

import json
from collections.abc import Iterator
from typing import Any, cast

from django.contrib.admin.helpers import AdminField, InlineAdminForm, InlineAdminFormSet
from django.http import HttpRequest
from django.utils.encoding import force_str
from django.utils.text import capfirst
from django.utils.translation import gettext

from polymorphic.formsets import BasePolymorphicModelFormSet


class PolymorphicInlineAdminForm(InlineAdminForm):
    """
    Expose the admin configuration for a form
    """

    def polymorphic_ctype_field(self) -> AdminField:
        return AdminField(self.form, "polymorphic_ctype", False)

    @property
    def is_empty(self) -> bool:
        if not self.form.prefix:
            return False
        return "__prefix__" in self.form.prefix


class PolymorphicInlineAdminFormSet(InlineAdminFormSet):
    """
    Internally used class to expose the formset in the template.
    """

    request: HttpRequest | None
    obj: Any | None

    def __init__(self, *args: Any, **kwargs: Any) -> None:
        # Assigned later via PolymorphicInlineSupportMixin later.
        self.request = kwargs.pop("request", None)
        self.obj = kwargs.pop("obj", None)
        super().__init__(*args, **kwargs)

    def __iter__(self) -> Iterator[PolymorphicInlineAdminForm]:
        """
        Output all forms using the proper subtype settings.
        """
        for form, original in zip(self.formset.initial_forms, self.formset.get_queryset()):
            # Output the form
            model = original.get_real_instance_class()
            child_inline = self.opts.get_child_inline_instance(model)
            view_on_site_url = self.opts.get_view_on_site_url(original)

            yield PolymorphicInlineAdminForm(
                formset=self.formset,
                form=form,
                fieldsets=self.get_child_fieldsets(child_inline),
                prepopulated_fields=self.get_child_prepopulated_fields(child_inline),
                original=original,
                readonly_fields=self.get_child_readonly_fields(child_inline),
                model_admin=child_inline,
                view_on_site_url=view_on_site_url,
            )

        # Extra rows, and empty prefixed forms.
        for form in self.formset.extra_forms + self.formset.empty_forms:
            model = form._meta.model
            child_inline = self.opts.get_child_inline_instance(model)
            yield PolymorphicInlineAdminForm(
                formset=self.formset,
                form=form,
                fieldsets=self.get_child_fieldsets(child_inline),
                prepopulated_fields=self.get_child_prepopulated_fields(child_inline),
                original=None,
                readonly_fields=self.get_child_readonly_fields(child_inline),
                model_admin=child_inline,
            )

    def get_child_fieldsets(self, child_inline: Any) -> list[tuple[str | None, dict[str, Any]]]:
        return list(child_inline.get_fieldsets(self.request, self.obj) or ())

    def get_child_readonly_fields(self, child_inline: Any) -> list[str]:
        return list(child_inline.get_readonly_fields(self.request, self.obj))

    def get_child_prepopulated_fields(self, child_inline: Any) -> dict[str, Any]:
        fields = self.prepopulated_fields.copy()
        fields.update(child_inline.get_prepopulated_fields(self.request, self.obj))
        return fields

    def inline_formset_data(self) -> str:
        """
        A JavaScript data structure for the JavaScript code
        This overrides the default Django version to add the ``childTypes`` data.
        """
        verbose_name = self.opts.verbose_name
        return json.dumps(
            {
                "name": f"#{self.formset.prefix}",
                "options": {
                    "prefix": self.formset.prefix,
                    "addText": gettext("Add another %(verbose_name)s")
                    % {"verbose_name": capfirst(verbose_name)},
                    "childTypes": [
                        {
                            "type": model._meta.model_name,
                            "name": force_str(model._meta.verbose_name),
                        }
                        for model in self.formset.child_forms.keys()
                    ],
                    "deleteText": gettext("Remove"),
                },
            }
        )


class PolymorphicInlineSupportMixin:
    """
    A Mixin to add to the regular admin, so it can work with our polymorphic inlines.

    This mixin needs to be included in the admin that hosts the ``inlines``.
    It makes sure the generated admin forms have different fieldsets/fields
    depending on the polymorphic type of the form instance.

    This is achieved by overwriting :func:`get_inline_formsets` to return
    an :class:`PolymorphicInlineAdminFormSet` instead of a standard Django
    :class:`~django.contrib.admin.helpers.InlineAdminFormSet` for the polymorphic formsets.
    """

    def get_inline_formsets(
        self,
        request: HttpRequest,
        formsets: list[Any],
        inline_instances: list[Any],
        obj: Any = None,
        *args: Any,
        **kwargs: Any,
    ) -> list[InlineAdminFormSet]:
        """
        Overwritten version to produce the proper admin wrapping for the
        polymorphic inline formset. This fixes the media and form appearance
        of the inline polymorphic models.
        """
        inline_admin_formsets = super().get_inline_formsets(  # type: ignore[misc]
            request, formsets, inline_instances, obj=obj
        )

        for admin_formset in inline_admin_formsets:
            if isinstance(admin_formset.formset, BasePolymorphicModelFormSet):
                # This is a polymorphic formset, which belongs to our inline.
                # Downcast the admin wrapper that generates the form fields.
                admin_formset.__class__ = PolymorphicInlineAdminFormSet
                admin_formset.request = request
                admin_formset.obj = obj
        return cast(list[InlineAdminFormSet], inline_admin_formsets)
