"""
Admin site models for managing :class:`.ConfigurationModel` subclasses.
"""


from django.contrib import admin
from django.contrib.admin import ListFilter
from django.core.cache import InvalidCacheBackendError, caches
from django.core.files.base import File
from django.forms import models
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import ugettext_lazy as _

try:
    cache = caches['configuration']
except InvalidCacheBackendError:
    from django.core.cache import cache


class ConfigurationModelAdmin(admin.ModelAdmin):
    """
    :class:`~django.contrib.admin.ModelAdmin` for :class:`.ConfigurationModel` subclasses
    """

    date_hierarchy = 'change_date'

    def get_actions(self, request):
        """
        Get the actions.
        """
        return {
            'revert': (ConfigurationModelAdmin.revert, 'revert', _('Revert to the selected configuration'))
        }

    def get_list_display(self, request):
        """
        Get the list display.
        """
        return self.get_displayable_field_names()

    def get_displayable_field_names(self):
        """
        Return all field names, excluding reverse foreign key relationships.
        """
        return [
            f.name
            for f in self.model._meta.get_fields()
            if not f.one_to_many
        ]

    # Don't allow deletion of configuration
    def has_delete_permission(self, request, obj=None):
        return False

    # Make all fields read-only when editing an object
    def get_readonly_fields(self, request, obj=None):
        if obj:  # editing an existing object
            return self.get_displayable_field_names()
        return self.readonly_fields

    def add_view(self, request, form_url='', extra_context=None):
        # Prepopulate new configuration entries with the value of the current config
        get = request.GET.copy()
        get.update(models.model_to_dict(self.model.current()))
        request.GET = get
        return super().add_view(request, form_url, extra_context)

    # Hide the save buttons in the change view
    def change_view(self, request, object_id, form_url='', extra_context=None):
        extra_context = extra_context or {}
        extra_context['readonly'] = True
        return super().change_view(
            request,
            object_id,
            form_url,
            extra_context=extra_context
        )

    def save_model(self, request, obj, form, change):
        obj.changed_by = request.user
        super().save_model(request, obj, form, change)
        cache.delete(obj.cache_key_name(*(getattr(obj, key_name) for key_name in obj.KEY_FIELDS)))
        cache.delete(obj.key_values_cache_key_name())

    def revert(self, request, queryset):
        """
        Admin action to revert a configuration back to the selected value
        """
        if queryset.count() != 1:
            self.message_user(request, _("Please select a single configuration to revert to."))
            return None

        target = queryset[0]
        target.id = None
        self.save_model(request, target, None, False)
        self.message_user(request, _("Reverted configuration."))

        return HttpResponseRedirect(
            reverse(
                'admin:{}_{}_change'.format(
                    self.model._meta.app_label,
                    self.model._meta.model_name,
                ),
                args=(target.id,),
            )
        )


class ShowHistoryFilter(ListFilter):
    """
    Admin change view filter to show only the most recent (i.e. the "current") row for each
    unique key value.
    """
    title = _('Status')
    parameter_name = 'show_history'

    def __init__(self, request, params, model, model_admin):
        super().__init__(request, params, model, model_admin)
        if self.parameter_name in params:
            value = params.pop(self.parameter_name)
            self.used_parameters[self.parameter_name] = value

    def has_output(self):
        """ Should this filter be shown? """
        return True

    def choices(self, changelist):
        """ Returns choices ready to be output in the template. """
        show_all = self.used_parameters.get(self.parameter_name) == "1"
        return (
            {
                'display': _('Current Configuration'),
                'selected': not show_all,
                'query_string': changelist.get_query_string({}, [self.parameter_name]),
            },
            {
                'display': _('All (Show History)'),
                'selected': show_all,
                'query_string': changelist.get_query_string({self.parameter_name: "1"}, []),
            }
        )

    def queryset(self, request, queryset):
        """ Filter the queryset. No-op since it's done by KeyedConfigurationModelAdmin """
        return queryset

    def expected_parameters(self):
        """ List the query string params used by this filter """
        return [self.parameter_name]


class KeyedConfigurationModelAdmin(ConfigurationModelAdmin):
    """
    :class:`~django.contrib.admin.ModelAdmin` for :class:`.ConfigurationModel` subclasses that
    use extra keys (i.e. they have KEY_FIELDS set).
    """
    date_hierarchy = None
    list_filter = (ShowHistoryFilter, )

    def get_queryset(self, request):
        """
        Annote the queryset with an 'is_active' property that's true iff that row is the most
        recently added row for that particular set of KEY_FIELDS values.
        Filter the queryset to show only is_active rows by default.
        """
        if request.GET.get(ShowHistoryFilter.parameter_name) == '1':
            queryset = self.model.objects.with_active_flag()
        else:
            # Show only the most recent row for each key.
            queryset = self.model.objects.current_set()
        ordering = self.get_ordering(request)
        if ordering:
            return queryset.order_by(*ordering)
        return queryset

    def get_list_display(self, request):
        """ Add a link to each row for creating a new row using the chosen row as a template """
        return self.get_displayable_field_names() + ['edit_link']

    def add_view(self, request, form_url='', extra_context=None):
        # Prepopulate new configuration entries with the value of the current config, if given:
        if 'source' in request.GET:
            get = request.GET.copy()
            source_id = int(get.pop('source')[0])
            source = get_object_or_404(self.model, pk=source_id)
            source_dict = models.model_to_dict(source)
            for field_name, field_value in source_dict.items():
                # read files into request.FILES, if:
                # * user hasn't ticked the "clear" checkbox
                # * user hasn't uploaded a new file
                if field_value and isinstance(field_value, File):
                    clear_checkbox_name = f'{field_name}-clear'
                    if request.POST.get(clear_checkbox_name) != 'on':
                        request.FILES.setdefault(field_name, field_value)
                get[field_name] = field_value
            request.GET = get
        # Call our grandparent's add_view, skipping the parent code
        # because the parent code has a different way to prepopulate new configuration entries
        # with the value of the latest config, which doesn't make sense for keyed models.
        # pylint: disable=bad-super-call
        return super(ConfigurationModelAdmin, self).add_view(request, form_url, extra_context)

    def edit_link(self, inst):
        """ Edit link for the change view """
        if not inst.is_active:
            return '--'
        update_url = reverse(f'admin:{self.model._meta.app_label}_{self.model._meta.model_name}_add')
        update_url += f"?source={inst.pk}"
        return format_html('<a href="{}">{}</a>', update_url, _('Update'))
    edit_link.short_description = _('Update')
