"""This module holds the managers and querysets for the ``roleplays`` app"""

from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import ugettext_lazy as _

from mtp.apps.assessments.managers import AssessableQuerySet
from mtp.apps.sync.managers import SyncableQuerySet


class StepManager(models.Manager):
    pass


class StepQueryset(SyncableQuerySet, models.QuerySet):
    """
    The Queryset for the
    ``mtp.apps.trainings.models.Step`` model

    """

    def for_roleplay(self, roleplay):
        """
        Filter only ``Step`` related to the given ``Roleplay``

        Returns
        -------
        Queryset of ``mtp.apps.trainings.models.Roleplay``

        """
        return self.filter(roleplay=roleplay)

    def for_user(self, user):
        """
        Filter only ``Step`` instances linked to a ``Training`` instance
        owned by the given user

        Returns
        -------
        Queryset of ``mtp.apps.trainings.models.Step``

        """

        return self.filter(roleplay__training__owner=user)


class CriterionManager(models.Manager):
    pass


class CriterionQueryset(AssessableQuerySet, SyncableQuerySet, models.QuerySet):
    """
    The Queryset for the
    ``mtp.apps.trainings.models.Criterion`` model

    """

    def for_step(self, step):
        """
        Filter only ``Criterion`` objects related to the given ``Step``.

        Returns
        -------
        Queryset of ``mtp.apps.trainings.models.Criterion`` objects.

        """
        return self.filter(step=step)

    def quantitative(self):
        """
        Filter only quantitative ``Criterion`` objects.

        Returns
        -------
        Queryset of ``mtp.apps.trainings.models.Criterion`` objects.

        """
        from .models import Criterion
        return self.filter(criterion_type=Criterion.CRITERION_TYPES.QUANTITATIVE)

    def qualitative(self):
        """
        Filter only qualitative ``Criterion`` objects.

        Returns
        -------
        Queryset of ``mtp.apps.trainings.models.Criterion`` objects.

        """
        from .models import Criterion
        return self.filter(criterion_type=Criterion.CRITERION_TYPES.QUALITATIVE)

    def boolean(self):
        """
        Filter only boolean ``Criterion`` objects.

        Returns
        -------
        Queryset of ``mtp.apps.trainings.models.Criterion`` objects.

        """
        from .models import Criterion
        return self.filter(criterion_type=Criterion.CRITERION_TYPES.BOOLEAN)


class RatingManager(models.Manager):

    def save_rating(self, rating):
        """Validate and save a ``mtp.apps.roleplays.models.Rating`` instance.

        Parameters
        ----------
        rating : instance of ``mtp.apps.roleplays.models.Rating``
            The rating instance to validate and save.

        Returns
        -------
        instance of ``mtp.apps.roleplays.models.Rating``
            The newly created, or updated, rating for the given criterion.

        Raises
        ------
        django.core.exceptions.ValidationError
            If anything is wrong, a ValidationError is raised, with a text and a code.
            Codes are:
            - invalid-roleplay
            - invalid-trainee
            - invalid-criterion
            - invalid-rating

        See also
        --------
        ``mtp.apps.roleplays.serializers.RatingSerializer.save`` method.
        """

        from .models import Criterion, Rating, Roleplay

        roleplay = rating.criterion.step.roleplay

        # Ensure the roleplay is correct for the current session.
        if roleplay.training != rating.training_session.training:
            raise ValidationError(
                _('Unexpected roleplay for this session.'),
                code='invalid-roleplay'
            )

        # Check that the user is a trainee...
        if not rating.trainee or not rating.trainee.is_trainee():
            raise ValidationError(
                _('User is not a trainee.'),
                code='invalid-trainee'
            )

        # ...and he's registered in the session
        if not rating.training_session.is_attendee(rating.trainee):
            raise ValidationError(
                _('User is not registered in the session.'),
                code='invalid-trainee'
            )

        # Ensure that quantitative criterion ratings have a correct ``score``.
        if rating.criterion.criterion_type == Criterion.CRITERION_TYPES.QUANTITATIVE:

            # The rating must be non-emtpy...
            if not rating.score:
                raise ValidationError(
                    _('Score is expected for a quantitative criterion.'),
                    code='invalid-rating'
                )

            # ...and not greater than roleplay ``criteria_rating_scale``
            if rating.score > roleplay.criteria_rating_scale:
                raise ValidationError(
                    _('Score cannot be greater than the roleplay settings allows.'),
                    code='invalid-rating'
                )

        elif rating.criterion.criterion_type == Criterion.CRITERION_TYPES.QUALITATIVE:
            # For the qualitative criterion, the rating ``text`` must be non-empty
            if not rating.text:
                raise ValidationError(
                    _('Text is expected for a qualitative criterion.'),
                    code='invalid-rating'
                )

        elif rating.criterion.criterion_type == Criterion.CRITERION_TYPES.BOOLEAN:

            # The rating must be non-emtpy...
            if rating.score == '':
                raise ValidationError(
                    _('Score is expected for a boolean criterion.'),
                    code='invalid-rating'
                )

            # ...And the score is 0 or 100
            if not rating.score in [0, 100]:
                raise ValidationError(
                    _('Score is expected to be 100 or 0 for a boolean criterion.'),
                    code='invalid-rating'
                )

        # Create or update the rating

        # If the rating are for the same ``training_session``, ``trainee`` and
        # ``criterion``, we update the score instead of creating another one

        rating, __ = Rating.objects.update_or_create(
            criterion=rating.criterion,
            training_session=rating.training_session,
            trainee=rating.trainee,
            defaults={
                'score': rating.score,
                'text': rating.text
            }
        )

        return rating
