"""This modules holds certifications models for the ``assessments`` app."""

from django.conf import settings
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
from django.core.urlresolvers import reverse
from django.db import models
from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _

from mtp.apps.assessments.models.competencies import (
    CompetencyTreeUserScore,
    CompetencyUserScore,
)
from mtp.apps.common.managers import ReferenceableSyncableManager
from mtp.apps.common.models import Referenceable
from mtp.apps.common.utils import (
    filter_list_of_dicts,
    get_dict_in_list,
)
from mtp.apps.sync.models import Syncable
from ..managers import (
    CertificationCompetencyThresholdManager,
    CertificationCompetencyThresholdQuerySet,
    CertificationProgramManager,
    CertificationProgramQuerySet,
    CertificationProgramUserStatusManager,
    CertificationProgramUserStatusQuerySet,
    CertificationStatusManager,
    CertificationStatusQuerySet,
)


class CertificationProgram(Syncable):
    """
    Django model representing a certification program.

    Attributes
    ----------
    name : django ``CharField``
        The name of the program.

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

    """
    name = models.CharField(max_length=255)
    tree = models.OneToOneField('assessments.CompetencyTree',
        related_name='certification_program')

    objects = CertificationProgramManager.from_queryset(
        CertificationProgramQuerySet)()

    class Meta:
        ordering = ('name',)
        verbose_name = _("certification program")
        verbose_name_plural = _("certification programs")

    def __str__(self):
        return self.name

    @cached_property
    def reference_status(self):
        """Get the reference status of this program.

        Returns
        -------
        CertificationStatus
            The status which acts as a reference.

        """

        return CertificationStatus.references.filter(program=self).first()

    def compute_trainee_status(self, trainee, session):
        """Compute the certification status reached by the specified trainee
        during the specified training session.

        Parameters
        ----------
        trainee : instance of ``mtp.apps.accounts.models.MTPUser``
        session : instance of ``mtp.apps.trainings.models.TrainingSession``

        Returns
        -------
        instance of ``mtp.apps.assessments.models.CertificationProgramUserStatus``
            A record of the status that was reached by the trainee.
        or None, if the trainee failed in reaching any status.

        Raises
        ------
        RuntimeError
            If no global score record can be found for this (trainee, session).

        """

        # We fetch the trainee's global score...
        try:
            tree_score = CompetencyTreeUserScore.objects.get(
                tree=self.tree, user=trainee, session=session
            )
        except (CompetencyTreeUserScore.DoesNotExist,
                CompetencyTreeUserScore.MultipleObjectsReturned):

            # No global score, we cannot continue.
            raise RuntimeError("No global score found for this trainee/session")

        trainee_global_score = tree_score.score

        # ... and check if it is sufficient to earn them
        # even the lowest status.
        all_statuses = self.statuses.order_by('-global_threshold')
        if not all_statuses.exists():
            return None

        lowest_status = all_statuses.last()
        if trainee_global_score < lowest_status.global_threshold:
            return None

        # OK, this trainee is not a complete failure.
        # Let's see which statuses they might earn,
        # based on their global score.
        potential_statuses = [
            status
            for status in all_statuses
            if trainee_global_score >= status.global_threshold
        ]

        # We are going to need thresholds and scores now.
        # Let's fetch everything at once.
        all_competency_thresholds = CertificationCompetencyThreshold.objects.filter(
            competency__tree=self.tree, status__program=self,
        ).order_by(
            'status', 'threshold'
        ).values('competency__id', 'status__id', 'threshold')

        all_trainee_competency_scores = CompetencyUserScore.objects.filter(
            user=trainee, session=session,
        ).values('competency__id', 'score')

        # We can now iterate through all potentially earnable statuses.
        # Remember, they are ordered by descending global score, so the
        # first one to match all the criteria will be the good one!
        for status in potential_statuses:

            validated = True

            status_id = status.id
            status_competency_thresholds = filter_list_of_dicts(
                all_competency_thresholds, 'status__id', status_id
            )

            for competency_threshold in status_competency_thresholds:
                competency_id = competency_threshold.get('competency__id')
                threshold = competency_threshold.get('threshold')

                # No score for this competency: FAIL.
                try:
                    trainee_score = get_dict_in_list(
                        all_trainee_competency_scores, 'competency__id', competency_id
                    )
                except (ObjectDoesNotExist, MultipleObjectsReturned):
                    validated = False
                    break

                # Insufficient score for this competency: FAIL.
                score = trainee_score.get('score')
                if score < threshold:
                    validated = False
                    break

            # This status has not been validated, let's try the next one.
            if not validated:
                continue

            # Yay! We have a winner!
            try:
                # If the certification already exists
                certification = CertificationProgramUserStatus.objects.get(
                    program=self, user=trainee, session=session
                )

                certification.score = trainee_global_score
                if not certification.is_forced:
                    certification.status = status

                certification.save()
            except CertificationProgramUserStatus.DoesNotExist:
                certification = CertificationProgramUserStatus.objects.create(
                    program=self, user=trainee, session=session,
                    status=status, score=trainee_global_score
                )

            return certification

        # Uh-oh. Looks like the trainee is, in fact, a complete failure.
        return None


class CertificationStatus(Referenceable, Syncable):
    """Django model representing a certification status for a given certification program.

    Attributes
    ----------
    name : django ``CharField``
        The name of the status.
    program : django ``ForeignKey``
        Foreign key to the ``CertificationProgram`` for which this
        status is valid.
    global_threshold : django ``PositiveIntegerField``.
        The global score to reach to acquire this status.

    Notes
    -----
    This model is ``Referenceable`` and ``Syncable``.
    The status marked as reference will be use to compare user scores with threshold related to it.
    The reference status is the "average" to be obtained to have the diploma.
    The status above that of reference are as optionals mentions.

    """

    name = models.CharField(max_length=255)
    program = models.ForeignKey('CertificationProgram', related_name='statuses')
    global_threshold = models.PositiveIntegerField(
        default=0,
        help_text=_("The minimal global score required to earn this status.")
    )

    objects = CertificationStatusManager.from_queryset(
        CertificationStatusQuerySet)()
    references = ReferenceableSyncableManager()

    class Meta:
        ordering = ('global_threshold',)
        verbose_name = "certification status"
        verbose_name_plural = "certification statuses"

    def __str__(self):
        return self.name

    def get_reference_dependencies(self, **kwargs):
        """Get the reference dependencies.

        Notes
        -----
        We have only one reference status by program.

        """

        return super().get_reference_dependencies(program=self.program)

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

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

        """

        return reverse('assessments:certification-status-edit', kwargs={
            'jobprofile_uuid': self.program.tree.job_profile_id,
            'certificationstatus_uuid': self.pk,
        })

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

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

        """

        return reverse('assessments:certification-status-delete', kwargs={
            'jobprofile_uuid': self.program.tree.job_profile_id,
            'certificationstatus_uuid': self.pk,
        })

    def get_reference_url(self):
        """Get the url to mark the current certification status as reference.

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

        """

        return reverse('assessments:certification-status-reference', kwargs={
            'jobprofile_uuid': self.program.tree.job_profile_id,
            'certificationstatus_uuid': self.pk,
        })


class CertificationCompetencyThreshold(Syncable):
    """
    Django model representing a score threshold for a given
    competency to reach a given certification status.

    Attributes
    ----------
    competency : django ``ForeignKey``
        Foreign key to the ``Competency`` for which this
        threshold is defined.
    status : django ``ForeignKey``
        Foreign key to the ``CertificationStatus`` for which this
        threshold is defined.
    threshold : django ``PositiveIntegerField``
        The score to reach, in this competency, to acquire this status.

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

    """
    competency = models.ForeignKey(
        'assessments.Competency',
        related_name='certification_thresholds'
    )
    status = models.ForeignKey(
        'CertificationStatus',
        related_name='competency_thresholds'
    )
    threshold = models.PositiveIntegerField(
        default=0,
        help_text=_("The minimal competency score required to earn this status.")
    )

    objects = CertificationCompetencyThresholdManager.from_queryset(
        CertificationCompetencyThresholdQuerySet)()

    class Meta:
        ordering = ('status__program', 'threshold',)
        verbose_name = "certification competency threshold"
        verbose_name_plural = "certification competency thresholds"

    def __str__(self):
        return "%s :: %s" % (self.competency, self.status)


class CertificationProgramUserStatus(Syncable):
    """
    Django model representing a trainee's current status
    for a given certification program.

    Attributes
    ----------
    user : django ``ForeignKey``
        Foreign key to the ``MTPUser`` for which this
        status is recorded.
    program : django ``ForeignKey``
        Foreign key to the ``CertificationProgram`` for which this
        status is recorded.
    status : django ``ForeignKey``
        Foreign key to the actual ``CertificationStatus`` the user has reached.
    session : django ``ForeignKey`` to ``TrainingSession``
        The session in which this certification was obtained.
    date_reached : django ``DateTimeField``
        The date/time the user reached this status.
        Basically the date/time this instance was created.
    is_validated : django ``BooleanField``
        Indicates if this status was validated by the relevant authority.
    is_forced : django ``BooleanField``
        Indicates if this status was forced by the relevant authority.
    score : django ``PositiveIntegerField``
        The user global score for this program competency tree.

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

    The ``program`` attribute is not strictly necessary from a data
    modelling point of view, since the ``CertificationStatus`` already
    contains a ``ForeignKey`` towards the ``CertificationProgram``.
    However, it was necessary to add it to be able to display this
    information in an ``InlineModelAdmin``.

    See: https://docs.djangoproject.com/en/1.8/ref/contrib/admin/#django.contrib.admin.ModelAdmin.fields

    """
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        related_name='certification_statuses'
    )
    program = models.ForeignKey(
        'CertificationProgram', null=True, blank=True,
        related_name='user_statuses'
    )
    status = models.ForeignKey(
        'CertificationStatus', null=True, blank=True,
        related_name='user_certifications'
    )
    session = models.ForeignKey(
        'trainings.TrainingSession', null=True,
        related_name='user_certifications'
    )

    score = models.PositiveIntegerField()
    date_reached = models.DateTimeField(default=timezone.now)
    is_validated = models.BooleanField(default=False)
    is_forced = models.BooleanField(default=False)

    objects = CertificationProgramUserStatusManager.from_queryset(
        CertificationProgramUserStatusQuerySet)()

    class Meta:
        ordering = (
            'program__tree__job_profile__name',
            'user__first_name',
            'user__last_name',
        )
        verbose_name = "certification program status"
        verbose_name_plural = "certification program statuses"

    def __str__(self):
        return "%s :: %s" % (self.status, self.user.get_full_name())

    @cached_property
    def business_unit(self):
        """Get the current result business unit.

        Notes
        -----
        The business_unit comes from the related session training.

        Returns
        -------
        Group

        """

        return self.session.training.business_unit

    def get_detail_url(self):
        """Get the url to view the current result in detail.

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

        """

        return reverse('assessments:certification-result-detail', kwargs={
            'jobprofile_uuid': self.program.tree.job_profile.pk,
            'certificationprogramuserstatus_uuid': self.pk,
        })

    def get_update_url(self):
        """Get the url to update the current result.

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

        """

        return reverse('assessments:certification-result-update', kwargs={
            'jobprofile_uuid': self.program.tree.job_profile.pk,
            'certificationprogramuserstatus_uuid': self.pk,
        })

    def get_validate_url(self):
        """Get the url to validate a certification

        Returns
        -------
        string

        """

        return reverse('assessments:certification-result-validate', kwargs={
            'jobprofile_uuid': self.program.tree.job_profile.pk,
            'certificationprogramuserstatus_uuid': self.pk,
        })

    def get_pdf_url(self):
        """Get the url to download the pdf report.

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

        """

        return reverse('assessments:certification-result-pdf', kwargs={
            'jobprofile_uuid': self.program.tree.job_profile.pk,
            'certificationprogramuserstatus_uuid': self.pk,
        })
