"""This module provides the ``Workshop`` and the ``WorkshopStep`` model for the ``workshops`` app"""

from statistics import mean, StatisticsError

import pandas as pd

from django.core.urlresolvers import reverse
from django.db import models
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _

from mtp.apps.common.limpyd_base.django_models import RedisBackedDjangoModel
from mtp.apps.common.models import Orderable
from mtp.apps.data_export.constants import EXCEL_STYLE_CHOICES
from mtp.apps.data_export.exporters import ExcelDataExporter
from mtp.apps.sync.models import Syncable, SyncableForPolymorphicMixin
from mtp.apps.trainings.models import (
    ResultsToExcelActivityMixin,
    Training,
    TrainingActivity
)

from mtp.discrete_apps.versionable.models import Dependency, PolymorphicModelForDependency

from ..constants import WORKSHOP_TYPE_CHOICES
from ..managers import DependencyWorkshopStepManager


class Workshop(RedisBackedDjangoModel, ResultsToExcelActivityMixin, TrainingActivity):
    """Django model representing a workshop.

    Attributes
    ----------
    is_broadcastable : django ``BooleanField`` field
        Can the workshop be broadcasted to the trainees devices ?
    is_steps_order_random : django ``BooleanField`` field
        Is the workshop's steps should be displayed in a random order ?
    is_answers_order_random: django ``BooleanField`` field
        Is the workshop's question's answers should be displayed in a random order ?
    are_questions_mandatory : django ``BooleanField`` field
        Must the questions of the workshop be all answered?
    is_questions_answers_order_random : django ``BooleanField`` field
        Should the answers of the workshop be displayed in a random order ?
    needs_approval : django ``BooleanField`` field
        Do the trainees need an approval from a trainer to open the workshop ?
    enable_score : django ``BooleanField`` field
        Do we want to enable the scoring on this workshop ?

    Notes
    -----
    This model inherits from ``TrainingActivity`` and therefore is a ``Dependency`` of
    the ``Training`` model.

    """
    is_broadcastable = models.BooleanField(default=False, verbose_name=_("broadcastable"))
    is_steps_order_random = models.BooleanField(default=False,
                                                verbose_name=_("randomize the steps order"))
    are_questions_mandatory = models.BooleanField(default=False, verbose_name=_(
        "questions are mandatory"), help_text=_("Does the questions must be mandatory ?"))
    is_questions_answers_order_random = models.BooleanField(  # pylint: disable=invalid-name
        default=False, verbose_name=_("randomize the questions answers order"))
    enable_score = models.BooleanField(default=False, verbose_name=_("with score"))

    verbose_activity_name = _('Quiz')

    def __str__(self):
        return self.name

    @cached_property
    def activity_type(self):
        """Return the activity type

        Returns
        -------
        ``CHOICE`` instance
            One of the ``WORKSHOP_TYPE_CHOICES`` choices.

        """

        if self.is_broadcastable:
            return WORKSHOP_TYPE_CHOICES.LIVEVOTING
        return WORKSHOP_TYPE_CHOICES.QUIZ

    @cached_property
    def redis_model(self):
        """Returns the ``DjangoBackedRedisModel`` linked to the current one."""

        from ..redis_models import Workshop as RedisWorkshop

        return RedisWorkshop

    @cached_property
    def available_step_types(self):
        """Get the available step types

        Returns
        -------
        list(dict)

        """

        return [
            {
                'create_url': cls.get_create_url(workshop=self),
                'verbose_name': cls.verbose_step_name
            }
            for cls in WorkshopStep.__subclasses__()
        ]

    @cached_property
    def assessing_parts(self):

        from .step_question import WorkshopStepQuestion

        return WorkshopStepQuestion.objects.filter(workshop=self).all()

    @cached_property
    def media_files(self):
        """Get a list of media file paths for this document

        Returns
        -------
        list(str)
            A list of media relative paths for this document

        """

        from .step_image import WorkshopStepImage

        return [
            question_image.image.name
            for question_image in WorkshopStepImage.objects.filter(workshop=self).non_deleted()
        ]

    def results_to_excel(self, session, path=None):
        """Get the session results of this workshop as an excel file

        For the parameters, see ``ResultsToExcelActivityMixin``

        """

        excel_exporter = ExcelDataExporter(path)

        # Add the sheet for the global scores
        # (data obtained with `get_results_excel_data` with the detailed parameter to `False`)
        excel_data = self.get_results_excel_data(session, detailed=False)
        excel_data['sheet_name'] = '%s - %s' % (
            _('Global scores'),
            self.name
        )
        excel_exporter.add_sheet(**excel_data)

        # Add the sheet for the detailed scores
        # (data obtained with the `get_results_excel_data` with the detailed parameter to `True`)
        excel_detailed_data = self.get_results_excel_data(session, detailed=True)
        excel_detailed_data['sheet_name'] = '%s - %s' % (
            _('Detailed answers'),
            self.name
        )
        excel_exporter.add_sheet(**excel_detailed_data)

        return excel_exporter.save()

    def get_results_excel_data(self, session, detailed=False):
        """Get the results data of this workshop for a given session

        Parameters
        ----------
        session: TrainingSession
            The training session we want the results for
        detailed: bool
            If False, only the score will be added to the output,
            If True, the verbatim (the details of the trainee answers for each step)
            will also be also be added to the output.

        Returns
        -------
        dict
            A dict containing the parameters for the ``ExcelDataExporter.add_sheet``

        """

        steps = self.assessing_parts.non_deleted()
        users = session.users.non_deleted()

        series_list = []
        header_styles = []

        for index, step in enumerate(steps, start=1):
            # Build the series for this step, containing the scores for each trainees
            step_series = pd.Series(
                [
                    step.get_trainee_session_score(user, session)
                    for user in users
                ],
                name='%d - %s' % (
                    index,
                    step.title
                )
            )

            # Append the max score available for this step to the series
            step_series[len(users)] = step.max_points_yield

            # Store the series and the styles for the header to apply to it
            series_list.append(step_series)
            header_styles.append(EXCEL_STYLE_CHOICES.WORKSHOP_STEP)

            # If the "detailed" flag was provided, store also the details of this steps
            # and the styles to apply to them
            if detailed:
                # pylint: disable=bad-builtin
                detailed_series, detailed_styles = map(
                    list, zip(*step.get_results_excel_data(session))
                )
                series_list.extend(detailed_series)
                header_styles.extend(detailed_styles)

        def build_data_frame(series):
            """Build a data frame for the given list of series

            Parameters
            ----------
            series: list(pandas.Series)
                The series to build the data frame from

            Returns
            -------
            pandas.DataFrame

            """

            data_frame = pd.concat(series, axis=1)

            # Pop the "Maximum points yield" index
            max_points = list(data_frame.loc[len(users)].values)
            data_frame = data_frame[:-1]

            # Add a "Mean" row containing the mean value for each columns
            data_frame.loc[str(_('Mean'))] = data_frame.mean(numeric_only=True)

            # Round the all the numeric values in the data frame
            data_frame = data_frame.round(2)

            # Finally, populate the indexes using the users last and first name (username)
            data_frame = data_frame.rename(
                index={
                    index: '%(last_name)s %(first_name)s (%(username)s)' % {
                        'last_name': user.last_name,
                        'first_name': user.first_name,
                        'username': user.username
                    }
                    for index, user in enumerate(users)
                }
            )

            # Re-add the "Maximum points yield" row
            data_frame.loc[str(_('Maximum points'))] = max_points

            # Add the "Total" column at the beginning
            data_frame.insert(0, str(_('Total')), data_frame.sum(axis=1))

            return data_frame

        # Prepend the styles for the "Total" column
        header_styles = [EXCEL_STYLE_CHOICES.HEADER] + header_styles

        return {
            'sheet_name': str(_('Global scores')),
            'data': build_data_frame(series_list),
            'header_styles': header_styles,
            'freeze_panes': [1, 1]
        }

    def get_trainee_session_score(self, session, trainee):
        """Get the score of this workshop for the given session and trainee

        The score of the workshop is the sum of all the points the trainee got
        from the workshop's assessing parts.

        Parameters
        ----------
        session: TrainingSession
            The session for which we want the score
        trainee: MTPUser
            The trainee for which we want the score

        Returns
        -------
        int
            The trainee's score for this workshop

        """

        return sum([
            self.redis_instance.get_trainee_score(session.pk, trainee.pk, part.id) or 0
            for part in self.assessing_parts
        ])

    def get_training_session_average_score(self, session):
        """Compute the average score of this workshop for the given session

        Parameters
        ----------
        session: TrainingSession
            The session we want to get the average score for.

        Returns
        -------
        float
            The average trainee score for this workshop

        """

        try:
            return mean([
                self.get_trainee_session_score(session, trainee)
                for trainee in session.users.non_deleted()
            ])
        except StatisticsError:
            return 0

    def get_absolute_url(self):
        """ Get the url of the current workshop.

        Returns
        -------
        string
            The wanted url.

        """
        return self.get_detail_url()

    def get_detail_url(self):
        """ Get the url to view the current workshop.

        Returns
        -------
        string
            The wanted url.

        """
        return reverse('workshops:workshop-detail', kwargs={'workshop_uuid': self.pk})

    def get_edit_url(self):
        """ Get the url to edit the current workshop.

        Returns
        -------
        string
            The wanted url.

        """
        return reverse('workshops:workshop-update', kwargs={'workshop_uuid': self.pk})

    def get_steps_sort_url(self):
        """ Get the url to sort the steps of current workshop.

        Returns
        -------
        string
            The wanted url.

        """
        return reverse('workshops:steps-sort', kwargs={'workshop_uuid': self.pk})

    def get_steps_delete_url(self):
        """Get the url to delete the steps of this workshop

        Returns
        -------
        str

        """

        return reverse('workshops:steps-delete', kwargs={'workshop_uuid': self.pk})

    @classmethod
    def get_create_url(cls, training):
        """ Get the url to create a new workshop.

        Parameters
        ----------
        training: Training
            The training for which we want to create a new workshop.

        Returns
        -------
        string
            The wanted url.

        """
        return reverse('trainings:workshop-create', kwargs={'training_uuid': training.pk})

    @classmethod
    def get_import_url(cls, training):
        """ Get the url to import a new workshop.

        Parameters
        ----------
        training: Training
            The training for which we want to import a new workshop.

        Returns
        -------
        string
            The wanted url.

        """
        return reverse(
            'workshops:import-xls',
            kwargs={'training_uuid': training.pk}
        )

    def get_export_url(self):
        """Get the url to export a workshop

        Returns
        -------
        string or None
            The wanted url or None if the Workshop is not exportable.

        """

        from . import WorkshopStepImage

        if self.steps.instance_of(WorkshopStepImage).exists():
            return None
        else:
            return reverse(
                'workshops:export-xls',
                kwargs={'workshop_uuid': self.pk}
            )


class WorkshopStep(Dependency,
                   PolymorphicModelForDependency,
                   Syncable,
                   SyncableForPolymorphicMixin,
                   Orderable):
    """Django model representing a workshop step.

    Attributes
    ----------
    title : django ``CharField`` field.
        The title of the step.
    description : django ``CharField`` field
        The description of the step.
    workshop : foreign key to ``mtp.apps.workshops.models.Workshop``
        The workshop this step belongs to.

    Notes
    -----
    This model is a ``Dependency`` of the ``Training`` model.

    """
    title = models.CharField(max_length=255, verbose_name=_("title"))
    description = models.CharField(
        max_length=500,
        blank=True,
        verbose_name=_('Description')
    )

    workshop = models.ForeignKey(
        'Workshop',
        related_name='steps',
        on_delete=models.CASCADE
    )

    versionable_link = (Training, 'workshop__training')
    non_lockable_fields = Dependency.non_lockable_fields + Syncable.non_lockable_fields

    objects = DependencyWorkshopStepManager()

    class Meta:
        unique_together = ('sort_order', 'workshop')
        ordering = ['sort_order']
        # Temporary fix for https://github.com/django-polymorphic/django-polymorphic/issues/229
        base_manager_name = 'base_objects'

    def __str__(self):
        return self.title

    def get_filtered_manager(self):
        """Override the ``get_filtered_manager`` from the ``Orderable`` parent

        We want the "sort_order" of all the step for the current workshop,
        not only for the current child class.

        """

        return self.workshop.steps
