# -*- coding: utf-8 -*-
from __future__ import division, absolute_import, unicode_literals

import operator
from datetime import datetime
from functools import reduce

import extra_views
from django.conf import settings
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.forms import (PasswordChangeForm,
                                       AdminPasswordChangeForm)
from django.contrib.auth.views import (logout as auth_logout,
                                       login as auth_login)
from django.contrib.contenttypes.models import ContentType
from django.db import models, router
from django.db.models.fields import FieldDoesNotExist
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.utils.encoding import force_text
from django.utils.text import capfirst
from django.utils.translation import ugettext_lazy
from django.urls import reverse, reverse_lazy

from django.views import generic

from . import permissions, utils
from .filters import build_list_filter, build_date_filter
from .forms import AdminAuthenticationForm
from .models import LogEntry
from .viewmixins import Admin2Mixin, Admin2ModelMixin, Admin2ModelFormMixin


class AdminView(object):

    def __init__(self, url, view, name=None):
        self.url = url
        self.view = view
        self.name = name

    def get_view_kwargs(self):
        return {
            'app_label': self.model_admin.app_label,
            'model': self.model_admin.model,
            'model_name': self.model_admin.model_name,
            'model_admin': self.model_admin,
        }


class IndexView(Admin2Mixin, generic.TemplateView):
    """Context Variables

    :apps: A dictionary of apps, each app being a dictionary with keys
           being models and the value being djadmin2.types.ModelAdmin2
           objects.
    :app_verbose_names: A dictionary containing the app verbose names,
                        each item has a key being the `app_label` and
                        the value being a string, (or even a lazy
                        translation object), with the custom app name.
    """
    default_template_name = "index.html"
    registry = None
    apps = None
    app_verbose_names = None

    def get_context_data(self, **kwargs):
        data = super(IndexView, self).get_context_data(**kwargs)
        data.update({
            'apps': self.apps,
            'app_verbose_names': self.app_verbose_names,
        })
        return data


class AppIndexView(Admin2Mixin, generic.TemplateView):
    """Context Variables

    :app_label: Name of your app
    :registry: A dictionary of registered models for a given app, each
               item has a key being the model and the value being
               djadmin2.types.ModelAdmin2 objects.
    :app_verbose_names: A dictionary containing the app verbose name for
                        a given app, the item has a key being the
                        `app_label` and the value being a string, (or
                        even a lazy translation object), with the custom
                        app name.
    """
    default_template_name = "app_index.html"
    registry = None
    apps = None
    app_verbose_names = None

    def get_context_data(self, **kwargs):
        data = super(AppIndexView, self).get_context_data(**kwargs)
        app_label = self.kwargs['app_label']
        registry = self.apps[app_label]
        data.update({
            'app_label': app_label,
            'registry': registry,
            'app_verbose_names': self.app_verbose_names,
        })
        return data


class ModelListView(Admin2ModelMixin, generic.ListView):
    """Context Variables

    :is_paginated: If the page is paginated (page has a next button)
    :model: Type of object you are editing
    :model_name: Name of the object you are editing
    :app_label: Name of your app
    :app_verbose_names: A dictionary containing the app verbose name for
                        a given app, the item has a key being the
                        `app_label` and the value being a string, (or
                        even a lazy translation object), with the custom
                        app name.
    """
    default_template_name = "model_list.html"
    paginate_by = 10
    permission_classes = (
        permissions.IsStaffPermission,
        permissions.ModelViewPermission)

    def post(self, request):
        action_name = request.POST['action']
        action_callable = self.get_actions()[action_name]['action_callable']
        selected_model_pks = request.POST.getlist('selected_model_pk')
        if getattr(action_callable, "only_selected", True):
            queryset = self.model.objects.filter(pk__in=selected_model_pks)
        else:
            queryset = self.model.objects.all()

        #  If action_callable is a class subclassing from
        #  actions.BaseListAction then we generate the callable object.
        if hasattr(action_callable, "process_queryset"):
            response = action_callable.as_view(queryset=queryset, model_admin=self.model_admin)(request)
        else:
            # generate the reponse if a function.
            response = action_callable(request, queryset)

        if response is None:
            return HttpResponseRedirect(self.get_success_url())
        else:
            return response

    def get_search_results(self, queryset, search_term):
        # Lifted from django.contrib.admin
        def construct_search(field_name):
            if field_name.startswith('^'):
                return "%s__istartswith" % field_name[1:]
            elif field_name.startswith('='):
                return "%s__iexact" % field_name[1:]
            elif field_name.startswith('@'):
                return "%s__search" % field_name[1:]
            else:
                return "%s__icontains" % field_name

        use_distinct = False

        orm_lookups = [construct_search(str(search_field))
                       for search_field in self.model_admin.search_fields]

        for bit in search_term.split():
            or_queries = [models.Q(**{orm_lookup: bit})
                          for orm_lookup in orm_lookups]
            queryset = queryset.filter(reduce(operator.or_, or_queries))

        if not use_distinct:
            for search_spec in orm_lookups:
                opts = utils.model_options(self.get_model())
                if utils.lookup_needs_distinct(opts, search_spec):
                    use_distinct = True
                    break

        return queryset, use_distinct

    def get_queryset(self):
        queryset = super(ModelListView, self).get_queryset()
        search_term = self.request.GET.get('q', None)
        search_use_distinct = False
        if self.model_admin.search_fields and search_term:
            queryset, search_use_distinct = self.get_search_results(
                queryset, search_term)

        queryset = self._modify_queryset_for_ordering(queryset)

        if self.model_admin.list_filter:
            queryset = self.build_list_filter(queryset).qs

        if self.model_admin.date_hierarchy:
            queryset = self.build_date_filter(queryset, self.model_admin.date_hierarchy).qs

        queryset = self._modify_queryset_for_sort(queryset)

        if search_use_distinct:
            return queryset.distinct()
        else:
            return queryset

    def _modify_queryset_for_ordering(self, queryset):
        ordering = self.model_admin.get_ordering(self.request)
        if ordering:
            queryset = queryset.order_by(*ordering)
        return queryset

    def _modify_queryset_for_sort(self, queryset):
        # If we are sorting AND the field exists on the model
        sort_by = self.request.GET.get('sort', None)
        if sort_by:
            # Special case when we are not explicityly displaying fields
            if sort_by == '-__str__':
                queryset = queryset[::-1]
            try:
                # If we sort on '-' remove it before looking for that field
                field_exists = sort_by
                if field_exists[0] == '-':
                    field_exists = field_exists[1:]

                options = utils.model_options(self.model)
                options.get_field(field_exists)
                queryset = queryset.order_by(sort_by)
            except FieldDoesNotExist:
                # If the field does not exist then we dont sort on it
                pass
        return queryset

    def build_list_filter(self, queryset=None):
        if not hasattr(self, '_list_filter'):
            if queryset is None:
                queryset = self.get_queryset()
            self._list_filter = build_list_filter(
                self.request,
                self.model_admin,
                queryset,
            )
        return self._list_filter

    def build_date_filter(self, queryset=None, field_name=None):
        if not hasattr(self, "_date_filter"):
            if queryset is None:
                queryset = self.get_queryset()
            self._date_filter = build_date_filter(
                self.request,
                self.model_admin,
                queryset,
                field_name
            )

        return self._date_filter

    def get_context_data(self, **kwargs):
        context = super(ModelListView, self).get_context_data(**kwargs)
        context['model'] = self.get_model()
        context['actions'] = self.get_actions().values()
        context['search_fields'] = self.get_search_fields()
        context['search_term'] = self.request.GET.get('q', '')
        context['list_filter'] = self.build_list_filter()
        context['sort_term'] = self.request.GET.get('sort', '')

        if self.model_admin.date_hierarchy:
            year = self.request.GET.get("year", False)
            month = self.request.GET.get("month", False)
            day = self.request.GET.get("day", False)

            if year and month and day:
                new_date = datetime.strptime(
                    "%s %s %s" % (month, day, year),
                    "%m %d %Y",
                )
                context["previous_date"] = {
                    "link": "?year=%s&month=%s" % (year, month),
                    "text": "‹ %s" % new_date.strftime("%B %Y")
                }

                context["active_day"] = new_date.strftime("%B %d")

                context["dates"] = self._format_days(self.get_queryset())
            elif year and month:
                context["previous_date"] = {
                    "link": "?year=%s" % (year),
                    "text": "‹ %s" % year,
                }

                context["dates"] = self._format_days(self.get_queryset())
            elif year:
                context["previous_date"] = {
                    "link": "?",
                    "text": ugettext_lazy("‹ All dates"),
                }

                context["dates"] = self._format_months(self.get_queryset())
            else:
                context["dates"] = self._format_years(self.get_queryset())

        return context

    def _format_years(self, queryset):
        years = self._qs_date_or_datetime(queryset, 'year')
        if len(years) == 1:
            return self._format_months(queryset)
        else:
            return [
                (("?year=%s" % year.strftime("%Y")), year.strftime("%Y"))
                for year in
                self._qs_date_or_datetime(queryset, 'year')
            ]

    def _format_months(self, queryset):
        return [
            (
                "?year=%s&month=%s" % (
                    date.strftime("%Y"), date.strftime("%m")
                ),
                date.strftime("%B %Y")
            ) for date in
            self._qs_date_or_datetime(queryset, 'month')
        ]

    def _format_days(self, queryset):
        return [
            (
                "?year=%s&month=%s&day=%s" % (
                    date.strftime("%Y"),
                    date.strftime("%m"),
                    date.strftime("%d"),
                ),
                date.strftime("%B %d")
            ) for date in
            self._qs_date_or_datetime(queryset, 'day')
        ]

    def _qs_date_or_datetime(self, object_list, type):
        if isinstance(self.model._meta.get_field(self.model_admin.date_hierarchy), models.DateTimeField):
            qs = object_list.datetimes(self.model_admin.date_hierarchy, type)
        else:
            qs = object_list.dates(self.model_admin.date_hierarchy, type)
        return qs

    def get_success_url(self):
        view_name = 'admin2:{}_{}_index'.format(
            self.app_label, self.model_name)
        return reverse(view_name)

    def get_actions(self):
        return self.model_admin.get_list_actions()

    def get_search_fields(self):
        return self.model_admin.search_fields


class ModelDetailView(Admin2ModelMixin, generic.DetailView):
    """Context Variables

    :model: Type of object you are editing
    :model_name: Name of the object you are editing
    :app_label: Name of your app
    :app_verbose_names: A dictionary containing the app verbose name for
                        a given app, the item has a key being the
                        `app_label` and the value being a string, (or
                        even a lazy translation object), with the custom
                        app name.
    """
    default_template_name = "model_detail.html"
    permission_classes = (
        permissions.IsStaffPermission,
        permissions.ModelViewPermission)


class ModelEditFormView(Admin2ModelMixin, Admin2ModelFormMixin,
                        extra_views.UpdateWithInlinesView):
    """Context Variables

    :model: Type of object you are editing
    :model_name: Name of the object you are editing
    :app_label: Name of your app
    :app_verbose_names: A dictionary containing the app verbose name for
                        a given app, the item has a key being the
                        `app_label` and the value being a string, (or
                        even a lazy translation object), with the custom
                        app name.
    """
    form_class = None
    default_template_name = "model_update_form.html"
    permission_classes = (
        permissions.IsStaffPermission,
        permissions.ModelChangePermission)

    def get_context_data(self, **kwargs):
        context = super(ModelEditFormView, self).get_context_data(**kwargs)
        context['model'] = self.get_model()
        context['action'] = "Change"
        context['action_name'] = ugettext_lazy("Change")
        return context

    def forms_valid(self, form, inlines):
        response = super(ModelEditFormView, self).forms_valid(form, inlines)
        LogEntry.objects.log_action(
            self.request.user.id,
            self.object,
            LogEntry.CHANGE,
            self.construct_change_message(self.request, form, inlines))
        return response


class ModelAddFormView(Admin2ModelMixin, Admin2ModelFormMixin,
                       extra_views.CreateWithInlinesView):
    """Context Variables

    :model: Type of object you are editing
    :model_name: Name of the object you are editing
    :app_label: Name of your app
    :app_verbose_names: A dictionary containing the app verbose name for
                        a given app, the item has a key being the
                        `app_label` and the value being a string, (or
                        even a lazy translation object), with the custom
                        app name.
    """
    form_class = None
    default_template_name = "model_update_form.html"
    permission_classes = (
        permissions.IsStaffPermission,
        permissions.ModelAddPermission)

    def get_context_data(self, **kwargs):
        context = super(ModelAddFormView, self).get_context_data(**kwargs)
        context['model'] = self.get_model()
        context['action'] = "Add"
        context['action_name'] = ugettext_lazy("Add")
        return context

    def forms_valid(self, form, inlines):
        response = super(ModelAddFormView, self).forms_valid(form, inlines)
        LogEntry.objects.log_action(
            self.request.user.id,
            self.object,
            LogEntry.ADDITION,
            'Object created.')
        return response


class ModelDeleteView(Admin2ModelMixin, generic.DeleteView):
    """Context Variables

    :model: Type of object you are editing
    :model_name: Name of the object you are editing
    :app_label: Name of your app
    :deletable_objects: Objects to delete
    :app_verbose_names: A dictionary containing the app verbose name for
                        a given app, the item has a key being the
                        `app_label` and the value being a string, (or
                        even a lazy translation object), with the custom
                        app name.
    """
    success_url = "../../"  # TODO - fix this!
    default_template_name = "model_confirm_delete.html"
    permission_classes = (
        permissions.IsStaffPermission,
        permissions.ModelDeletePermission)

    def get_context_data(self, **kwargs):
        context = super(ModelDeleteView, self).get_context_data(**kwargs)

        def _format_callback(obj):
            opts = utils.model_options(obj)
            return '%s: %s' % (force_text(capfirst(opts.verbose_name)),
                               force_text(obj))

        using = router.db_for_write(self.get_object()._meta.model)
        collector = utils.NestedObjects(using=using)
        collector.collect([self.get_object()])
        context.update({
            'deletable_objects': collector.nested(_format_callback)
        })
        return context

    def delete(self, request, *args, **kwargs):
        LogEntry.objects.log_action(
            request.user.id,
            self.get_object(),
            LogEntry.DELETION,
            'Object deleted.')
        return super(ModelDeleteView, self).delete(request, *args, **kwargs)


class ModelHistoryView(Admin2ModelMixin, generic.ListView):
    """Context Variables

    :model: Type of object you are editing
    :model_name: Name of the object you are editing
    :app_label: Name of your app
    :app_verbose_names: A dictionary containing the app verbose name for
                        a given app, the item has a key being the
                        `app_label` and the value being a string, (or
                        even a lazy translation object), with the custom
                        app name.
    """
    default_template_name = "model_history.html"
    paginate_by = 10
    permission_classes = (
        permissions.IsStaffPermission,
        permissions.ModelChangePermission
    )

    def get_context_data(self, **kwargs):
        context = super(ModelHistoryView, self).get_context_data(**kwargs)
        context['model'] = self.get_model()
        context['object'] = self.get_object()
        return context

    def get_object(self):
        return get_object_or_404(self.get_model(), pk=self.kwargs.get('pk'))

    def get_queryset(self):
        content_type = ContentType.objects.get_for_model(self.get_object())
        return LogEntry.objects.filter(
            content_type=content_type,
            object_id=self.get_object().id
        )


class PasswordChangeView(Admin2Mixin, generic.UpdateView):

    default_template_name = 'auth/password_change_form.html'
    form_class = AdminPasswordChangeForm
    admin_form_class = PasswordChangeForm
    model = settings.AUTH_USER_MODEL
    success_url = reverse_lazy('admin2:password_change_done')

    def get_form_kwargs(self, **kwargs):
        data = {'user': self.get_object()}

        if self.request.method in ('POST', 'PUT'):
            data.update({
                'data': self.request.POST
            })

        return data

    def get_form_class(self):
        if self.request.user == self.get_object():
            return self.admin_form_class
        return super(PasswordChangeView, self).get_form_class()

    def get_queryset(self):
        from django.contrib.auth import get_user_model
        return get_user_model()._default_manager.all()

    def form_valid(self, form):
        self.object = form.save()
        if self.request.user == self.get_object():
            update_session_auth_hash(self.request, form.user)
        return HttpResponseRedirect(self.get_success_url())


class PasswordChangeDoneView(Admin2Mixin, generic.TemplateView):

    default_template_name = 'auth/password_change_done.html'


class LoginView(Admin2Mixin, generic.TemplateView):
    """Context Variables

    :site_name: Name of the site
    """

    default_template_name = 'auth/login.html'
    authentication_form = AdminAuthenticationForm

    def dispatch(self, request, *args, **kwargs):
        return auth_login(request,
                          authentication_form=self.authentication_form,
                          template_name=self.get_template_names(),
                          *args, **kwargs)


class LogoutView(Admin2Mixin, generic.TemplateView):
    """Context Variables

    :site_name: Name of the site
    """

    default_template_name = 'auth/logout.html'

    def get(self, request, *args, **kwargs):
        return auth_logout(request, template_name=self.get_template_names(),
                           *args, **kwargs)
