"""Utilities for writing views."""

from io import BytesIO
from zipfile import BadZipfile, ZipFile, ZIP_LZMA

from braces.views import UserPassesTestMixin
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required, user_passes_test
from django.core.exceptions import ValidationError
from django.http import Http404, HttpResponse, HttpResponseRedirect, JsonResponse
from django.utils.decorators import method_decorator
from django.utils.http import is_safe_url
from django.views import generic
from django.views.decorators.cache import cache_page
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_safe
from slugify import slugify

from mtp.apps.accounts.models import MTPUser


class RequireSafeMixin:
    """Mixin for views that expect only a GET/HEAD call.

    Notes
    -----
    This uses the ``require_safe`` decorator, which must be applied to the ``dispatch``
    method, as we use class based view, with help of ``method_decorator``

    """

    @method_decorator(require_safe)
    def dispatch(self, request, *args, **kwargs):
        return super().dispatch(request, *args, **kwargs)


class CrsfExemptMixin:
    """Mixin for override the ``dispatch`` method to apply the ``csrf_exempt`` decorator."""

    @method_decorator(csrf_exempt)
    def dispatch(self, request, *args, **kwargs):
        """Override the dispatch method, applying the method decorator

        For the parameters, see ``django.views.generic.View"""

        return super().dispatch(request, *args, **kwargs)


class LoginRequiredMixin(object):
    """Mixin for views that expect an authenticated user."""

    @classmethod
    def as_view(cls, **initkwargs):
        view = super(LoginRequiredMixin, cls).as_view(**initkwargs)
        return login_required(view)


class RestrictToSuperuserMixin:
    """Mixin for views that expect a superuser user."""

    @method_decorator(user_passes_test(MTPUser.test_if_superuser))
    def dispatch(self, request, *args, **kwargs):
        return super().dispatch(request, *args, **kwargs)


class RestrictToSuperuserOrTrainerMixin:
    """Mixin for views that expect a superuser or a trainer user."""

    @method_decorator(user_passes_test(MTPUser.test_if_superuser_or_trainer))
    def dispatch(self, request, *args, **kwargs):
        return super().dispatch(request, *args, **kwargs)


class RestrictToTrainerMixin:
    """Mixin for views that expect a trainer user."""

    @method_decorator(user_passes_test(MTPUser.test_if_trainer))
    def dispatch(self, request, *args, **kwargs):
        return super().dispatch(request, *args, **kwargs)


class RestrictToTraineeMixin:
    """Mixin for views that expect a trainee user."""

    @method_decorator(user_passes_test(MTPUser.test_if_trainee))
    def dispatch(self, request, *args, **kwargs):
        return super().dispatch(request, *args, **kwargs)


class RestrictToObjectOwner(UserPassesTestMixin):
    """Mixin for views that expect a user "owner" of the current object"""

    def test_func(self, user):
        """Test if the current user is the "owner" of the object

        Parameters
        ----------
        user: MTPUser
            The current user

        Returns
        -------
        bool
            True if the current user is the object owner, False otherwise

        """
        # it's a mixin, `self.object` will be defined in get/post of standard imported view class
        self.object = self.get_object()  # pylint: disable=attribute-defined-outside-init
        return self.object.user == self.request.user


class ConditionalMixin:
    """Abstract Mixin for views that should met a given condition.

    You need to subclass this Mixin and implement the ``is_condition_met`` method.

    Examples
    --------
    >>> class AlwaysTrueMixin(ConditionalMixin):
    ...     def is_condition_met(self):
    >>>         return True

    """

    def condition_not_met(self):
        """Action to take if the condition is not met

        Per default, we raise an ``Http404`` exception.

        Raises
        ------
        Http404

        """

        raise Http404

    def is_condition_met(self):
        """Test if the given condition is met

        Notes
        -----
        Must be implemented in subclasses, the method must return a bool.

        """

        raise NotImplementedError('You must implement the `is_condition_met` method.')

    def dispatch(self, request, *args, **kwargs):
        """Override the dispatch method to test if the condition is met

        For the parameters, see ``django.views.generic.View"""

        if not self.is_condition_met():
            return self.condition_not_met()

        return super().dispatch(request, *args, **kwargs)


class RequireMasterMixin(ConditionalMixin):
    """Mixin for views that should be only accessible on a ``MASTER`` server."""

    def is_condition_met(self):
        """Test if the current instance is a ``MASTER`` one.

        Returns
        -------
        bool
            True if the the current instance is a ``MASTER`` one,
            False otherwise

        """

        return settings.INSTANCE_IS_MASTER


class RequireSlaveMixin(ConditionalMixin):
    """Mixin for views that should be only accessible on a ``SLAVE`` server."""

    def is_condition_met(self):
        """Test if the current instance is a ``SLAVE`` one.

        Returns
        -------
        bool
            True if the the current instance is a ``SLAVE`` one,
            False otherwise

        """

        return settings.INSTANCE_IS_SLAVE


class GlobalFormsetValidationError(ValidationError):
    """Exception to be raised if a view if the form is valid but not the formset.

    Attributes
    ----------
    form : form
        The form that is marked as valid by the view, to be used to call ``form_invalid``.

    """

    def __init__(self, form):
        """Override the default ``__init__`` to pass default arguments.

        For the parameters, see ``django.core.exceptions.ValidationError``

        Notes
        -----
        The ``message`` and the ``code`` are set to default value because they are not meant
        to be used.

        """

        super().__init__('Error in formset', code='invalid-formset', params=None)
        self.form = form


class EditFormWithFormsetMixin:

    formset_class = None
    context_formset_name = None

    def get_formset_class(self):
        """Returns the formset class to use in this view"""

        return self.formset_class

    def get_formset(self, **kwargs):
        formset_attrs = {
            'instance': self.object,
        }

        if self.request.method == 'POST':
            formset_attrs['data'] = self.request.POST

        formset_attrs.update(kwargs)

        # Tell pylint that this IS callable, as it's a class that should be defined in subclasses.
        # pylint: disable=not-callable
        return self.get_formset_class()(**formset_attrs)

    def get_form(self, *args, **kwargs):
        form = super().get_form(*args, **kwargs)
        # it's a mixin, `self.formset` will be defined in get/post of standard imported view class
        self.formset = self.get_formset()  # pylint: disable=attribute-defined-outside-init
        return form

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['formset'] = self.formset
        if self.context_formset_name:
            context[self.context_formset_name] = self.formset
        return context

    def save_form(self, form, commit=True):
        # it's a mixin, `self.object` will be defined in get/post of standard imported view class
        self.object = form.save(commit=commit)  # pylint: disable=attribute-defined-outside-init

    def save_formset(self, formset):
        formset.save()

    def post(self, request, *args, **kwargs):
        """Handle form AND formset validation.

        If the form is valid but not the formset, ``form_valid`` triggers a
        ``GlobalFormsetValidationError`` exception. We catch it to call ``form_invalid``, with
        the form attached to the exception.

        For the parameters, see ``django.views.generic.View``

        """

        try:
            return super().post(request, *args, **kwargs)
        except GlobalFormsetValidationError as exc:
            return self.form_invalid(exc.form)

    def form_valid(self, form):
        """Validate the form and the formset, save the data and redirect the user.

        Parameters
        ----------
        form : instance of ``django.forms.models.ModelForm``
            The validated form to save.

        Returns
        -------
        instance of django.http.HttpResponseRedirect
            A redirect to the url defined in ``get_success_url``.

        Raises
        ------
        GlobalFormsetValidationError
            If the form is valid but not the formset. Used by ``post`` to call ``form_invalid``.

        Notes
        -----
        This method doesn't call ``super`` but implements all the necessary stuff (managing
        formset, saving and redirecting)

        """

        # Will set ``self.object`` to be ready if needed for formset validation
        self.save_form(form, commit=False)
        self.formset.instance = self.object

        if not self.formset.is_valid():
            raise GlobalFormsetValidationError(form=form)

        # Everything is ok, we can save all data.
        self.save_form(form, commit=True)
        self.formset.instance = self.object
        self.save_formset(self.formset)

        return HttpResponseRedirect(self.get_success_url())


class SyncableDeleteView(generic.DeleteView):
    """View to use instead for deletion of models inheriting from ``Syncable``.

    The ``delete`` method will not really delete the object, but will instead call the
    ``remove`` method, managed by the ``Syncable`` abstract model.

    """

    def delete(self, request, *args, **kwargs):
        """ Fake delete the object and returns to the success url.

        For the parameters, see ``django.forms.generic.DeleteView``"""
        self.object = self.get_object()
        success_url = self.get_success_url()
        self.object.remove()
        return HttpResponseRedirect(success_url)


class SyncableUndeleteView(generic.DeleteView):
    """View to use to undelete an instance of models inheriting from ``Syncable``.

    The ``delete`` method will restore the object by calling the
    ``unremove`` method, managed by the ``Syncable`` abstract model.

    We use a ``DeleteView`` even if it's a undelete action because we want
    the same behavior : a confirmation popup, then a POST.
    """

    template_name_suffix = '_confirm_undelete'

    def delete(self, request, *args, **kwargs):
        """ Undelete the object and returns to the success url.

        For the parameters, see ``django.forms.generic.DeleteView``"""
        self.object = self.get_object()  # pylint: disable=attribute-defined-outside-init
        success_url = self.get_success_url()
        self.object.unremove()
        return HttpResponseRedirect(success_url)


class SuccessMessageMixin:
    """Rewrite of ``django.contrib.messages.views.SuccessMessageMixin``.

    Notes
    -----

    This mixin should be the first in the list of mixins your form will inherit from.
    If not and you define your own `form_valid` method,  the success message will be displayed
    even if your define the form as invalid after calling ``super``.

    """

    success_message = ''

    def get_success_message_data(self, data=None):
        """Return the given data or the attributes of ``self.object`` if it exists."""

        # We have some data, use it
        if data:
            return data

        # We don't have any data, we'll use the attributes of ``self.object`` if it exists.
        # Useful for example to display a message with the name of something after it has been
        # deleted.
        if hasattr(self, 'object') and self.object:
            return vars(self.object)

        # Finally no data and no object, return an empty dict
        return {}

    def get_success_message(self, data=None):
        """Compose the final message with the attribute and the data."""

        # If no success message was defined on the form, we have nothing to do
        if not self.success_message:
            return None

        # Compose the final success message and return it
        return self.success_message % self.get_success_message_data(data)

    def send_message(self, data=None):
        """Actually send the message if the middleware is installed and we have a message."""

        # Do nothing if the ``messages`` middleware is not installed.
        if not hasattr(self.request, '_messages'):
            return

        # Compose the success message with the data.
        success_message = self.get_success_message(data)

        # Send the message only if we have one.
        if success_message:
            messages.success(self.request, success_message)

    def form_valid(self, form):
        """Send the message when the form is valid and actions are done.

        For the parameters, see ``django.forms.generic.edit.FormMixin``
        """

        # Original action, so we'll only send the message when it will be fully done.
        response = super(SuccessMessageMixin, self).form_valid(form)

        # Send the message composed with the data we get from the form.
        self.send_message(form.cleaned_data)

        # Then we can return the original response.
        return response

    def delete(self, request, *args, **kwargs):
        """Send the message when the work in the original ``delete`` method is done.

        For the parameters, see ``django.forms.generic.DeleteView``"""

        # Original action, so we'll only send the message when it will be fully done.
        response = super().delete(request, *args, **kwargs)

        # Send the message without passing any data, they will be filled by the call to the
        # ``get_success_message_data`` method.
        self.send_message()

        # Then we can return the original response.
        return response


class OrderableListMixin:
    """Mixin that saves via an AJAX call the orders set by django-orderable.

    To work as expected, you create a template named ``yourapp/yourmodelname_sort.html``.

    This template should include the template ``cms/includes/orderable-head.html`` in the
    ``head`` block. See this template for full documentation on how to use it, the available
    arguments, and an exemple on how to write your ``table``.

    And your model should inherit from ``orderable.models.Orderable``.

    """

    template_name_suffix = '_sort'

    def post(self, request, *args, **kwargs):  # pylint: disable=unused-argument
        """Save each model passed in the ``neworder[]`` list, in this order.

        This view is expected to be called via ajax. Response for normal post call is not handled.

        For the parameters, see ``django.views.generic.View``
        """

        neworder = 1
        for object_id in request.POST.getlist('neworder[]'):
            obj = self.model.objects.get(pk=object_id)
            obj.sort_order = neworder
            obj.save()
            neworder += 1

        return HttpResponse("OK")


class AjaxResponseMixin:
    """Mixin to return an ajax response on class based view."""

    def form_invalid(self, form):
        # We make sure to call the parent's form_invalid() method because
        # it might do some processing.
        super(AjaxResponseMixin, self).form_invalid(form)

        return JsonResponse(
            {
                'valid': False,
                'errors': form.errors
            }
        )

    def form_valid(self, form):
        # We make sure to call the parent's form_valid() method because
        # it might do some processing (in the case of CreateView, it will
        # call form.save() for example).
        super(AjaxResponseMixin, self).form_valid(form)

        return JsonResponse(
            {
                'valid': True,
                'success_url': self.get_success_url()
            }
        )


class ExportFileMixin:
    """Mixin which provide methods to export file.

    Attributes
    ----------
    content_type: str or None
        The content-type of the file to export.
    extension: str or None
        The extension of the file to export.

    """

    content_type = None
    extension = None

    def get_filename(self):
        """Get the filename used to name the generated file.

        Raises
        ------
        NotImplementedError
            If no filename is provided in the view which use this mixin.

        Returns
        -------
        str
            The wanted filename.

        """

        raise NotImplementedError(
            "You must implement the `get_filename` method "
            "in %s class." % self.__class__.__name__
        )

    def get_slugified_filename(self):
        """Get the slugified filename.

        Returns
        -------
        str
            The slugified filename.

        """

        return slugify(self.get_filename())


class NextSuccessUrlMixin:
    """A mixin to override get_success_url when a next GET param is available

    Notes
    -----
    You need to inherit from this Mixin *after* (on the left) the last class that defines
    a ``get_success_url`` method.

    You can not use it in a class that already defines a ``get_success_url`` method either.

    """

    def get_success_url(self):
        """Get the success_url from GET.next if any

        Returns
        -------
        string
            The success url to redirect to.
        """

        success_url = self.request.GET.get('next')

        if not success_url or not is_safe_url(url=success_url, host=self.request.get_host()):
            success_url = super().get_success_url()

        return success_url


class HandleZipFileMixin:
    """A mixin to check and handle an uploaded zip file"""

    def handle_zipfile(self, field_name):
        """Check and handle the provided uploaded file identified by the ``field_name``

        Parameters
        ----------
        field_name: str
            The field name the file is placed into

        Raises
        ------
        ValueError
            If the provided file does not contains a valid zip file

        Returns
        -------
        zip_file: ZipFile

        """

        file = self.request.FILES.get(field_name)

        if not file:
            raise ValueError

        try:
            zip_buffer = BytesIO(file.read())
            zip_file = ZipFile(zip_buffer, 'r', ZIP_LZMA)
        except BadZipfile:
            raise ValueError

        # The testzip() method return the first file in the zip with errors, if any
        if zip_file.testzip():
            raise ValueError

        return zip_file


class CacheMixin:
    """A mixin to cache a CBV using django "cache_page" method decorator

    Attributes
    ----------
    cache_timeout: int
        The cache timeout in seconds, default to 60 seconds

    """

    cache_timeout = 60

    def get_cache_timeout(self):
        """Get the cache timeout from the attribute

        Returns
        -------
        int
            The cache timeout, expressed in seconds

        """

        return self.cache_timeout

    def dispatch(self, *args, **kwargs):
        """Override the dispatch method, applying the cache_page decorator"""

        cached = cache_page(self.get_cache_timeout())

        return cached(super().dispatch)(*args, **kwargs)
