from datetime import datetime

from django import forms
from django.conf.urls import url
from django.contrib import admin, messages
from django.contrib.admin.checks import ModelAdminChecks
from django.contrib.admin.options import get_content_type_for_model
from django.contrib.admin.templatetags.admin_static import static
from django.contrib.admin.utils import unquote
from django.contrib.admin.widgets import AdminSplitDateTime
from django.core.exceptions import PermissionDenied
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.template.response import TemplateResponse
from django.utils.encoding import force_text
from django.utils.html import format_html
from django.utils.http import urlquote
from django.utils.text import capfirst
from django.utils.translation import ugettext as _


class DateTimeFilterForm(forms.Form):
    def __init__(self, request, *args, **kwargs):
        field_name = kwargs.pop('field_name')
        super(DateTimeFilterForm, self).__init__(*args, **kwargs)
        self.request = request
        self.fields['%s_as_of' % field_name] = forms.SplitDateTimeField(
            label='',
            input_time_formats=['%I:%M %p', '%H:%M:%S'],
            widget=AdminSplitDateTime(
                attrs={'placeholder': 'as of date and time'}
            ),
            localize=True,
            required=True
        )

    @property
    def media(self):
        try:
            if getattr(self.request, 'daterange_filter_media_included'):
                return forms.Media()
        except AttributeError:
            setattr(self.request, 'daterange_filter_media_included', True)

            js = ['calendar.js', 'admin/DateTimeShortcuts.js', ]
            css = ['widgets.css', ]

            return forms.Media(
                js=[static('admin/js/%s' % path) for path in js],
                css={'all': [static('admin/css/%s' % path) for path in css]}
            )


class DateTimeFilter(admin.FieldListFilter):
    template = 'versions/datetimefilter.html'
    title = 'DateTime filter'

    def __init__(self, field, request, params, model, model_admin, field_path):
        self.field_path = field_path
        self.lookup_kwarg_as_ofdate = '%s_as_of_0' % field_path
        self.lookup_kwarg_as_oftime = '%s_as_of_1' % field_path
        super(DateTimeFilter, self).__init__(field, request, params, model,
                                             model_admin, field_path)
        self.form = self.get_form(request)

    def choices(self, cl):
        return []

    def expected_parameters(self):
        return [self.lookup_kwarg_as_ofdate, self.lookup_kwarg_as_oftime]

    def get_form(self, request):
        return DateTimeFilterForm(request, data=self.used_parameters,
                                  field_name=self.field_path)

    def queryset(self, request, queryset):
        fieldname = '%s_as_of' % self.field_path
        if self.form.is_valid() and fieldname in self.form.cleaned_data:
            filter_params = self.form.cleaned_data.get(fieldname,
                                                       datetime.utcnow())
            return queryset.as_of(filter_params)
        else:
            return queryset


class IsCurrentFilter(admin.SimpleListFilter):
    title = 'Is Current filter'
    parameter_name = 'is_current'

    def __init__(self, request, params, model, model_admin):
        self.lookup_kwarg = 'is_current'
        self.lookup_val = request.GET.get(self.lookup_kwarg, None)
        super(IsCurrentFilter, self).__init__(request, params, model,
                                              model_admin)

    def lookups(self, request, model_admin):
        return [(None, 'All'), ('1', 'Current'), ]

    def choices(self, cl):
        for lookup, title in self.lookup_choices:
            yield {
                'selected': self.value() == lookup,
                'query_string': cl.get_query_string({
                    self.parameter_name: lookup,
                }, []),
                'display': title,
            }

    def queryset(self, request, queryset):
        if self.lookup_val:
            return queryset.as_of()
        else:
            return queryset


class VersionedAdminChecks(ModelAdminChecks):
    def _check_exclude(self, cls, model=None):
        """
        Required to suppress error about exclude not being a tuple since we
        are using @property to dynamically change it
        """
        return []


class VersionedAdmin(admin.ModelAdmin):
    """
    VersionedAdmin provides functionality to allow cloning of objects when
    saving, not cloning if a mistake was made, and making a current object
    historical by deleting it
    """

    VERSIONED_EXCLUDE = ['id', 'identity', 'version_end_date',
                         'version_start_date', 'version_birth_date']

    # These are so that the subclasses can overwrite these attributes
    # to have the identity, end date, or start date column not show
    list_display_show_identity = True
    list_display_show_end_date = True
    list_display_show_start_date = True
    ordering = []

    checks_class = VersionedAdminChecks

    def get_readonly_fields(self, request, obj=None):
        """
        This is required a subclass of VersionedAdmin has readonly_fields
        ours won't be undone
        """
        if obj:
            return list(self.readonly_fields) + ['id', 'identity',
                                                 'is_current']
        return self.readonly_fields

    def get_ordering(self, request):
        return ['identity', '-version_start_date', ] + self.ordering

    def get_list_display(self, request):
        """
        This method determines which fields go in the changelist
        """

        # Force cast to list as super get_list_display could return a tuple
        list_display = list(
            super(VersionedAdmin, self).get_list_display(request))

        # Preprend the following fields to list display
        if self.list_display_show_identity:
            list_display = ['identity_shortener', ] + list_display

        # Append the following fields to list display
        if self.list_display_show_start_date:
            list_display += ['version_start_date', ]
        if self.list_display_show_end_date:
            list_display += ['version_end_date', ]

        return list_display + ['is_current', ]

    def get_list_filter(self, request):
        """
        Adds versionable custom filtering ability to changelist
        """
        list_filter = super(VersionedAdmin, self).get_list_filter(request)
        return list(list_filter) + [('version_start_date', DateTimeFilter),
                                    IsCurrentFilter]

    def restore(self, request, *args, **kwargs):
        """
        View for restoring object from change view
        """
        paths = request.path_info.split('/')
        object_id_index = paths.index("restore") - 2
        object_id = paths[object_id_index]

        obj = super(VersionedAdmin, self).get_object(request, object_id)
        obj.restore()
        admin_wordIndex = object_id_index - 3
        path = "/%s" % ("/".join(paths[admin_wordIndex:object_id_index]))

        opts = self.model._meta
        msg_dict = {
            'name': force_text(opts.verbose_name),
            'obj': format_html('<a href="{}">{}</a>',
                               urlquote(request.path), obj),
        }

        msg = format_html(_('The {name} "{obj}" was restored successfully.'),
                          **msg_dict)
        self.message_user(request, msg, messages.SUCCESS)
        return HttpResponseRedirect(path)

    def will_not_clone(self, request, *args, **kwargs):
        """
        Add save but not clone capability in the changeview
        """
        paths = request.path_info.split('/')
        index_of_object_id = paths.index("will_not_clone") - 1
        object_id = paths[index_of_object_id]
        self.change_view(request, object_id)

        admin_wordInUrl = index_of_object_id - 3
        # This gets the adminsite for the app, and the model name and joins
        # together with /
        path = '/' + '/'.join(paths[admin_wordInUrl:index_of_object_id])
        return HttpResponseRedirect(path)

    @property
    def exclude(self):
        """
        Custom descriptor for exclude since there is no get_exclude method to
        be overridden
        """
        exclude = self.VERSIONED_EXCLUDE

        if super(VersionedAdmin, self).exclude is not None:
            # Force cast to list as super exclude could return a tuple
            exclude = list(super(VersionedAdmin, self).exclude) + exclude

        return exclude

    def get_object(self, request, object_id, from_field=None):
        """
        our implementation of get_object allows for cloning when updating an
        object, not cloning when the button 'save but not clone' is pushed
        and at no other time will clone be called
        """
        # from_field breaks in 1.7.8
        obj = super(VersionedAdmin, self).get_object(request,
                                                     object_id)
        # Only clone if update view as get_object() is also called for change,
        # delete, and history views
        if request.method == 'POST' and \
                obj and \
                obj.is_latest and \
                'will_not_clone' not in request.path and \
                'delete' not in request.path and \
                'restore' not in request.path:
            obj = obj.clone()

        return obj

    def history_view(self, request, object_id, extra_context=None):
        "The 'history' admin view for this model."
        from django.contrib.admin.models import LogEntry
        # First check if the user can see this history.
        model = self.model
        obj = get_object_or_404(self.get_queryset(request),
                                pk=unquote(object_id))
        if not self.has_change_permission(request, obj):
            raise PermissionDenied

        # Then get the history for this object.
        opts = model._meta
        app_label = opts.app_label
        action_list = LogEntry.objects.filter(
            object_id=unquote(str(obj.identity)),
            # this is the change for our override;
            content_type=get_content_type_for_model(model)
        ).select_related().order_by('action_time')
        ctx = self.admin_site.each_context(request)

        context = dict(ctx,
                       title=('Change history: %s') % force_text(obj),
                       action_list=action_list,
                       module_name=capfirst(
                           force_text(opts.verbose_name_plural)),
                       object=obj,
                       opts=opts,
                       preserved_filters=self.get_preserved_filters(request),
                       )
        context.update(extra_context or {})
        return TemplateResponse(request, self.object_history_template or [
            "admin/%s/%s/object_history.html" % (app_label, opts.model_name),
            "admin/%s/object_history.html" % app_label,
            "admin/object_history.html"
        ], context)

    def get_urls(self):
        """
        Appends the custom will_not_clone url to the admin site
        """
        not_clone_url = [url(r'^(.+)/will_not_clone/$',
                             admin.site.admin_view(self.will_not_clone))]
        restore_url = [
            url(r'^(.+)/restore/$', admin.site.admin_view(self.restore))]
        return not_clone_url + restore_url + super(VersionedAdmin,
                                                   self).get_urls()

    def is_current(self, obj):
        return obj.is_current

    is_current.boolean = True
    is_current.short_description = "Current"

    def identity_shortener(self, obj):
        """
        Shortens identity to the last 12 characters
        """
        return "..." + str(obj.identity)[-12:]

    identity_shortener.boolean = False
    identity_shortener.short_description = "Short Identity"

    class Media:
        # This supports dynamically adding 'Save without cloning' button:
        # http://bit.ly/1T2fGOP
        js = ('js/admin_addon.js',)