"""This module holds the ``Training`` related views for the ``trainings`` app."""

import json

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

from django_filters.views import FilterView
from waffle import switch_is_active

from mtp.apps.common.views_utils import (
    RestrictToSuperuserOrTrainerMixin,
    SuccessMessageMixin,
    SyncableDeleteView,
)
from mtp.apps.surveys.models import Pool
from mtp.apps.tasks.views import TaskListJsonView
from mtp.apps.trainings.forms import TrainingOptionsForm

from ..filters import TrainingFilterSet
from ..forms import (
    TrainingArchiveForm,
    TrainingForm,
)
from ..models import (
    Training,
    TrainingClientOptions,
    TrainingTask,
)


class TrainingBaseView(RestrictToSuperuserOrTrainerMixin):
    """Base view for all views related to the trainings.

    Define the queryset to use, depending on the user, and set the
    ``current_main_section``in the context.

    """

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

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

        if switch_is_active('enable_surveys'):
            user_can_have_survey = bool(Pool.non_deleted.count_valid_for_user(self.request.user))
        else:
            user_can_have_survey = False

        return super().get_context_data(
            current_main_section='trainings',
            user_can_have_survey=user_can_have_survey,
            **kwargs
        )

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

        The Queryset is filtered accordingly to the training's ``owner``.

        Returns
        -------
        Queryset of ``mtp.apps.trainings.models.Training``
            A lazy queryset with the wanted trainings.

        See Also
        --------
        ``mtp.apps.trainings.managers.TrainingQueryset``

        """

        return Training.objects.non_deleted().owned_by(
            self.request.user
        ).annotate_is_duplicable_by(self.request.user)


class TrainingEditBase(SuccessMessageMixin, TrainingBaseView):
    """Base view to edit a given instance of ``mtp.apps.trainings.models.Training`` instance."""

    model = Training
    form_class = TrainingForm
    pk_url_kwarg = 'training_uuid'

    def get_form_kwargs(self):
        """Return the form kwargs accordingly to the context.

        See Also
        --------
        ``mtp.apps.trainings.forms.TrainingForm.__init__`` method.

        """
        kwargs = super().get_form_kwargs()
        kwargs['current_user'] = self.request.user

        return kwargs

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

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

        return super().get_context_data(
            current_sub_section='general_informations',
            **kwargs
        )

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

        It actually returns the url to list of the trainings.

        """

        return self.model.get_list_url()

    def get_queryset(self):
        """The user may edit only the trainings he own."""

        return super().get_training_queryset_for_current_user().owned_by(
            self.request.user
        )


class TrainingCreateView(TrainingEditBase, generic.CreateView):
    """The view to create a ``mtp.apps.training.models.Training`` instance.

    Notes
    -----
    The ``owner`` is automatically set when saving the instance.

    """

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

    def form_valid(self, form):
        """Set the ``owner``, save the training 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 activities list of the training.

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

        """

        training = form.save(commit=False)
        training.owner = self.request.user
        training.save()

        self.send_message(form.cleaned_data)

        return HttpResponseRedirect(training.get_activity_list_url())


class TrainingUpdateView(TrainingEditBase, generic.UpdateView):
    """The view to update a ``mtp.apps.training.models.Training`` instance."""

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

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

        It actually returns the url to the activities of the training.

        """

        return self.get_object().get_activity_list_url()


class TrainingPublishView(TrainingEditBase, generic.UpdateView):
    """The view to publish a ``mtp.apps.training.models.Training`` instance.

    """

    template_name_suffix = '_publish'
    success_message = _("Your training was successfully published.")

    def get_queryset(self):
        """Filter the trainings that can be published."""

        return super().get_queryset().filter_is_publishable()

    def post(self, request, *args, **kwargs):
        """Publish the training, and redirect to the list of training.

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

        Returns
        -------
        instance of django.http.HttpResponseRedirect
            A redirect to the created draft instance.

        """

        training = self.get_object()

        # Publish the training
        training.publish()

        self.send_message()

        return HttpResponseRedirect(self.get_success_url())


class TrainingPublishDuplicateView(TrainingEditBase, generic.UpdateView):
    """The view to publish and duplicate a given ``mtp.apps.training.models.Training`` instance."""

    template_name_suffix = '_duplicate_and_share'
    success_message = _("Your training is currently being published and duplicated.")

    def get_queryset(self):
        """Filter the trainings that can be published and duplicated."""

        return super().get_queryset().filter_is_publishable().filter_is_duplicable_by(
            self.request.user
        )

    def get_context_data(self, **kwargs):
        """Get the potential duplicates of the current training.

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

        to_duplicate = self.get_object().get_members_to_duplicate(replace_published=False)

        return super().get_context_data(
            total_duplicates_nb=len(to_duplicate),
            **kwargs
        )

    def post(self, request, *args, **kwargs):
        """Publish the training and duplicate it.

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

        Returns
        -------
        instance of `django.http.HttpResponseRedirect`
            A redirect to the list of trainings.
        """

        training = self.get_object()
        training.publish()
        training.duplicate_to_members(
            replace_published=False,
            async=True,
            author=self.request.user
        )
        self.send_message()

        return HttpResponseRedirect(self.get_success_url())


class TrainingArchiveView(TrainingEditBase, generic.UpdateView):
    """View to archive a ``Training`` instance, with confirmation"""

    template_name_suffix = '_archive'
    success_message = _("Your training was successfully archived.")
    form_class = TrainingArchiveForm

    def get_queryset(self):
        """Filter the trainings that can be archived."""

        return super().get_training_queryset_for_current_user().exclude_is_archived()

    def form_valid(self, form):
        """Archive the training and the children if asked to do so.

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

        """

        self.object.archive(cascade=form.cleaned_data.get('archive_children', False))

        self.send_message()

        return HttpResponseRedirect(self.get_success_url())


class TrainingCreateDraftView(TrainingEditBase, generic.UpdateView):
    """The view to create a new draft from a ``mtp.apps.training.models.Training``instance."""

    template_name_suffix = '_draft'
    success_message = _("The new version is currently being created.")

    def get_queryset(self):
        """Filter the trainings that can be drafted."""

        return super().get_queryset().filter_is_draftable()

    def get_context_data(self, **kwargs):
        """Check if the draft creation and add in the context the check and reason if not.

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

        context = super().get_context_data(**kwargs)

        training = self.object

        allowed, reason = training.is_new_draft_allowed()

        context['create_draft_allowed'] = allowed

        if not allowed:
            context['create_draft_not_allowed_reason'] = str(reason)

        return context

    def post(self, request, *args, **kwargs):
        """Create a draft from a ``mtp.apps.training.models.Training`` instance.

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

        Returns
        -------
        instance of django.http.HttpResponseRedirect
            A redirect to the created draft instance.

        Notes
        -----
        In the ``get`` method we stop the user if a new draft is being created.
        We don't do it in this ``post`` method, by passing ``skip_in_progress=True``, because it
        really should happen very rarely, when the user submits the form twice. So we simply
        ignore and do nothing if a draft creation is already in progress, and give the user its
        success message.

        """

        self.get_object().create_draft(
            author=self.request.user,
            async=True,
            skip_in_progress=True,
        )

        self.send_message()

        return HttpResponseRedirect(self.get_success_url())


class TrainingDuplicateView(TrainingEditBase, generic.UpdateView):
    """The view to duplicate a given ``mtp.apps.training.models.Training`` instance."""

    template_name_suffix = '_duplicate'
    success_message = _("Your training is currently being duplicated.")

    def get_queryset(self):
        """Filter the trainings that can be duplicated."""

        return super().get_queryset().filter_is_duplicable_by(self.request.user)

    def get_context_data(self, **kwargs):
        """Get the potential duplicates of the current training.

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

        to_duplicate = self.get_object().get_members_to_duplicate(replace_published=False)

        return super().get_context_data(
            total_duplicates_nb=len(to_duplicate),
            **kwargs
        )

    def post(self, request, *args, **kwargs):
        """Duplicate the training, and redirect the user.

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

        Returns
        -------
        instance of django.http.HttpResponseRedirect
            A redirect to the url of the list of trainings.

        """

        self.get_object().duplicate_to_members(replace_published=False, async=True,
                                               author=self.request.user)

        self.send_message()

        return HttpResponseRedirect(self.get_success_url())


class TrainingDeleteView(TrainingEditBase, SyncableDeleteView):
    """View to soft-delete a ``Training`` instance, with confirmation.

    A training can only be soft-deleted if he is in the ``DRAFT`` version_state
    (so we are sure it's not used by any ``TrainingSession`` instances).

    """

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

    def get_queryset(self):
        """Filter the trainings that can be soft-deleted."""
        return super().get_queryset().filter_is_draft()


class TrainingListView(TrainingBaseView, FilterView):
    """View to list the ``Training`` instances the current user is allowed to see."""

    model = Training  # to be able to access ``view.model`` in template
    paginate_by = 15
    template_name = 'cms/trainings/training_list.html'
    filterset_class = TrainingFilterSet

    def get_filterset_kwargs(self, filterset_class):
        """Set the additional kwargs for the filter form

        Parameters
        ----------
        filterset_class: object
            The filter form class to use

        Returns
        -------
        dict: A dict with the updated kwargs

        """

        kwargs = super().get_filterset_kwargs(filterset_class)

        kwargs.update({
            'business_unit_queryset': self.request.user.mgroups.non_deleted().business_units(),
            'job_profile_queryset': self.request.user.mgroups.non_deleted().job_profiles()
        })

        return kwargs

    def get_queryset(self):
        """Filter the training list according to the current user"""

        return super().get_training_queryset_for_current_user().select_related('business_unit')

    def get_context_data(self, **kwargs):
        """Add information about unfinished tasks asked by the user

        For the parameters, see ``django.forms.generic.base.ContextMixin``
        """
        context = super().get_context_data(**kwargs)

        # We'll display waiting tasks and finished ones only on the first page
        display_waiting = not context.get('page_obj') or context['page_obj'].number == 1

        # And we'll do this only for trainings displayed in the current list
        if display_waiting and context['training_list']:

            # But for the whole list, ie the trainings on all pages
            if context.get('paginator'):
                all_trainings = context['paginator'].object_list
            else:
                all_trainings = context['training_list']

            training_ids = list(all_trainings.values_list('pk', flat=True))

            # Get all the tasks pending for the user having as original training a training being
            # in the  current displayed list
            tasks = TrainingTask.objects.filter(
                user=self.request.user,
                task_type__in=TrainingTask.TASK_TYPES.TRAINING_CREATION.values,
                state__in=TrainingTask.TASK_STATES.UNREADY_STATES.values,
                training_id__in=training_ids,
            ).select_related('training').order_by('started_at')

            if tasks:
                # group them by type + original training
                context['waiting_tasks_grouped'] = TrainingTask.objects.group_tasks(tasks)
                # idem but for the js
                context['waiting_tasks_grouped_json'] = json.dumps(
                    TrainingTask.objects.group_tasks_for_json(tasks)
                )

            # Get the recently created trainings
            context['recent_trainings'] = all_trainings.recently_created(self.request.user)
            context['recent_trainings_ids'] = set(
                training.pk for training in context['recent_trainings']
            )

        return context


class WaitingAndRecentTrainingsJsonView(TaskListJsonView):
    """A simple list view for ajax only that will return a json view of training tasks"""

    model = TrainingTask

    def get_queryset(self):
        """Limit the returned task to only be the ones creating trainings"""

        return super().get_queryset().filter(
            task_type__in=TrainingTask.TASK_TYPES.TRAINING_CREATION.values
        )

    def get_recent_trainings(self):
        """Return the html of the recent trainings of the users

        Returns
        -------
        str
            The html with the list of recent trainings

        """

        trainings = Training.objects.non_deleted().available_for_user(
            self.request.user
        ).annotate_is_duplicable_by(
            self.request.user
        ).recently_created(
            self.request.user
        )

        return render_to_string('cms/trainings/training_list_include_trs.html', {
            'training_list': trainings
        })

    def render_task_to_dict(self, task):
        """Convert a task to a dict to be converted into Json to be put in the response

        Parameters
        ----------
        task: TrainingTask
            The task to convert to a dict

        Returns
        -------
        dict
            A dict with data from the task, that will be converted into Json

        """

        result = super().render_task_to_dict(task)

        result.update({
            'training_id': task.training_id,
            'task_type': task.task_type,
        })

        return result

    def render_json(self, context):
        """Prepare the json to return in ``render_to_response.

        This override the parent method to add a ``grouped_tasks`` to be used by the javascript,
        which is the result of a call to ``group_tasks`` with all the tasks in the context.

        For the parameters, see ``mtp.apps.tasks.views import TaskListJsonView``

        """

        result = super().render_json(context)
        result['grouped_tasks'] = TrainingTask.objects.group_tasks_for_json(context['object_list'])

        if self.request.GET.get('include_recent'):
            result['recent_trainings'] = self.get_recent_trainings()

        return result


class TrainingOptionsEditView(SuccessMessageMixin, TrainingBaseView, generic.UpdateView):
    """View to edit a ``TrainingClientOptions`` instance"""

    pk_url_kwarg = 'options_uuid'
    model = TrainingClientOptions
    form_class = TrainingOptionsForm
    success_message = _('The options was successfully updated.')

    @cached_property
    def training(self):
        """Try to get the current selected training from the url.

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

        Returns
        -------
        ``Training`` instance

        """

        return get_object_or_404(
            super().get_training_queryset_for_current_user().filter_is_editable(),
            pk=self.kwargs['training_uuid']
        )

    def get_queryset(self):
        """Return the queryset of options the current user can edit"""

        return TrainingClientOptions.objects.filter(
            training=self.training
        )

    def get_success_url(self):
        return self.training.get_activity_list_url()
