from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.db.models.loading import get_models
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _

from django_extensions.db.fields import UUIDField

import waffle

from mtp.apps.sync.models import Syncable, UUIDPkModel

from . import UserScoreMixin


class Assessable(models.Model):
    """Abstract model to allow inheriting models to become assessing.

    Attributes
    ----------
    is_assessing : django ``BooleanField``
        If set to ``True``, the model instance is assessing.

    """
    is_assessing = models.BooleanField(default=False, verbose_name=_(
        "assessing"
    ))

    class Meta:
        abstract = True

    @staticmethod
    def get_repr(model):
        """"Return a string representation of the given model class.

        Returns
        -------
        string
            A representation of the model class, i.e. a string
            following this format:
            %(app_label)s.%(model_name)s

        """
        return '%s.%s' % (model._meta.app_label, model._meta.object_name)

    @classmethod
    def get_model_classes(cls):
        """Return all assessable activity part model classes.

        Returns
        -------
        list of strings
            Each string is the representation of a model class, i.e. a string
            following this format:
            %(app_label)s.%(model_name)s

        """
        return [Assessable.get_repr(m) for m in get_models()
                if issubclass(m, cls)]

    @cached_property
    def should_be_assessed(self):
        """Indicate if this model should be part of an assessment.

        Returns
        -------
        boolean

        Notes
        -----
        Will probably be overriden in inheriting classes.

        """
        return self.is_assessing and waffle.switch_is_active('enable_assessments')

    @property
    def should_be_debriefed(self):
        """Indicate if this model should be included in an assessment debrief.

        Returns
        -------
        boolean

        Notes
        -----
        Will probably be overriden in inheriting classes.

        """
        return False

    def prepare_debrief(self, trainee, session):
        """Build assessment debrief data for this model.

        Returns
        -------
        dict
            The debrief data. Highly dependant on the context.

        Notes
        -----
        Must be implemented in inheriting classes.

        """
        return NotImplemented


class AssessableActivity(Assessable):
    """Abstract model to allow MTP activities to become assessing.

    Attributes
    ----------
    weight : django ``PositiveIntegerField``
        The relative weight of this assessing activity in the training.

    Notes
    -----
    This model inherits from ``Assessable``.

    All assessable activities must have a ``training`` attribute pointing
    to the ``trainings.Training`` they belong to.

    """
    weight = models.PositiveIntegerField(default=1)

    class Meta:
        abstract = True

    @cached_property
    def assessing_parts(self):
        """Return a QuerySet of ``AssessingActivityPart``s linked to this
        activity.

        Returns
        -------
        QuerySet of ``AssessingActivityPart``s
            The activity parts composing this activity.

        Notes
        -----
        Must be implemented in inheriting classes.

        """
        return NotImplemented

    @cached_property
    def max_points_yield(self):
        """"Return the maximum number of points a trainee can earn while
        performing this activity.

        Returns
        -------
        int
            The number of points.

        Notes
        -----
        This is basically the sum of the ``max_points_yield``s of the parts
        of this assessing activity.

        """
        return sum([
            part.max_points_yield for part in self.assessing_parts
                if part.max_points_yield
        ])

    def normalize_score(self, score):
        """Convert the given score into a percentage.

        Returns
        -------
        int
            The score converted to a percentage.

        """
        try:
            return round(score / self.max_points_yield * 100.0)

        # If the activity cannot yield any points, the score is 0.
        except (TypeError, ZeroDivisionError):
            return 0


class AssessableActivityUserScore(UserScoreMixin, UUIDPkModel):
    """
    Django model used to keep track of the score of a user
    for a specific assessing activity.

    Inherits from ``BaseUserScore`` -- which is defined in
    ``__init__.py`` to be able to easily share it with models in
    ``competencies.py``.

    Attributes
    ----------
    activity_type : django ``ForeignKey``
        Foreign key to the ``ContentType`` of the activity.
    activity_id : ``django_extensions.db.fields.UUIDField``
        The primary key (uuid) of the activity.
    activity : django ``GenericForeignKey``
        The generic foreign key to the activity.
    activity_order : django ``PositiveIntegerField``
        The order in which this activity appears in the frontend app
        and in the scores export files.
    points : django ``IntegerField``
        The absolute amount of points earned in this activity.
        This differs from the `score` field insofar as the latter is normalized
        to a percentage value.

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

    See also
    --------
    The docstring of the ``record_assessment_score`` method below, to understand
    why the ``points`` field is necessary, at least at time of this writing.

    """
    activity_type = models.ForeignKey(ContentType, related_name='scores_on')
    activity_id = UUIDField()
    activity = GenericForeignKey('activity_type', 'activity_id')

    activity_order = models.PositiveIntegerField(null=True, blank=True)

    points = models.IntegerField(null=True, blank=True)

    class Meta:
        verbose_name = "activity part score"
        verbose_name_plural = "activity part scores"


class AssessableActivityPart(Assessable):
    """
    Abstract model to allow MTP activity parts to become assessing.
    (an activity part can be a quiz question, a roleplay criterion, ...)

    Inherits from ``Assessable``.

    Attributes
    ----------
    activity_type : django ``ForeignKey``
        Foreign key to the ``ContentType`` of the activity.
    activity_id : ``django_extensions.db.fields.UUIDField``
        The primary key (uuid) of the activity.
    activity : django ``GenericForeignKey``
        The generic foreign key to the activity.
    dimensions : django ``ManyToManyField``
        The assessing dimensions this step is linked to.

    """
    training = models.ForeignKey('trainings.Training', null=True, blank=True)

    activity_type = models.ForeignKey(ContentType, null=True, blank=True)
    activity_id = UUIDField(null=True, blank=True)
    activity = GenericForeignKey('activity_type', 'activity_id')

    dimensions = models.ManyToManyField('assessments.Dimension', blank=True)

    # We don't want to duplicate the dimension relation on the through table on the
    # ``dimensions`` M2M
    non_duplicable_through_relations = {
        'dimensions': ['dimension']
    }

    # Allow to access all user scores from a `AssessableActivityPart`
    user_scores = GenericRelation(
        'AssessableActivityPartUserScore',
        content_type_field='activity_part_type',
        object_id_field='activity_part_id'
    )

    class Meta:
        abstract = True

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

        # We need to keep a few specific references.
        if self.is_assessing:
            is_dirty = False
            actual_activity = self.get_actual_activity()

            actual_activity_id = actual_activity.id
            if self.activity_id != actual_activity_id:
                self.activity_id = actual_activity_id
                is_dirty = True

            actual_activity_training = actual_activity.training
            if self.training != actual_activity_training:
                self.training = actual_activity_training
                is_dirty = True

            if is_dirty:
                self.save()

    @property
    def max_points_yield(self):
        """"Return the maximum number of points a trainee can earn while
        performing this activity part.

        Returns
        -------
        int
            The number of points.

        Notes
        -----
        Must be implemented in inheriting classes.

        """
        return NotImplemented

    def get_actual_activity(self):
        return NotImplemented

    def normalize_score(self, score):
        """Convert the given score into a percentage.

        Returns
        -------
        int
            The score converted to a percentage.

        Notes
        -----
        Depends on the local implementation of the ``max_points_yield``
        method above.

        """
        try:
            return round(score / self.max_points_yield * 100.0)

        # If the activity part cannot yield any points, the score is 0.
        except (TypeError, ZeroDivisionError):
            return 0

    def record_assessment_score(self, user, points, session):
        """Record the score of a trainee for this activity part.

        Notes
        -----
        The score is recorded as an ``AssessableActivityPartUserScore`` object.
        It is previously normalized as a percentage using the ``normalize_score``
        method above.

        If ``AssessableActivityPartUserScore`` already exists, we will replace the
        score saved in a previously created object rather than creating a newone.

        """
        content_type = ContentType.objects.get_for_model(self)
        content_id = self.id

        # We record the score, or get a pre-existing score if one exists.
        record, created = AssessableActivityPartUserScore.objects.get_or_create(
            activity_part_type=content_type,
            activity_part_id=content_id,
            session=session,
            user=user,
            defaults={
                'points': points,
                'score': self.normalize_score(points),
            }
        )

        # If a pre-existing score exists, that must mean we are in the special
        # case described in the docstring, so we increase the score instead of
        # replacing it. Legacy API my ass.
        if not created:
            record.points = points
            record.score = self.normalize_score(record.points)
            record.save()


class AssessableActivityPartUserScore(UserScoreMixin, Syncable):
    """
    Django model used to keep track of the score of a user
    for a specific assessing activity part.

    Inherits from ``BaseUserScore`` -- which is defined in
    ``__init__.py`` to be able to easily share it with models in
    ``competencies.py``.

    Attributes
    ----------
    activity_part_type : django ``ForeignKey``
        Foreign key to the ``ContentType`` of the activity part.
    activity_part_id : ``django_extensions.db.fields.UUIDField``
        The primary key (uuid) of the activity part.
    activity_part : django ``GenericForeignKey``
        The generic foreign key to the activity part.
    points : django ``IntegerField``
        The absolute amount of points earned in this activity.
        This differs from the `score` field insofar as the latter is normalized
        to a percentage value.

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

    See also
    --------
    The docstring of the ``record_assessment_score`` method below, to understand
    why the ``points`` field is necessary, at least at time of this writing.

    """
    activity_part_type = models.ForeignKey(ContentType, related_name='part_scores_on')
    activity_part_id = UUIDField()
    activity_part = GenericForeignKey('activity_part_type', 'activity_part_id')

    points = models.IntegerField(null=True, blank=True)

    class Meta:
        verbose_name = "activity part score"
        verbose_name_plural = "activity part scores"
