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

All views in this module must inherit from ``EvaluationBaseView`` 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.

"""

from django.db.models import Prefetch
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.common.views_utils import (
    AjaxResponseMixin,
    EditFormWithFormsetMixin,
    OrderableListMixin
)
from mtp.apps.import_export.exceptions import ImportDataError
from mtp.apps.import_export.views import XLSExportDetailView, ActivityXLSImportView
from mtp.apps.trainings.views.activities import (
    TrainingActivityCreateBase,
    TrainingActivityDetailBase,
    TrainingActivityEditBase,
)

from .forms import (
    EvaluationExercisesCriteriaFormSet,
    EvaluationForm,
    ExerciseForm,
    ExercisesDeleteForm,
)
from .models import Criterion, Evaluation, Exercise
from .resources import EvaluationResource, EvaluationCriterionResource, EvaluationExerciseResource


class EvaluationBase(TrainingActivityDetailBase):
    """Base view to use on all views related to the evaluations.

    Define the queryset to use for the current user.

    """

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

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

        Returns
        -------
        Queryset of ``mtp.apps.evaluations.models.Evaluations``
            A lazy queryset with the wanted evaluations.

        """

        return super().get_queryset().instance_of(Evaluation)

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

        return self.object.get_detail_url()


class EvaluationEditBase(AjaxResponseMixin, EvaluationBase):
    """Base view to create or update a given instance of
    ``mtp.apps.evaluations.models.Evaluation``

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

    """
    model = Evaluation
    form_class = EvaluationForm
    pk_url_kwarg = 'evaluation_uuid'

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

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

        Returns
        -------
        Queryset of ``mtp.apps.evaluations.models.Evaluation``
            A lazy queryset with the wanted evaluations.

        """
        return super().get_evaluation_queryset_for_current_user()


class EvaluationCreate(EvaluationEditBase, TrainingActivityCreateBase):
    """The view to create a ``mtp.apps.evaluations.models.Evaluation`` instance."""

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


class EvaluationDetail(EvaluationEditBase, generic.DetailView):
    """The view to show the details of a ``mtp.apps.evaluations.models.Evaluation`` instance."""

    def get_queryset(self):
        """Prefetch the criteria and the exercises"""

        return super().get_queryset().prefetch_related(
            Prefetch(
                'exercises',
                queryset=Exercise.objects.non_deleted().prefetch_related(
                    Prefetch(
                        'criteria',
                        queryset=Criterion.objects.non_deleted(),
                        to_attr='non_deleted_criteria'
                    )
                ),
                to_attr='non_deleted_exercises'
            ),
        )


class EvaluationUpdate(EvaluationEditBase, TrainingActivityEditBase):
    """The view to update a ``mtp.apps.evaluations.models.Evaluation`` instance"""

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

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

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

        return super().get_context_data(
            exercises=Exercise.objects.non_deleted().for_evaluation(self.object),
            **kwargs
        )


class ExerciseBase(EvaluationBase):
    """Base view for ``mtp.apps.evaluations.models.Exercise``"""

    model = Exercise
    form_class = ExerciseForm
    pk_url_kwarg = 'exercise_uuid'

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

        """
        return Exercise.objects.non_deleted().for_evaluation(self.evaluation)

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

        It actually returns the url to see the evaluation details.

        """
        return self.evaluation.get_detail_url()

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

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

        """
        return get_object_or_404(
            self.get_evaluation_queryset_for_current_user(),
            pk=self.kwargs['evaluation_uuid']
        )

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

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

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

    def get_form_kwargs(self):
        """Add the current evaluation in the form kwargs."""

        kwargs = super().get_form_kwargs()
        kwargs['evaluation'] = self.evaluation

        return kwargs


class ExercisesSort(OrderableListMixin, ExerciseBase, generic.ListView):
    """View to sort the ``Exercise`` instances of the given ``Evaluation``."""


class ExercisesDelete(ExerciseBase, generic.FormView):
    """View to soft delete a list of ``Exercise`` instances"""

    template_name = 'evaluations/exercises_delete.html'
    form_class = ExercisesDeleteForm
    success_message = _('Your exercises were successfully deleted.')

    def form_valid(self, form):
        """Handle the soft-delete action on the exercises

        For the parameters, see ``generic.FormView``

        """

        super().form_valid(form)

        for exercise in form.cleaned_data['exercises']:
            exercise.remove()

        return HttpResponseRedirect(self.get_success_url())


class ExerciseBaseEdit(ExerciseBase, EditFormWithFormsetMixin):
    """Base view to create or update an ``Exercise`` instance"""

    formset_class = EvaluationExercisesCriteriaFormSet
    context_formset_name = 'criteria_formset'

    def get_formset(self, **kwargs):
        """Filter out the deleted ``Criterion`` in the formset

        Parameters
        ----------
        For the parameters, see ``EditFormWithFormsetMixin``

        """

        return super().get_formset(
            queryset=Criterion.objects.non_deleted(),
            **kwargs
        )


class ExerciseCreate(ExerciseBaseEdit, generic.CreateView):
    """The view to create a ``mtp.apps.evaluations.models.Exercise`` instance.

    Notes
    -----

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

    """

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

    def form_valid(self, form):
        """Handle the redirection accordingly to the "_addanother" post param.

        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 evaluation or to the new exercise url

        """

        super().form_valid(form)

        if '_addanother' in self.request.POST:
            return HttpResponseRedirect(self.evaluation.get_exercise_create_url())
        else:
            return HttpResponseRedirect(self.get_success_url())


class ExerciseUpdate(ExerciseBaseEdit, generic.UpdateView):
    """The view to update a ``mtp.apps.evaluations.models.Exercise`` instance."""

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


class XLSEvaluationExportView(XLSExportDetailView):
    """Export XLS Evaluation."""

    resource = EvaluationResource
    pk_url_kwarg = 'evaluation_uuid'


class XLSEvaluationImportView(ActivityXLSImportView):
    """ View for import evaluation """

    model = Evaluation
    template_example_file = 'import_export/Evaluation.template.xlsx'

    def import_data(self, sheet_name):

        resource = EvaluationResource
        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)
        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, EvaluationExerciseResource):
                exercise, __ = self.create_object(
                    data, resource=EvaluationExerciseResource, force_creation=True,
                    evaluation=obj
                )
            elif self.is_compatible(data, EvaluationCriterionResource):
                try:
                    if 'criteria verbatim description' in data:
                        data['criterion_type'] = 'QL'
                    elif 'criteria numeric description' in data:
                        data['criterion_type'] = 'QT'
                    self.create_object(
                        data, resource=EvaluationCriterionResource, force_creation=True,
                        exercise=exercise
                    )
                except UnboundLocalError:
                    raise ImportDataError(
                        _("Problem in your file: no exercise is provided before criterions"),
                        line=data.line
                    )
            else:
                raise ImportDataError(_("Data is not compatible."), line=data.line)

        return obj
