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

import csv

from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse
from django.http import HttpResponse, Http404
from django.shortcuts import get_object_or_404
from django.utils.functional import cached_property
from django.utils.translation import ugettext as _
from django.views.generic.detail import DetailView
from django.views.generic.edit import CreateView, UpdateView
from django.views.generic.list import ListView

from mtp.apps.common.utils import EXCEL_BOM
from mtp.apps.common.views_utils import (
    AjaxResponseMixin,
    ConditionalMixin,
    RestrictToSuperuserOrTrainerMixin,
    SuccessMessageMixin,
    SyncableDeleteView,
)
from mtp.apps.import_export.exceptions import ImportDataError
from mtp.apps.import_export.views import (
    XLSExportDetailView, ModelXLSImportView, ActivityXLSImportView
)
from mtp.apps.trainings.models import Training, TrainingSession

from .constants import BRAINSTORMER_TYPE_CHOICES
from .forms import (
    BrainstormerForm,
    BrainstormerPropositionForm,
    BrainstormerPropositionGroupForm,
)
from .models import (
    Brainstormer,
    BrainstormerProposition,
    BrainstormerPropositionGroup,
    BrainstormerTemplate,
    PropositionWall
)
from .resources import (
    BrainstormerGroupResource,
    BrainstormerPropositionResource,
    BrainstormerTemplateResource,
    PropositionWallResource
)


class BrainstormerTypeMixin:
    """A mixin with method to check and return brainstormer_type."""

    @cached_property
    def brainstormer_type(self):
        """Return the brainstormer_type.

        Notes
        -----
        brainstormer type come from either:

        - kwargs `brainstormer_type_slug` (when action is a create)
        - self.object.brainstormer_type (edit, read, delete)

        Raises
        ------
        ``django.http.Http404``
            If the brainstormer_type is not a valid choice.

        """

        brainstormer_type = self.kwargs.get("brainstormer_type_slug", None)
        if not brainstormer_type:
            try:
                brainstormer_type = self.object.brainstormer_type
            except AttributeError:
                raise Http404
        try:
            return BRAINSTORMER_TYPE_CHOICES.for_value(brainstormer_type)
        except KeyError:
            raise Http404


class BrainstormerSessionListView(BrainstormerTypeMixin,
                                  RestrictToSuperuserOrTrainerMixin,
                                  ListView):
    """ Return Brainstormer List for a session. """

    def get_queryset(self):
        session = get_object_or_404(TrainingSession, id=self.kwargs['session_uuid'])
        return Brainstormer.objects.filter(
            session=session,
            is_deleted=False,
            brainstormer_type=self.brainstormer_type.value
        )

    def get_context_data(self, **kwargs):
        context = super(
            BrainstormerSessionListView, self).get_context_data(**kwargs)
        session = get_object_or_404(TrainingSession, id=self.kwargs['session_uuid'])
        context.update({
            'session': session,
            'brainstormer_list': [obj.proxified() for obj in context['brainstormer_list']],
            'brainstormer_display': self.brainstormer_type.display,
            'brainstormer_type': self.brainstormer_type.value
        })
        return context


class BrainstormerDetailView(RestrictToSuperuserOrTrainerMixin, DetailView):
    """Detail of Brainstormer propositions."""

    model = Brainstormer
    pk_url_kwarg = 'brainstormer_uuid'

    def get_object(self, *args):
        """Function returning the proxy model associated with the brainstormer_type.

        For the parameters, see ``django.views.generic.details.SingleObjectMixin.get_object``
        """
        obj = super().get_object(*args)
        return obj.proxified()


class BrainstormerEditBaseMixin(BrainstormerTypeMixin, RestrictToSuperuserOrTrainerMixin,
                                SuccessMessageMixin):
    """ Edit base for Brainstormer. """

    model = Brainstormer
    pk_url_kwarg = 'brainstormer_uuid'

    def get_success_url(self):
        return reverse('brainstormers:list_by_session',
                       kwargs={
                           'session_uuid': self.object.session.id,
                           'brainstormer_type_slug': self.object.brainstormer_type
                       })

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

        For the parameters, see ``django.views.generic.details.SingleObjectMixin.get_context_data``
        """

        return super().get_context_data(
            brainstormer_display=self.brainstormer_type.display,
            **kwargs
        )


class BrainstormerCreateView(ConditionalMixin, BrainstormerEditBaseMixin, CreateView):
    """ Create Brainstormer. """

    form_class = BrainstormerForm
    success_message = _('Your brainstormer was successfully created.')

    @cached_property
    def session(self):
        """Get the session associated with this request"""

        return get_object_or_404(TrainingSession, id=self.kwargs['session_uuid'])

    def get_success_url(self):
        """Get the url of the redirection when create is successful"""

        return reverse('brainstormers:list_by_session',
                       kwargs={
                           'session_uuid': self.session.id,
                           'brainstormer_type_slug': self.brainstormer_type.value
                       })

    def get_initial(self):
        """Get initial data for the form"""

        initial = {'session': self.session.id, 'brainstormer_type': self.brainstormer_type.value}

        return initial

    def is_condition_met(self):
        """Only allow create when session is not closed"""

        return not self.session.is_closed

    def condition_not_met(self):
        """Action to take if the session is closed.

        Raises
        ------
        PermissionDenied
            When the session is closed.

        """

        raise PermissionDenied


class BrainstormerUpdateView(BrainstormerEditBaseMixin, UpdateView):
    """ Update Brainstormer. """

    form_class = BrainstormerForm
    success_message = _('Your brainstormer was successfully updated.')


class BrainstormerDeleteView(BrainstormerEditBaseMixin, SyncableDeleteView):
    """ Delete Brainstormer. """

    success_message = _('Your brainstormer was successfully deleted.')


class BrainstormerTemplateEditBaseMixin(
        AjaxResponseMixin, RestrictToSuperuserOrTrainerMixin, SuccessMessageMixin):
    """ Edit base for Brainstormer Template. """

    model = BrainstormerTemplate
    pk_url_kwarg = 'template_uuid'
    fields = [
        'name',
        'description'
    ]

    def get_form(self, form_class=None):
        """Override the max_length and the help_text attributes on the field name

        We can't enforce this at the model level since the "name" field
        is inherited from ``TrainingActivity``.

        For the parameters, see ``django.views.generics.edit.ModelFormMixin``
        """

        form = super().get_form(form_class)
        form.fields['name'].widget.attrs['maxlength'] = 100
        form.fields['name'].help_text = _('Maximum 100 characters')

        return form


class BrainstormerTemplateCreateView(BrainstormerTypeMixin,
                                     BrainstormerTemplateEditBaseMixin,
                                     CreateView):
    """ Create Brainstormer Template. """

    success_message = _('Your brainstormer template 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']
        )

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

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

        """

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

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

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

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

        return super().form_valid(form)


class BrainstormerTemplateDetailView(BrainstormerTemplateEditBaseMixin, DetailView):
    """ Detail Template Brainstormer. """


class BrainstormerTemplateUpdateView(BrainstormerTemplateEditBaseMixin, UpdateView):
    """ Update Template Brainstormer. """

    success_message = _('Your brainstormer template was successfully updated.')


class BrainstormerTemplateDeleteView(BrainstormerTemplateEditBaseMixin, SyncableDeleteView):
    """ Delete Template Brainstormer. """

    success_message = _('Your brainstormer template was successfully deleted.')


class BrainstormerPropositionEditBaseMixin(RestrictToSuperuserOrTrainerMixin, SuccessMessageMixin):
    """ Edit base for Brainstormer Proposition. """

    model = BrainstormerProposition
    pk_url_kwarg = 'proposition_uuid'

    def get_success_url(self):
        return reverse(
            'brainstormers:detail',
            kwargs={'brainstormer_uuid': self.kwargs['brainstormer_uuid']}
        )


class BrainstormerPropositionCreateView(BrainstormerPropositionEditBaseMixin, CreateView):
    """Create a BrainstormerProposition."""

    form_class = BrainstormerPropositionForm
    success_message = _('Your brainstormer proposition was successfully created.')

    def get_form(self, form_class=None):
        """Edit the form according the current brainstormer.

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

        form = super().get_form(form_class)
        brainstormer = get_object_or_404(Brainstormer, id=self.kwargs['brainstormer_uuid'])

        # Change field queryset for adapt them to current brainstormer
        form.fields['groups'].queryset = brainstormer.groups.non_deleted()
        form.fields['user'].queryset = brainstormer.session.users.non_deleted()
        form.initial['brainstormer'] = brainstormer
        return form


class BrainstormerPropositionUpdateView(BrainstormerPropositionEditBaseMixin, UpdateView):
    """ Update BrainstormerProposition. """

    form_class = BrainstormerPropositionForm
    success_message = _('Your brainstormer proposition was successfully updated.')


class BrainstormerPropositionDeleteView(BrainstormerPropositionEditBaseMixin, SyncableDeleteView):
    """ Delete BrainstormerProposition. """

    success_message = _('Your brainstormer proposition was successfully deleted.')


class BrainstormerPropositionGroupEditBaseMixin(RestrictToSuperuserOrTrainerMixin,
                                                SuccessMessageMixin):
    """ Edit base for Brainstormer Proposition Group. """

    model = BrainstormerPropositionGroup
    pk_url_kwarg = 'group_uuid'

    def get_success_url(self):
        return reverse(
            'brainstormers:detail',
            kwargs={'brainstormer_uuid': self.kwargs['brainstormer_uuid']}
        )


class BrainstormerPropositionGroupCreateView(BrainstormerPropositionGroupEditBaseMixin, CreateView):
    """ Create BrainstormerPropositionGroup. """

    form_class = BrainstormerPropositionGroupForm
    success_message = _('Your brainstormer group was successfully created.')

    def get_initial(self):
        brainstormer = get_object_or_404(Brainstormer, id=self.kwargs['brainstormer_uuid'])
        initial = {'brainstormer': brainstormer}
        return initial


class BrainstormerPropositionGroupUpdateView(BrainstormerPropositionGroupEditBaseMixin, UpdateView):
    """ Update BrainstormerPropositionGroup. """

    form_class = BrainstormerPropositionGroupForm
    success_message = _('Your brainstormer group was successfully updated.')


class BrainstormerPropositionGroupDeleteView(BrainstormerPropositionGroupEditBaseMixin,
                                             SyncableDeleteView):
    """ Delete BrainstormerPropositionGroup. """

    success_message = _('Your brainstormer group was successfully deleted.')

    def delete(self, request, *args, **kwargs):
        group = self.get_object()
        if 'deleteAll' not in request.POST:
            group.propositions.clear()
        return super().delete(request, *args, **kwargs)


class CSVBrainstormerExportView(RestrictToSuperuserOrTrainerMixin, DetailView):
    """ Export CSV Brainstormer propositions. """

    model = Brainstormer
    pk_url_kwarg = 'brainstormer_uuid'

    def get_queryset(self):
        """For parameters see ``django.views.generic.detail.SingleObjectMixin.get_queryset``."""
        return super().get_queryset().select_related('session__training')

    def get(self, *args, **kwargs):
        super().get(*args, **kwargs)
        # Create the HttpResponse object with the appropriate CSV header.
        response = HttpResponse(content_type='text/csv')
        response['Content-Disposition'] = 'attachment; filename="brainstormer.csv"'

        writer = csv.writer(response, delimiter=';')

        self.object = self.object.proxified()

        # Header
        writer.writerow([EXCEL_BOM])  # BOM for excel
        writer.writerow(
            [
                'Export %s %s %s' % (
                    BRAINSTORMER_TYPE_CHOICES.for_value(self.object.brainstormer_type).display,
                    self.object.session.training.name,
                    self.object.session.start_date
                ),
                self.object.name
            ]
        )
        writer.writerow([])

        # Generation of CSV
        if isinstance(self.object, PropositionWall):

            grouped_propositions = self.object.get_grouped_propositions()
            for group, propositions in grouped_propositions.items():
                writer.writerow(
                    [
                        'Group',
                        group.name,
                        '%s propositions' % (len(propositions)),
                        'like(s)'
                    ]
                )
                for ind, proposition in enumerate(propositions):
                    writer.writerow(
                        [
                            str(ind + 1),
                            proposition.text,
                            proposition.user.first_name,
                            proposition.user.last_name,
                            proposition.likes__count
                        ]
                    )

            ungrouped = self.object.get_ungrouped_propositions()
            writer.writerow(
                [
                    'Group',
                    _('ungrouped'),
                    '%d propositions' % len(ungrouped),
                    'like(s)'
                ]
            )
            for ind, proposition in enumerate(ungrouped):
                writer.writerow(
                    [
                        str(ind + 1),
                        proposition.text,
                        proposition.user.first_name,
                        proposition.user.last_name,
                        proposition.likes__count
                    ]
                )
        else:
            for ind, proposition in enumerate(self.object.get_export_propositions()):
                writer.writerow(
                    [
                        str(ind + 1),
                        proposition.text,
                        proposition.user.first_name,
                        proposition.user.last_name,
                        proposition.likes__count
                    ]
                )
        return response


class XLSBrainstormerExportView(XLSExportDetailView):
    """Export XLS Brainstormer groups and propositions."""

    pk_url_kwarg = 'brainstormer_uuid'
    model = Brainstormer

    def get_queryset(self):
        """For parameters see ``django.views.generic.detail.SingleObjectMixin.get_queryset``."""
        return super().get_queryset().select_related('session__training')

    # pylint: disable=unused-argument
    def get_object(self, *args, **kwargs):
        """We need to get the object in order to determine the resource to use.

        For the parameters, see ``django.forms.generic.detail.SingleObjectMixin``
        """
        self.object = super().get_object()
        self.get_resource()
        return self.object

    def get_resource(self):
        """Determine the resource class to use."""
        self.resource = self.object.proxified().get_resource()

    @property
    def get_title(self):
        """Return the sheet title according to the brainstormer_type."""
        return "Export %s" % (
            BRAINSTORMER_TYPE_CHOICES.for_value(
                self.object.brainstormer_type
            ).display
        )


class XLSBrainstormerImportView(ConditionalMixin, ModelXLSImportView):
    """Import XLS Brainstormer"""

    @cached_property
    def session(self):
        """Get the session associated with this request"""

        return get_object_or_404(TrainingSession, pk=self.kwargs['session_uuid'])

    def is_condition_met(self):
        """Only allow import when session is not closed"""

        return not self.session.is_closed

    def condition_not_met(self):
        """Action to take if the session is closed.

        Raises
        ------
        PermissionDenied
            When the session is closed.

        """

        raise PermissionDenied

    def get_success_url(self):
        if not self.imported_object:
            return self.success_url
        return reverse(
            'brainstormers:detail',
            kwargs={'brainstormer_uuid': self.imported_object.pk}
        )

    def import_data(self, sheet_name):
        data_object = self.reader.read_values(sheet_name, 2)

        obj, __ = self.create_object(
            data_object, resource=PropositionWallResource, force_creation=True,
            training_session=self.session)

        for data in self.reader.read_table(sheet_name):
            if self.is_compatible(data, BrainstormerGroupResource):
                group, __ = self.create_object(
                    data, resource=BrainstormerGroupResource,
                    brainstormer=obj
                )
                obj.groups.add(group)
            elif self.is_compatible(data, BrainstormerPropositionResource):
                try:
                    self.create_object(
                        data, resource=BrainstormerPropositionResource,
                        group=group
                    )
                except UnboundLocalError:
                    raise ImportDataError(
                        _("Problem in your file: no group is provided before proposition"),
                        line=data.line
                    )
            else:
                raise ImportDataError(_("Data is not compatible."), line=data.line)
        return obj


class XLSBrainstormerTemplateExportView(XLSExportDetailView):
    """Export XLS Brainstormer groups and propositions."""

    resource = BrainstormerTemplateResource
    pk_url_kwarg = 'brainstormer_template_uuid'


class XLSBrainstormerTemplateImportView(ActivityXLSImportView):

    def import_data(self, sheet_name):

        resource = BrainstormerTemplateResource
        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 %s." % fields,
                line=data_object.line
            )

        return obj
