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

import logging
import os
import sys
from collections import namedtuple

import extra_views
from django.conf.urls import url
from django.forms import modelform_factory
from django.urls import reverse
from django.utils.six import with_metaclass

from . import actions
from . import apiviews
from . import settings
from . import utils
from . import views


logger = logging.getLogger('djadmin2')


class ModelAdminBase2(type):

    def __new__(cls, name, bases, attrs):
        new_class = super(ModelAdminBase2, cls).__new__(cls, name,
                                                        bases, attrs)
        view_list = []
        for key, value in attrs.items():
            if isinstance(value, views.AdminView):
                if not value.name:
                    value.name = key
                view_list.append(value)

        view_list.extend(getattr(new_class, 'views', []))
        new_class.views = view_list
        return new_class


class ModelAdmin2(with_metaclass(ModelAdminBase2)):
    """
    Adding new ModelAdmin2 attributes:

        Step 1: Add the attribute to this class
        Step 2: Add the attribute to djadmin2.settings.MODEL_ADMIN_ATTRS

        Reasoning:

            Changing values on ModelAdmin2 objects or their attributes from
            within a view results in leaky scoping issues. Therefore, we use
            the immutable_admin_factory to render the ModelAdmin2 class
            practically immutable before passing it to the view. To constrain
            things further (in order to protect ourselves from causing
            hard-to-find security problems), we also restrict which attrs are
            passed to the final ImmutableAdmin object (i.e. a namedtuple).
            This prevents us from easily implementing methods/setters which
            bypass the blocking features of the ImmutableAdmin.
    """
    actions_selection_counter = True
    date_hierarchy = False
    list_display = ('__str__',)
    list_display_links = ()
    list_filter = ()
    list_select_related = False
    list_per_page = 100
    list_max_show_all = 200
    list_editable = ()
    search_fields = ()
    save_as = False
    save_on_top = False
    verbose_name = None
    verbose_name_plural = None
    model_admin_attributes = settings.MODEL_ADMIN_ATTRS
    ordering = False
    save_on_top = False
    save_on_bottom = True

    # Not yet implemented. See #267 and #268
    actions_on_bottom = False
    actions_on_top = True

    search_fields = []

    # Show the fields to be displayed as columns
    # TODO: Confirm that this is what the Django admin uses
    list_fields = []

    # This shows up on the DocumentListView of the Posts
    list_actions = [actions.DeleteSelectedAction]

    # This shows up in the DocumentDetailView of the Posts.
    document_actions = []

    # Shows up on a particular field
    field_actions = {}

    # Defines custom field renderers
    field_renderers = {}

    fields = None
    exclude = None
    fieldsets = None
    form_class = None
    filter_vertical = ()
    filter_horizontal = ()
    radio_fields = {}
    prepopulated_fields = {}
    formfield_overrides = {}
    readonly_fields = ()
    ordering = None

    create_form_class = None
    update_form_class = None

    inlines = []

    #  Views
    index_view = views.AdminView(r'^$', views.ModelListView, name='index')
    create_view = views.AdminView(r'^create/$', views.ModelAddFormView, name='create')
    update_view = views.AdminView(r'^(?P<pk>[0-9]+)/$', views.ModelEditFormView, name='update')
    detail_view = views.AdminView(r'^(?P<pk>[0-9]+)/update/$', views.ModelDetailView, name='detail')
    delete_view = views.AdminView(r'^(?P<pk>[0-9]+)/delete/$', views.ModelDeleteView, name='delete')
    history_view = views.AdminView(r'^(?P<pk>[0-9]+)/history/$', views.ModelHistoryView, name='history')
    views = []

    # API configuration
    api_serializer_class = None

    # API Views
    api_list_view = apiviews.ListCreateAPIView
    api_detail_view = apiviews.RetrieveUpdateDestroyAPIView

    def __init__(self, model, admin, name=None, **kwargs):
        self.name = name
        self.model = model
        self.admin = admin
        model_options = utils.model_options(model)
        self.app_label = model_options.app_label
        self.model_name = model_options.object_name.lower()

        if self.name is None:
            self.name = '{}_{}'.format(self.app_label, self.model_name)

        if self.verbose_name is None:
            self.verbose_name = model_options.verbose_name
        if self.verbose_name_plural is None:
            self.verbose_name_plural = model_options.verbose_name_plural

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

    def get_index_kwargs(self):
        kwargs = self.get_default_view_kwargs()
        kwargs.update({
            'paginate_by': self.list_per_page,
        })
        return kwargs

    def get_default_api_view_kwargs(self):
        kwargs = self.get_default_view_kwargs()
        kwargs.update({
            'serializer_class': self.api_serializer_class,
        })
        return kwargs

    def get_prefixed_view_name(self, view_name):
        return '{}_{}'.format(self.name, view_name)

    def get_create_kwargs(self):
        kwargs = self.get_default_view_kwargs()
        kwargs.update({
            'inlines': self.inlines,
            'form_class': (self.create_form_class if
                           self.create_form_class else self.form_class),
        })
        return kwargs

    def get_update_kwargs(self):
        kwargs = self.get_default_view_kwargs()
        form_class = (self.update_form_class if
                      self.update_form_class else self.form_class)
        if form_class is None:
            form_class = modelform_factory(self.model, fields='__all__')
        kwargs.update({
            'inlines': self.inlines,
            'form_class': form_class,
        })
        return kwargs

    def get_index_url(self):
        return reverse('admin2:{}'.format(
            self.get_prefixed_view_name('index')))

    def get_api_list_kwargs(self):
        kwargs = self.get_default_api_view_kwargs()
        kwargs.update({
            'queryset': self.model.objects.all(),
            # 'paginate_by': self.list_per_page,
        })
        return kwargs

    def get_api_detail_kwargs(self):
        return self.get_default_api_view_kwargs()

    def get_urls(self):
        pattern_list = []
        for admin_view in self.views:
            admin_view.model_admin = self
            get_kwargs = getattr(self, "get_%s_kwargs" % admin_view.name, None)
            if not get_kwargs:
                get_kwargs = admin_view.get_view_kwargs
            try:
                view_instance = admin_view.view.as_view(**get_kwargs())
            except Exception as e:
                trace = sys.exc_info()[2]
                new_exception = TypeError(
                    'Cannot instantiate admin view "{}.{}". '
                    'The error that got raised was: {}'.format(
                        self.__class__.__name__, admin_view.name, e))
                try:
                    raise new_exception.with_traceback(trace)
                except AttributeError:
                    raise (new_exception, None, trace)

            pattern_list.append(
                url(
                    regex=admin_view.url,
                    view=view_instance,
                    name=self.get_prefixed_view_name(admin_view.name)
                )
            )
        return pattern_list

    def get_api_urls(self):
        return [
            url(
                regex=r'^$',
                view=self.api_list_view.as_view(**self.get_api_list_kwargs()),
                name=self.get_prefixed_view_name('api_list'),
            ),
            url(
                regex=r'^(?P<pk>[0-9]+)/$',
                view=self.api_detail_view.as_view(
                    **self.get_api_detail_kwargs()),
                name=self.get_prefixed_view_name('api_detail'),
            ),
        ]

    @property
    def urls(self):
        # We set the application and instance namespace here
        return self.get_urls(), None, None

    @property
    def api_urls(self):
        return self.get_api_urls(), None, None

    def get_list_actions(self):
        actions_dict = {}

        for cls in type(self).mro()[::-1]:
            class_actions = getattr(cls, 'list_actions', [])
            for action in class_actions:
                actions_dict[action.__name__] = {
                    'name': action.__name__,
                    'description': actions.get_description(action),
                    'action_callable': action
                }
        return actions_dict

    def get_ordering(self, request):
        return self.ordering


class Admin2Inline(extra_views.InlineFormSet):
    """
    A simple extension of django-extra-view's InlineFormSet that
    adds some useful functionality.
    """
    template = None
    fields = '__all__'

    def construct_formset(self):
        """
        Overrides construct_formset to attach the model class as
        an attribute of the returned formset instance.
        """
        formset = super(Admin2Inline, self).construct_formset()
        formset.model = self.inline_model
        formset.template = self.template
        return formset


class Admin2TabularInline(Admin2Inline):
    template = os.path.join(
        settings.ADMIN2_THEME_DIRECTORY, 'edit_inlines/tabular.html')


class Admin2StackedInline(Admin2Inline):
    template = os.path.join(
        settings.ADMIN2_THEME_DIRECTORY, 'edit_inlines/stacked.html')


def immutable_admin_factory(model_admin):
    """
    Provide an ImmutableAdmin to make it harder for developers to
    dig themselves into holes.
    See https://github.com/twoscoops/django-admin2/issues/99
    Frozen class implementation as namedtuple suggested by Audrey Roy

    Note: This won't stop developers from saving mutable objects to
    the result, but hopefully developers attempting that
    'workaround/hack' will read our documentation.
    """
    ImmutableAdmin = namedtuple('ImmutableAdmin',
                                model_admin.model_admin_attributes,
                                verbose=False)
    return ImmutableAdmin(*[getattr(
        model_admin, x) for x in model_admin.model_admin_attributes])
