"""This module holds the views of the ``roleplays`` app.

All views in this module must inherit from ``RoleplayBaseView`` to correctly
handle the currently active section in the main menu and the access
restrictions.

Notes
-----
The choice was done to use class-based views.

"""

import datetime

from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from django.views import generic

from mtp.apps.assessments.views import AssessableCreateMixin
from mtp.apps.common.views_utils import (
    AjaxResponseMixin,
    OrderableListMixin,
    RestrictToSuperuserOrTrainerMixin,
    SuccessMessageMixin,
    SyncableDeleteView
)
from mtp.apps.import_export.exceptions import ImportDataError
from mtp.apps.import_export.views import XLSExportDetailView, ActivityXLSImportView
from mtp.apps.trainings.models import Training

from .forms import CriterionForm, RoleplayAssessableForm
from .models import Criterion, Roleplay, Step
from .resources import CriterionResource, RoleplayResource, StepResource


class RoleplayEditBase(AjaxResponseMixin, SuccessMessageMixin, RestrictToSuperuserOrTrainerMixin):
    """Base view to create or update a given instance of
    ``mtp.apps.roleplays.models.Roleplay``

    Notes
    -----
    We need to get the current ``Training`` and add it to the context.

    """
    model = Roleplay
    form_class = RoleplayAssessableForm
    pk_url_kwarg = 'roleplay_uuid'

    def get_queryset(self):
        """Get all ``Roleplay`` instances the current user is allowed to see.

        The user can only see roleplays instance he own.
        Also excludes deleted roleplays from the result.

        Returns
        -------
        Queryset of ``mtp.apps.surveys.models.Roleplay``
            A lazy queryset with the wanted roleplays.

        """
        return Roleplay.objects.non_deleted().for_user(self.request.user)


class RoleplayCreate(AssessableCreateMixin, RoleplayEditBase, generic.CreateView):
    """The view to create a ``mtp.apps.roleplays.models.Roleplay`` instance.

    Notes
    -----

    We need to get the ``Training`` instance from the url and add it to
    the context. The ``training`` and the ``author`` are automatically set
    on saving.

    """
    success_message = _("Your roleplay was successfully created.")

    @cached_property
    def training(self):
        """Returns the current training based on the ``training_uuid``
        argument in the url.

        Raises
        ------
        ``django.http.Http404``
            If the training doesn't exists.

        """
        return get_object_or_404(
            Training,
            pk=self.kwargs['training_uuid']
        )

    @cached_property
    def is_assessing_container(self):
        return self.training.job_profile is not None

    def get_context_data(self, **kwargs):
        """Add ``training`` to the context.

        For the parameters, see ``django.forms.generic.base.ContextMixin``
        """

        return super().get_context_data(training=self.training, **kwargs)

    def form_valid(self, form):
        """Set the ``training``, the ``author``, save the roleplay 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 edit url of the roleplay.

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

        """
        self.object = form.save(commit=False)
        self.object.training = self.training
        self.object.save()

        return super().form_valid(form)


class RoleplayDetail(RoleplayEditBase, generic.DetailView):
    """The view to show the details of a ``mtp.apps.roleplays.models.Roleplay`` instance."""

    def get_context_data(self, **kwargs):
        """Add the ``Training`` and the related ``Steps`` to the context.

        For the parameters, see ``django.forms.generic.base.ContextMixin``
        """

        return super().get_context_data(
            training=self.object.training,
            steps=Step.objects.non_deleted().for_roleplay(self.object),
            **kwargs)


class RoleplayUpdate(RoleplayEditBase, generic.UpdateView):
    """The view to update a ``mtp.apps.roleplays.models.Roleplay`` instance.

    """
    success_message = _("Your roleplay was successfully updated.")

    def get_context_data(self, **kwargs):
        """Add the ``Training`` and the related ``Steps`` to the context.

        For the parameters, see ``django.forms.generic.base.ContextMixin``
        """

        return super().get_context_data(
            training=self.object.training,
            steps=Step.objects.non_deleted().for_roleplay(self.object),
            **kwargs)


class StepBaseEdit(RestrictToSuperuserOrTrainerMixin):
    """Base view to create or update a given instance of
    ``mtp.apps.roleplays.models.Step``

    """
    model = Step
    fields = [
        'name',
        'description',
        'duration'
    ]
    pk_url_kwarg = 'step_uuid'

    def get_queryset(self):
        """Queryset to use to filter only steps associated to the current roleplay.

        """
        return Step.objects.non_deleted().for_roleplay(self.roleplay)

    def get_success_url(self):
        """Get the url where to go back when an action is done on a step.

        It actually returns the url to edit the roleplay.

        """
        return self.object.get_edit_url()

    @cached_property
    def roleplay(self):
        """Returns the current roleplay based on the ``roleplay_uuid``
        argument in the url.

        Raises
        ------
        ``django.http.Http404``
            If the current user cannot see the roleplay, or the roleplay
            doesn't exist.

        """
        return get_object_or_404(
            Roleplay.objects.non_deleted().for_user(self.request.user),
            pk=self.kwargs['roleplay_uuid']
        )

    def get_context_data(self, **kwargs):
        """Add ``roleplay`` to the context.

        For the parameters, see ``django.forms.generic.base.ContextMixin``
        """

        return super().get_context_data(roleplay=self.roleplay, **kwargs)


class StepsSort(OrderableListMixin, StepBaseEdit, generic.ListView):
    """View to sort the ``Step`` instances of the given ``Roleplay``."""


class StepCreate(SuccessMessageMixin, StepBaseEdit, generic.CreateView):
    """The view to create a ``mtp.apps.roleplays.models.Step`` instance.

    Notes
    -----

    The ``roleplay`` is automatically set on saving.

    """
    success_message = _("Your step was successfully created.")

    def form_valid(self, form):
        """Set the ``roleplay``, save the step 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 edit url of the roleplay.

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

        """

        self.object = form.save(commit=False)
        self.object.roleplay = self.roleplay
        self.object.save()

        self.send_message(form.cleaned_data)

        return HttpResponseRedirect(self.get_success_url())


class StepUpdate(SuccessMessageMixin, StepBaseEdit, generic.UpdateView):
    """The view to update a ``mtp.apps.roleplays.models.Step`` instance.

    """
    success_message = _("Your step was successfully updated.")

    def get_context_data(self, **kwargs):
        """Add the related ``Criterion`` to the context.

        For the parameters, see ``django.forms.generic.base.ContextMixin``
        """

        return super().get_context_data(
            criteria=Criterion.objects.non_deleted().for_step(self.object),
            **kwargs)


class StepDelete(SuccessMessageMixin, StepBaseEdit, SyncableDeleteView):
    """The view to delete a ``mtp.apps.roleplays.models.Step`` instance.

    """
    success_message = _("Your step was successfully deleted.")

    def get_success_url(self):
        """Get the url where to go back when a step is deleted.

        It actually returns the url to edit the roleplay.

        """

        return self.object.roleplay.get_detail_url()


class CriterionEditBase(RestrictToSuperuserOrTrainerMixin):
    """Base view to create or update a given instance of
    ``mtp.apps.roleplays.models.Criterion``

    """
    model = Criterion
    form_class = CriterionForm
    pk_url_kwarg = 'criterion_uuid'

    def get_queryset(self):
        """Queryset to use to filter only criteria associated to the current step.

        """
        return Criterion.objects.non_deleted().for_step(self.step)

    def get_form_kwargs(self):
        kwargs = super().get_form_kwargs()

        # If the roleplay is assessing, we must be able to add dimensions
        # to the criterion. To be able to do so, the ``AssessableMixin`` used
        # by the form expects additional initialization kwargs.
        roleplay = self.step.roleplay
        if roleplay.is_assessing:
            kwargs['training'] = roleplay.training
            kwargs['activity'] = roleplay

        return kwargs

    def get_success_url(self):
        """Get the url where to go back when an action is done on a criterion.

        It actually returns the url to edit the step.

        """
        return self.object.step.get_edit_url()

    @cached_property
    def step(self):
        """Return the current step based on the ``step_uuid`` argument
        in the url.

        Raises
        ------
        ``django.http.Http404``
            If the current user cannot see the step, or the step doesn't exist.

        """
        return get_object_or_404(
            Step.objects.non_deleted().for_user(self.request.user),
            pk=self.kwargs['step_uuid']
        )

    def get_context_data(self, **kwargs):
        """Add ``step`` to the context.

        For the parameters, see ``django.forms.generic.base.ContextMixin``
        """

        return super().get_context_data(step=self.step, **kwargs)


class CriteriaSort(OrderableListMixin, CriterionEditBase, generic.ListView):
    """View to sort the ``Criterion`` instances of the given ``Step``."""


class CriterionCreate(SuccessMessageMixin, AssessableCreateMixin,
                      CriterionEditBase, generic.CreateView):
    """The view to create a ``mtp.apps.roleplays.models.Criterion`` instance.

    Notes
    -----

    The ``step`` is automatically set on saving.

    """
    success_message = _("Your criterion was successfully created.")

    @cached_property
    def is_assessing_container(self):
        return self.step.roleplay.is_assessing

    def form_valid(self, form):
        """Set the ``step``, save the criterion 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 edit url of the step.

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

        """

        self.object = form.save(commit=False)
        self.object.step = self.step
        self.object.save()

        # Add many to many relations
        form.save_m2m()
        self.send_message(form.cleaned_data)

        return HttpResponseRedirect(self.get_success_url())


class CriterionUpdate(SuccessMessageMixin, CriterionEditBase, generic.UpdateView):
    """The view to update a ``mtp.apps.roleplays.models.Criterion`` instance.

    """
    success_message = _("Your criterion was successfully updated.")


class CriterionDelete(SuccessMessageMixin, CriterionEditBase, SyncableDeleteView):
    """The view to delete a ``mtp.apps.roleplays.models.Criterion`` instance.

    """
    success_message = _("Your criterion was successfully deleted.")


class XLSRoleplayExportView(XLSExportDetailView):
    """Export XLS Roleplay."""

    resource = RoleplayResource
    pk_url_kwarg = 'roleplay_uuid'


class XLSRoleplayImportView(ActivityXLSImportView):
    """View for import roleplay."""

    def import_data(self, sheet_name):
        resource = RoleplayResource
        data_object = self.reader.read_values(
            sheet_name,
            len(resource.get_fields(allow_nested=False))
        )

        if self.is_compatible(data_object, resource):
            obj, __ = self.create_object(
                data_object, resource=resource, force_creation=True,
                training=self.training, is_assessing=self.is_assessing
            )
        else:
            fields = resource.get_display_fields(allow_nested=False)
            raise ImportDataError(
                _("Data is not compatible, we need this fields %(fields)s.") % {'fields': fields},
                line=data_object.line
            )

        for data in self.reader.read_table(sheet_name):
            if self.is_compatible(data, StepResource):
                duration_field = StepResource.field_name_to_display_field('duration')
                if duration_field in data:
                    data[duration_field] = datetime.timedelta(minutes=float(data[duration_field]))
                step, __ = self.create_object(
                    data, resource=StepResource, force_creation=True,
                    roleplay=obj
                )
            elif self.is_compatible(data, CriterionResource):
                try:
                    self.create_object(
                        data, resource=CriterionResource, force_creation=True, step=step
                    )
                except UnboundLocalError:
                    raise ImportDataError(
                        _("Problem in your file: no step is provided before criterions."),
                        line=data.line
                    )
            else:
                raise ImportDataError(_("Data is not compatible"), line=data.line)

        return obj
