"""This module holds the models for the ``evaluations`` app."""

from collections import OrderedDict

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 extended_choices import Choices

from mtp.apps.accounts.constants import INTERNAL_GROUP_TRAINEE, INTERNAL_GROUP_TRAINER
from mtp.apps.accounts.models import Group, MTPUser
from mtp.apps.common.models import Orderable
from mtp.apps.common.utils import upload_to_generator
from mtp.apps.sync.models import Syncable
from mtp.apps.trainings.models import Training, TrainingActivity
from mtp.discrete_apps.versionable.models import Dependency

from .managers import (
    CriterionManager,
    CriterionQueryset,
    ExerciseManager,
    ExerciseQueryset,
)


class EvaluationGroupPermission(Syncable):
    """Django model allowing to give permissions to designated groups during
    a given evaluation.

    Attributes
    ----------
    PERMISSION_TYPES : ``Choices`` instance
        It contains all available permissions:
        - ``MAY_PARTICIPATE``
        - ``MAY_EVALUATE_SELF``
        - ``MAY_EVALUATE_OTHERS``
        - ``MAY_OBSERVE_SELF``
        - ``MAY_OBSERVE_OTHERS``
        - ``MAY_MODERATE``

    evaluation : django ``ForeignKey``
        The evaluation for which we give this permission.
    group : django ``ForeignKey``
        The group to which we give this permission.
    permission : django ``CharField``
        The permission, a value from ``PERMISSION_TYPES``.

    Notes
    -----
    This model is used as the ``through`` attribute of a ``ManyToManyField``
    between the ``Evaluation`` and ``accounts.Group`` models.

    """
    PERMISSION_TYPES = Choices(
        ('MAY_PARTICIPATE', 'may_participate', _("May participate")),
        ('MAY_EVALUATE_SELF', 'may_evaluate_self', _("May evaluate self")),
        ('MAY_EVALUATE_OTHERS', 'may_evaluate_others', _("May evaluate others")),
        ('MAY_OBSERVE_SELF', 'may_observe_self', _("May observe self")),
        ('MAY_OBSERVE_OTHERS', 'may_observe_others', _("May observe others")),
        ('MAY_MODERATE', 'may_moderate', _("May moderate")),
        dict_class=OrderedDict,
    )

    evaluation = models.ForeignKey('Evaluation')
    group = models.ForeignKey('accounts.Group')
    permission = models.CharField(
        _('permission'),
        choices=PERMISSION_TYPES,
        max_length=32,
    )

    class Meta:
        unique_together = ('group', 'permission', 'evaluation')


class Evaluation(TrainingActivity):
    """Django model representing an evaluation.

    Attributes
    ----------
    name : django ``CharField``
        The name of this evaluation.
        Inherited from ``TrainingActivity``.
    training : django ``ForeignKey``
        The training this evaluation belongs to.
        Inherited from ``TrainingActivity``.
    sort_order : django ``PositiveIntegerField``
        The sort order of this evaluation.
        Inherited from ``Orderable``.

    EVALUATION_TYPES : ``Choices`` instance
        It contains all available permissions:
        - ``MAY_EVALUATE_SELF``
        - ``MAY_EVALUATE_OTHERS``
        - ``MAY_EVALUATE_ALL``

    evaluation_type : django ``CharField``
        The type of the evaluation, a value from ``EVALUATION_TYPES``
    permissions : django ``ManyToManyField``
        The permissions granted to specific user groups during
        this evaluation.
    preview : django ``ForeignKey``
        A downloadable preview for this evaluation.
    hidden : django ``BooleanField``
        True if this evaluation should be hidden.

    """

    EVALUATION_TYPES = Choices(
        ('MAY_EVALUATE_SELF', 'may_evaluate_self', _(
            "The candidate may evaluate themselves"
        )),
        ('MAY_EVALUATE_OTHERS', 'may_evaluate_others', _(
            "The candidate may evaluate others"
        )),
        ('MAY_EVALUATE_ALL', 'may_evaluate_all', _(
            "The candidate may evaluate themselves and others"
        )),
        dict_class=OrderedDict,
    )

    evaluation_type = models.CharField(
        _("evaluation type"),
        choices=EVALUATION_TYPES,
        default=EVALUATION_TYPES.MAY_EVALUATE_OTHERS,
        max_length=32,
    )

    permissions = models.ManyToManyField(
        'accounts.Group',
        blank=True,
        through='EvaluationGroupPermission'
    )

    hidden = models.BooleanField(default=False, verbose_name=_('hidden'))

    verbose_activity_name = _('Evaluation')

    non_duplicable_relations = ['permissions']

    def __str__(self):
        return self.name

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

        # First, we actually save the evaluation.
        super().save(*args, **kwargs)

        # Then, we are going to need a few groups...
        trainee_group = Group.objects.get(name=INTERNAL_GROUP_TRAINEE)
        trainer_group = Group.objects.get(name=INTERNAL_GROUP_TRAINER)

        perms = EvaluationGroupPermission.PERMISSION_TYPES

        # First, clean all permissions previously set
        EvaluationGroupPermission.objects.filter(evaluation=self).delete()

        # We set the appropriates permissions on the trainer group...
        trainer_permissions = [
            perms.MAY_MODERATE,
            perms.MAY_EVALUATE_SELF,
            perms.MAY_EVALUATE_OTHERS
        ]

        for permission in trainer_permissions:
            EvaluationGroupPermission.objects.get_or_create(
                evaluation=self,
                group=trainer_group,
                permission=permission
            )

        # ...and on the trainee group, depending on the ``evaluation_type`` attribute
        trainee_permissions_set = {
            self.EVALUATION_TYPES.MAY_EVALUATE_SELF: [perms.MAY_EVALUATE_SELF,
                                                      perms.MAY_PARTICIPATE],
            self.EVALUATION_TYPES.MAY_EVALUATE_OTHERS: [perms.MAY_EVALUATE_OTHERS,
                                                        perms.MAY_PARTICIPATE],
            self.EVALUATION_TYPES.MAY_EVALUATE_ALL: [perms.MAY_EVALUATE_SELF,
                                                     perms.MAY_EVALUATE_OTHERS,
                                                     perms.MAY_PARTICIPATE]
        }

        for permission in trainee_permissions_set[self.evaluation_type]:
            EvaluationGroupPermission.objects.get_or_create(
                evaluation=self,
                group=trainee_group,
                permission=permission
            )

    def _permissions_group_names(self, permission):
        """Get the group names for the given permission role

        Parameters
        ----------
        permission: ``ChoiceEntry``
            The ``EVALUATION_TYPES`` entry we want the associated groups for

        Returns
        -------
        list
            A list of group names

        """

        return EvaluationGroupPermission.objects.filter(
            evaluation=self,
            permission=permission,
        ).values_list('group__name', flat=True)

    @cached_property
    def candidates_group_names(self):
        """Get the list of group names for the ``MAY_PARTICIPATE`` role

        Returns
        -------
        list
            A list of group names

        """

        return self._permissions_group_names(
            EvaluationGroupPermission.PERMISSION_TYPES.MAY_PARTICIPATE
        )

    @cached_property
    def evaluators_group_names(self):
        """Get the list of group names for the ``MAY_EVALUATE_OTHERS`` role

        Returns
        -------
        list
            A list of group names

        """

        return self._permissions_group_names(
            EvaluationGroupPermission.PERMISSION_TYPES.MAY_EVALUATE_OTHERS
        )

    @cached_property
    def auto_evaluators_group_names(self):
        """Get the list of group names for the ``MAY_EVALUATE_SELF`` role

        Returns
        -------
        list
            A list of group names

        """

        return self._permissions_group_names(
            EvaluationGroupPermission.PERMISSION_TYPES.MAY_EVALUATE_SELF
        )

    @cached_property
    def observers_group_names(self):
        """Get the list of group names for the ``MAY_OBSERVE_OTHERS`` role

        Returns
        -------
        list
            A list of group names

        """

        return self._permissions_group_names(
            EvaluationGroupPermission.PERMISSION_TYPES.MAY_OBSERVE_OTHERS
        )

    @cached_property
    def auto_observers_group_names(self):
        """Get the list of group names for the ``MAY_OBSERVE_SELF`` role

        Returns
        -------
        list
            A list of group names

        """

        return self._permissions_group_names(
            EvaluationGroupPermission.PERMISSION_TYPES.MAY_OBSERVE_SELF
        )

    @cached_property
    def moderators_group_names(self):
        """Get the list of group names for the ``MAY_MODERATE`` role

        Returns
        -------
        list
            A list of group names

        """

        return self._permissions_group_names(
            EvaluationGroupPermission.PERMISSION_TYPES.MAY_MODERATE
        )

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

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

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

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

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

        """
        return self.get_detail_url()

    def get_detail_url(self):
        """ Get the url to view the details of the current evaluation.

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

        """
        return reverse('evaluations:evaluation-detail', kwargs={'evaluation_uuid': self.pk})

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

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

        """
        return reverse('evaluations:evaluation-update', kwargs={'evaluation_uuid': self.pk})

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

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

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

    def get_export_url(self):
        """ Get the url of to export a evaluation.

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

        """
        return reverse('evaluations:evaluation-export-xls', kwargs={'evaluation_uuid': self.pk})

    def get_exercise_create_url(self):
        """ Get the url to create an exercise for the current evaluation.

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

        """
        return reverse('evaluations:exercise-create', kwargs={'evaluation_uuid': self.pk})

    def get_exercises_sort_url(self):
        """ Get the url to order the exercises of the current evaluation.

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

        """
        return reverse('evaluations:exercises-sort', kwargs={'evaluation_uuid': self.pk})


class Exercise(Orderable, Syncable, Dependency):
    """Django model representing an evaluation exercise.

    Attributes
    ----------
    name : django ``CharField``
        The name of the exercise, mandatory.
    description : django ``TextField``
        Optional description for this exercise.

    start_range : django ``PositiveIntegerField``
    end_range : django ``PositiveIntegerField``
    step : django ``PositiveIntegerField``
    preview: django ``ImageField``

    sort_order : django ``PositiveIntegerField``
        The sort order of this exercise.
        Inherited from ``Orderable``.

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

    """
    name = models.CharField(max_length=50, verbose_name=_('name'))
    description = models.TextField(verbose_name=_('description'), blank=True)
    evaluation = models.ForeignKey('Evaluation', related_name='exercises')

    start_range = models.PositiveIntegerField(default=1)
    end_range = models.PositiveIntegerField(default=5)
    step = models.PositiveIntegerField(default=1)

    preview = models.ImageField(
        blank=True, null=True,
        verbose_name=_('preview'),
        upload_to=upload_to_generator,
    )

    objects = ExerciseManager.from_queryset(ExerciseQueryset)()

    versionable_link = (Training, 'evaluation__training')

    def __str__(self):
        return self.name

    def get_sync_files(self):
        return [{'name': self.preview.name, 'hash': None}, ]

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

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

        """
        return reverse('evaluations:exercise-update', kwargs={
            'evaluation_uuid': self.evaluation.pk,
            'exercise_uuid': self.pk
        })

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

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

        """
        return reverse('evaluations:exercise-delete', kwargs={
            'evaluation_uuid': self.evaluation.pk,
            'exercise_uuid': self.pk
        })

    def get_criterion_create_url(self):
        """ Get the url to create a criterion for the current exercise.

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

        """
        return reverse('evaluations:criterion-create', kwargs={
            'evaluation_uuid': self.evaluation.pk,
            'exercise_uuid': self.pk
        })

    def get_criteria_sort_url(self):
        """ Get the url to order the criteria of the current exercise.

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

        """
        return reverse('evaluations:criteria-sort', kwargs={
            'evaluation_uuid': self.evaluation.pk,
            'exercise_uuid': self.pk
        })


class Criterion(Orderable, Syncable, Dependency):
    """Django model representing an evaluation criterion.

    Attributes
    ----------
    CRITERION_TYPES : ``Choices`` instance
        It contains all available types of criterion:
        - ``QUALITATIVE`` : the rating expects a text value from the judge
        - ``QUANTITATIVE`` : the rating expects a numeric value from the judge

    description : django ``TextField``
        A mandatory description for this criterion.
    exercise: django ``ForeignKey``
        The exercise for which this criterion exists, mandatory.
    criterion_type : django ``CharField``
        Hold a value from ``CRITERION_TYPES``.

    sort_order : django ``PositiveIntegerField``
        The sort order of this criterion.
        Inherited from ``Orderable``.

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

    """

    CRITERION_TYPES = Choices(
        ('QUALITATIVE', 'QL', _('Qualitative')),
        ('QUANTITATIVE', 'QT', _('Quantitative')),
        dict_class=OrderedDict,
    )

    exercise = models.ForeignKey('Exercise', related_name='criteria')
    description = models.TextField(verbose_name=_('description'))
    criterion_type = models.CharField(
        max_length=2, blank=False, null=False,
        choices=CRITERION_TYPES,
        help_text=_('Type of criterion'),
        verbose_name=_('criterion type')
    )

    objects = CriterionManager.from_queryset(CriterionQueryset)()

    versionable_link = (Training, 'exercise__evaluation__training')

    class Meta:
        verbose_name_plural = 'criteria'
        ordering = ['sort_order']

    def __str__(self):
        return '%s - %s (%s)' % (
            self.exercise.name,
            self.description,
            self.criterion_type,
        )

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

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

        """
        return reverse('evaluations:criterion-update', kwargs={
            'evaluation_uuid': self.exercise.evaluation.pk,
            'exercise_uuid': self.exercise.pk,
            'criterion_uuid': self.pk
        })

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

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

        """
        return reverse('evaluations:criterion-delete', kwargs={
            'evaluation_uuid': self.exercise.evaluation.pk,
            'exercise_uuid': self.exercise.pk,
            'criterion_uuid': self.pk
        })

    def get_rating(self, trainee, session):
        return self.ratings.filter(trainee=trainee, session=session).first()


class Rating(Syncable):
    """Django model representing a rating specified for a criterion.

    Attributes
    ----------
    criterion: django ``ForeignKey``
        The criterion for which this rating applies.
    judge: django ``ForeignKey``
        The user who emitted this rating.
    trainee: django ``ForeignKey``
        The user who received this rating.
    session : foreign key to ``TrainingSession``
        The training session during which this rating was given.
    text: django ``TextField``
        Filled when the type of criterion is ``QUALITATIVE``
    score: django ``PositiveIntegerField``
        Filled when the type of criterion is ``QUANTITATIVE``
    """
    criterion = models.ForeignKey('Criterion', related_name='ratings')

    judge = models.ForeignKey(MTPUser, related_name='given_ratings')
    trainee = models.ForeignKey(MTPUser, related_name='received_ratings')
    session = models.ForeignKey('trainings.TrainingSession', null=True, blank=True,
                                related_name='evaluation_ratings')

    text = models.TextField(blank=True)
    score = models.PositiveIntegerField(null=True, blank=True)


