from django import forms
from django.contrib import admin, messages
from django.core.exceptions import ImproperlyConfigured
from django.db.models.base import ModelBase
from django.http import HttpResponseRedirect
from django.shortcuts import render

from . import forms as tag_forms
from . import models as tag_models


# ##############################################################################
# ########################################################## Admin classes
# ##############################################################################


class TaggedBaseModelAdminMixin(admin.options.BaseModelAdmin):
    """
    Mixin for BaseModelAdmin subclasses which are for tagged models
    """

    def formfield_for_dbfield(self, db_field, **kwargs):
        """
        Remove the RelatedFieldWidgetWrapper from tag fields, so they don't
        display popup buttons
        """
        formfield = super(TaggedBaseModelAdminMixin, self).formfield_for_dbfield(
            db_field, request=kwargs.pop("request", None), **kwargs
        )

        if (
            isinstance(db_field, (tag_models.SingleTagField, tag_models.TagField))
            and isinstance(formfield.widget, admin.widgets.RelatedFieldWidgetWrapper)
            and isinstance(formfield.widget.widget, tag_forms.AdminTagWidget)
        ):
            formfield.widget = formfield.widget.widget
        return formfield

    def get_autocomplete_fields(self, request):
        """
        Ensure TagFields aren't listed in Django's autocomplete_fields - they don't
        play well with Django's autocomplete, and will use their own
        """
        autocomplete_fields = super().get_autocomplete_fields(request)
        safe_fields = [
            field
            for field in autocomplete_fields
            if not isinstance(
                self.opts.get_field(field),
                (tag_models.SingleTagField, tag_models.TagField),
            )
        ]
        return safe_fields


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#   ModelAdmin for Tagged models
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #


class TaggedModelAdmin(TaggedBaseModelAdminMixin, admin.ModelAdmin):
    pass


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#   ModelAdmin for TagModel models
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #


class TagModelAdmin(admin.ModelAdmin):
    list_display = ["name", "count", "protected"]
    list_filter = ["protected"]
    search_fields = ["name"]
    exclude = ["count"]
    actions = ["merge_tags"]
    prepopulated_fields = {"slug": ("name",)}

    def merge_tags(self, request, queryset):
        """
        Admin action to merge tags
        """
        # Thanks to:
        #   http://www.hoboes.com/Mimsy/hacks/django-actions-their-own-intermediate-page/

        # Create a form
        is_tree = issubclass(self.model, tag_models.TagTreeModel)

        class MergeForm(forms.Form):
            # Keep selected items in same field, admin.helpers.ACTION_CHECKBOX_NAME
            _selected_action = forms.CharField(widget=forms.MultipleHiddenInput)
            # Allow use to select from selected items
            merge_to = forms.ModelChoiceField(queryset)

        if is_tree:

            class MergeForm(MergeForm):
                # Allow to merge recursively
                merge_children = forms.BooleanField(required=False)

        if "merge" in request.POST:
            merge_form = MergeForm(request.POST)
            if merge_form.is_valid():
                # Merge - with children if set
                merge_to = merge_form.cleaned_data["merge_to"]
                kwargs = {}
                if is_tree and merge_form.cleaned_data["merge_children"]:
                    kwargs["children"] = True
                merge_to.merge_tags(queryset, **kwargs)

                self.message_user(request, "Tags merged", messages.SUCCESS)
                return HttpResponseRedirect(request.get_full_path())

        else:
            tag_pks = request.POST.getlist(admin.helpers.ACTION_CHECKBOX_NAME)
            if len(tag_pks) < 2:
                self.message_user(
                    request,
                    "You must select at least two tags to merge",
                    messages.ERROR,
                )
                return HttpResponseRedirect(request.get_full_path())

            merge_form = MergeForm(
                initial={
                    admin.helpers.ACTION_CHECKBOX_NAME: request.POST.getlist(
                        admin.helpers.ACTION_CHECKBOX_NAME
                    ),
                    "merge_children": True,
                }
            )

        return render(
            request,
            "tagulous/admin/merge_tags.html",
            {
                "title": "Merge tags",
                "opts": self.model._meta,
                "merge_form": merge_form,
                "tags": queryset,
            },
        )

    merge_tags.short_description = "Merge selected tags..."


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#   ModelAdmin for TagTreeModel models
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #


class TagTreeModelAdmin(TagModelAdmin):
    exclude = ["count", "parent", "path", "label", "level"]


# ##############################################################################
# ########################################################## Admin registration
# ##############################################################################

# Give contrib.admin a default widget for tag fields
admin.options.FORMFIELD_FOR_DBFIELD_DEFAULTS.update(
    {
        tag_models.SingleTagField: {"widget": tag_forms.AdminTagWidget},
        tag_models.TagField: {"widget": tag_forms.AdminTagWidget},
    }
)


def _create_display(field):
    """
    ModelAdmin display function factory
    """

    def display(self, obj):
        return getattr(obj, field).get_tag_string()

    display.short_description = field.replace("_", " ")
    return display


def enhance(model, admin_class):
    """
    Add tag support to the admin class based on the specified model
    """
    #
    # Get a list of all tag fields
    #

    # Dict of single tag fields, {name: tag}
    single_tag_fields = {}

    # Dict of normal tag fields, {name: tag}
    tag_fields = {}

    # List of all single and normal tag fields
    tag_field_names = []

    # Check for SingleTagField related fields
    for field in tag_models.singletagfields_from_model(model):
        single_tag_fields[field.name] = field
        tag_field_names.append(field.name)

    # Check for TagField m2m fields
    for field in tag_models.tagfields_from_model(model):
        tag_fields[field.name] = field
        tag_field_names.append(field.name)

    #
    # Ensure any tag fields in list_display are rendered by functions
    #

    # The admin.site.register will complain if it's a ManyToManyField, so this
    # will work around that.
    #
    # We also need to have a different name to the model field, otherwise the
    # ChangeList class will just use the model field - that would get the tag
    # strings showing in the table, but the column would be sortable which
    # would cause problems for TagFields, and the display function would never
    # get called, which would be unexpected for anyone maintaining this code.
    if hasattr(admin_class, "list_display"):
        # Make sure we're working with a list
        if isinstance(admin_class.list_display, tuple):
            admin_class.list_display = list(admin_class.list_display)

        for i, field in enumerate(admin_class.list_display):
            # If the field's not a callable, and not in the admin class already
            if not hasattr(field, "__call__") and not hasattr(admin_class, field):
                # Only TagFields (admin can already handle SingleTagField FKs)
                if field in tag_fields:
                    # Create new field name and replace in list_display
                    display_name = "_tagulous_display_%s" % field
                    admin_class.list_display[i] = display_name

                    # Add display function to admin class
                    setattr(admin_class, display_name, _create_display(field))

    #
    # If admin is for a tag model, ensure any inlines for tagged models are
    # subclasses of TaggedInlineFormSet.
    #
    if issubclass(model, tag_models.BaseTagModel) and hasattr(admin_class, "inlines"):
        for inline_cls in admin_class.inlines:
            # Make sure inline class uses TaggedBaseModelAdminMixin
            if not issubclass(inline_cls, TaggedBaseModelAdminMixin):
                inline_cls.__bases__ = (
                    TaggedBaseModelAdminMixin,
                ) + inline_cls.__bases__

            # Make sure inlines used TaggedInlineFormSet
            if issubclass(inline_cls.model, tag_models.TaggedModel) and not issubclass(
                inline_cls.formset, tag_forms.TaggedInlineFormSet
            ):
                orig_cls = inline_cls.formset
                inline_cls.formset = type(
                    str("Tagged%s" % orig_cls.__name__),
                    (tag_forms.TaggedInlineFormSet, orig_cls),
                    {},
                )


def register(model, admin_class=None, site=None, **options):
    """
    Provide tag support to the model when it is registered with the admin site.

    For tagged models (have one or more SingleTagField or TagField fields):
        * Admin will support TagField in list_display

    For tag models (subclass of TagModel):
        * Admin will provide a merge action to merge tags

    For other models:
        * No changes made

    Arguments:
        model       Model to register
        admin_class Admin class for model
        site        Admin site to register with
                    Default: django.contrib.admin.site
        **options   Extra options for admin class

    This only supports one model, but is otherwise safe to use with non-tagged
    models.
    """
    # Look at the model we've been given
    if isinstance(model, tag_models.BaseTagDescriptor):
        # It's a tag descriptor; change it for the tag model itself
        model = model.tag_model

    elif not isinstance(model, ModelBase):
        raise ImproperlyConfigured(
            "Tagulous can only register a single model with admin."
        )

    # Ensure we have a valid admin site
    if site is None:
        site = admin.site

    #
    # Determine appropriate admin class
    #
    if not admin_class:
        admin_class = admin.ModelAdmin

    # Going to make a list of base classes to inject
    cls_bases = []

    # If it's a tag model, subclass TagModelAdmin or TagTreeModelAdmin
    if issubclass(model, tag_models.BaseTagModel):
        if issubclass(model, tag_models.TagTreeModel):
            if not issubclass(admin_class, TagTreeModelAdmin):
                cls_bases += [TagTreeModelAdmin]
        else:
            if not issubclass(admin_class, TagModelAdmin):
                cls_bases += [TagModelAdmin]

    # If it's a tagged model, subclass TaggedModelAdmin
    singletagfields = tag_models.singletagfields_from_model(model)
    tagfields = tag_models.tagfields_from_model(model)
    if singletagfields or tagfields:
        if not issubclass(admin_class, TaggedModelAdmin):
            cls_bases += [TaggedModelAdmin]

    # If options specified, or other bases, will need to subclass admin_class
    if options or cls_bases:
        cls_bases += [admin_class]
        # Update options with anything the new subclasses could have overidden
        # in a custom ModelAdmin - unless they're already overridden in options
        options["__module__"] = __name__
        if admin_class != admin.ModelAdmin:
            options.update(
                dict(
                    (k, v)
                    for k, v in admin_class.__dict__.items()
                    if k in ["list_display", "list_filter", "exclude", "actions"]
                    and k not in options
                )
            )
        admin_class = type(str("%sAdmin" % model.__name__), tuple(cls_bases), options)

    # Enhance the model admin class
    enhance(model, admin_class)

    # Register the model
    # Don't pass options - we've already dealt with that
    site.register(model, admin_class)
