"""This module holds the models of the ``workshops`` app."""

from django.conf import settings
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.assessments.models import AssessableActivityPart
from mtp.apps.common.models import Orderable
from mtp.apps.sync.models import Syncable
from mtp.apps.trainings.models import Training, TrainingActivity
from mtp.discrete_apps.versionable.models import Dependency, PolymorphicModelForDependency

from .managers import (
    DependencyWorkshopStepManager,
    WorkshopStepQuestionAnswerManager,
    WorkshopStepQuestionAnswerQuerySet,
    WorkshopStepQuestionUserAnswerManager,
    WorkshopStepQuestionUserAnswerQuerySet
)


class Workshop(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 ?
    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(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 a verbose name for this activity type."""

        return _('Workshop')

    @cached_property
    def available_step_types(self):
        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):
        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

        """

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

    def compute_challenge_score(self, session, trainee):
        """Compute 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.

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

        """

        return sum(
            part.get_trainee_session_score(session=session, trainee=trainee)
            for part in self.assessing_parts
        )

    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})

    @classmethod
    def get_create_url(cls, training):
        """ Get the url of 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 of 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 of to export a workshop.

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

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


class WorkshopStep(Dependency, PolymorphicModelForDependency, Syncable, 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)

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

    versionable_link = (Training, 'workshop__training')

    objects = DependencyWorkshopStepManager()

    class Meta:
        unique_together = ('sort_order', 'workshop')
        ordering = ['sort_order']

    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

    def get_delete_url(self):
        """Get the url to soft-delete the step

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

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


class WorkshopStepImage(WorkshopStep):
    """Django model representing a workshop image step.

    Attributes
    ----------
    image : django ``ImageField``
        The image of the step.

    """
    image = models.ImageField(upload_to="workshops", verbose_name=_('Image'))

    step_name = 'image'
    verbose_step_name = _('Image')

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

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

        """
        return self.get_edit_url()

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

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

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

    @classmethod
    def get_create_url(self, workshop):
        """ Get the url to create a new image step.

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

        """
        return reverse('workshops:step-image-create', kwargs={'workshop_uuid': workshop.pk})


class WorkshopStepQuestion(AssessableActivityPart, WorkshopStep):
    """Django model representing a workshop question step.

    Attributes
    ----------
    is_multiple_choices : django ``BooleanField`` field
        Are the multiple answers possible for this question ?
    is_answers_order_random : django ``BooleanField`` field
        Should the answers for this question be displayed in a random order ?

    """
    is_multiple_choices = models.BooleanField(
        default=False,
        verbose_name=_("is multiple answer question")
    )
    is_answers_order_random = models.BooleanField(
        default=False,
        verbose_name=_("randomize the answers order")
    )
    is_all_or_nothing = models.BooleanField(
        default=False,
        verbose_name=_("trainee must find all the right answers to get the points")
    )

    step_name = 'question'
    verbose_step_name = _('Question')

    class Meta:
        ordering = ['sort_order']

    @property
    def is_single_choice(self):
        return not self.is_multiple_choices

    def is_valid_for_trainee(self, trainee, session):
        """Return the question "validity" accordingly to the provided user and session.

        A question is known as "valid", when the user got at least good answer on it (with the
        ``is_correct`` flag).
        If the question got the ``all_or_nothing`` flag, then the user must got all, and only the "valid"
        answers for the question to be valid.

        Returns
        -------
        bool
            True if this question is valid else False.

        """

        trainee_answers = self.user_answers.filter(
            user=trainee,
            training_session=session,
        )

        if self.is_all_or_nothing:
            correct_answers = self.answers.filter(is_correct=True).values_list('pk', flat=True)

            return set(trainee_answers.values_list('answer__pk', flat=True)) == set(correct_answers)
        else:
            return trainee_answers.filter(answer__is_correct=True).exists()

    def get_trainee_session_score(self, trainee, session):
        """Get the session score of the trainee for this question.

        Returns
        -------
        int
            The question score of the trainee

        """

        if self.is_all_or_nothing:
            if self.is_valid_for_trainee(trainee, session):
                # If the 'all_or_nothing' question is valid so only good answers have been answered.
                # The score is the points sum of good answers.
                return WorkshopStepQuestionAnswer.objects.filter(
                    question=self,
                    is_correct=True
                ).aggregate_points()
            else:
                # It's all or nothing, the question is invalid for this trainee so NOTHING (GAME OVER!)
                return 0
        else:
            # It's a normal question, the score is the points sum of user answer.
            return WorkshopStepQuestionUserAnswer.objects.filter(
                user=trainee,
                question=self,
                training_session=session
            ).aggregate_points()

    @cached_property
    def max_points_yield(self):
        valuable_answers = self.answers.filter(points__gt=0)
        if not valuable_answers.exists():
            return 0

        answer_points = valuable_answers.values_list('points', flat=True)
        if self.is_single_choice:
            return max(answer_points)
        else:
            return sum(answer_points)

    def get_actual_activity(self):
        return self.workshop

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

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

        """
        return self.get_edit_url()

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

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

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

    def get_create_answer_url(self):
        """ Get the url to create a new answer for this question.

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

        """
        return reverse('workshops:question-answer-create', kwargs={
            'question_uuid': self.pk})

    def get_answers_sort_url(self):
        """ Get the url to sort the answers of the current question.

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

        """
        return reverse('workshops:question-answers-sort', kwargs={'question_uuid': self.pk})

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

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

        """
        return reverse('workshops:step-question-create', kwargs={'workshop_uuid': workshop.pk})


class WorkshopStepQuestionAnswer(Dependency, Syncable, Orderable):
    """Django model representing a workshop question answer.

    Attributes
    ----------
    label : django ``CharField`` field
        The label for this answer.
    color : django ``CharField`` field
        The color for this answer (displayed on client's devices)
    is_correct : django ``BooleanField`` field
        Is the answer correct ?
    points : django ``IntegerField`` field
        The number of points this answer can give.
    question : foreign key to ``mtp.apps.workshops.models.WorkshopStepQuestion``
        The question instance this answer belongs to.

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

    """
    label = models.CharField(max_length=500, verbose_name=_("answer"))
    color = models.CharField(max_length=50, null=True, blank=True, verbose_name=_("color"))
    is_correct = models.BooleanField(default=False, verbose_name=_("correct"))
    points = models.IntegerField(default=0)

    question = models.ForeignKey('WorkshopStepQuestion', related_name='answers')

    objects = WorkshopStepQuestionAnswerManager.from_queryset(
        WorkshopStepQuestionAnswerQuerySet)()

    versionable_link = (Training, 'question__workshop__training')

    class Meta:
        unique_together = ('sort_order', 'question')
        ordering = ['sort_order']

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

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

        """
        return self.get_edit_url()

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

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

        """
        return reverse('workshops:question-answer-update', kwargs={
            'question_uuid': self.question.pk, 'answer_uuid': self.pk})

    def get_delete_url(self):
        """ Get the url to delete the answer.

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

        """
        return reverse('workshops:question-answer-delete', kwargs={
            'question_uuid': self.question.pk, 'answer_uuid': self.pk})


class WorkshopStepQuestionUserAnswer(Syncable):
    """Django model representing a workshop question answer.

    Attributes
    ----------
    id : django field, instance of ``django_extensions.db.fields.UUIDField``
    user : foreign key to ``mtp.apps.accounts.models.MTPUser``
        The user who made this answer.
    answer : foreign key to ``mtp.apps.workshops.models.WorkshopStepQuestionAnswer``
        The workshop answer this user answer belongs to.
    text : django ``TextField`` field
        The user text answer for the question.
    training_session : foreign key to ``mtp.apps.trainings.models.TrainingSession``
        The training session in which this user's answer was created.

    Notes
    -----
    This model is ``Syncable``.

    """
    user = models.ForeignKey(settings.AUTH_USER_MODEL)
    question = models.ForeignKey('WorkshopStepQuestion', related_name='user_answers')
    answer = models.ForeignKey('WorkshopStepQuestionAnswer', null=True)
    training_session = models.ForeignKey('trainings.TrainingSession')

    text = models.TextField()

    objects = WorkshopStepQuestionUserAnswerManager.from_queryset(
        WorkshopStepQuestionUserAnswerQuerySet)()

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)

        # Record assessment score if needed.
        question, answer = self.question, self.answer

        if question.should_be_assessed and answer is not None:

            score = question.get_trainee_session_score(self.user, self.training_session)

            question.record_assessment_score(
                user=self.user,
                points=score,
                session=self.training_session
            )
