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

import logging

from django.apps import apps
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
from django.core.urlresolvers import reverse
from django.db import models
from django.utils.functional import cached_property
from django.utils.translation import ugettext, ugettext_lazy as _

from mtp.apps.accounts.constants import (
    GROUPTYPE_JOB_PROFILE
)
from mtp.apps.common.models import Orderable
from mtp.apps.common.utils import (
    bypass_orderable,
    filter_list_of_dicts,
    get_dict_in_list,
    sort_list_of_dicts,
)
from mtp.apps.sync.models import Syncable, UUIDPkModel

from mtp.discrete_apps.versionable.models import Dependency, Versionable

from ..managers import (
    CompetencyManager,
    CompetencyTreeManager,
    DimensionManager,
    DimensionUserScoreManager,
    DimensionUserScoreQuerySet,
)

from . import UserScoreMixin
from .activities import (
    AssessableActivityUserScore,
    AssessableActivityPart,
    AssessableActivityPartUserScore,
)


logger = logging.getLogger('mtp.apps.assessments')


class CompetencyTree(Versionable, Syncable):
    """
    Django model representing a competency tree, i.e. a list of
    ``Competencies``, each one containing a list of ``Dimensions``.

    Attributes
    ----------
    name : django ``CharField``
        The name of the competency tree.
    job_profile : django ``ForeignKey``
        Foreign key to the ``Group``, of type "job_profile" for which this
        competency tree is defined.

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

    """

    name = models.CharField(max_length=255)
    job_profile = models.OneToOneField(
        'accounts.Group',
        null=True,
        blank=True,
        limit_choices_to={'type__name': GROUPTYPE_JOB_PROFILE},
        on_delete=models.CASCADE
    )

    group = models.ForeignKey(
        'accounts.Group',
        null=True,
        blank=True,
        related_name='trees',
        on_delete=models.CASCADE
    )

    objects = CompetencyTreeManager()

    non_lockable_fields = Versionable.non_lockable_fields + Syncable.non_lockable_fields

    # When instantiating a competency tree,
    # we may want to set these.
    _session = None
    _trainee = None

    class Meta:
        verbose_name = _("competency tree")
        verbose_name_plural = _("competency trees")

    def __str__(self):
        return self.name

    @cached_property
    def original_job_profile(self):
        """Shortcut property to get the job profile associated with the current tree

        Returns
        -------
        ``mtp.apps.account.models.Group`` instance
            The current job profile or the original if the competency tree
            is a duplicate

        """

        return self.job_profile or self.original_version.job_profile

    def duplicate_to_groups(self, groups, dry_run=False):
        """Duplicate the current ``CompetencyTree`` instance.

        The ``CompetencyTree`` instance will be copied to all provided groups,
        who don't own an instance with the same ``original_version``.
        The new copies ``group`` attribute will be populated in the process.

        Parameters
        ----------
        dry_run : bool
            If True, we just check the number of potential duplicates,
            without actually creating the duplicates

        Returns
        -------
        list of ``Group`` instances OR list of ``CompetencyTree`` instances.
            The list of duplicates ``CompetencyTree`` instances,
            or, if dry_run=True, a list of groups, the ``group`` of the potential copy.

        """

        # Get all the child versions for this tree
        existing_duplicates = self.all_duplicates.all()

        # Get all the ``groups`` of the child versions
        owned_groups = [duplicate.group.pk for duplicate in existing_duplicates]

        # Get all the groups, minus the ``owned_groups``.
        potential_groups = [
            group
            for group in groups
            if group.pk not in owned_groups
        ]

        if dry_run:
            return potential_groups

        with bypass_orderable():
            duplicates = self.create_new_versions(
                self.VERSION_STATES.DRAFT,
                [
                    {
                        'group': group,
                        'job_profile': None
                    }
                    for group in potential_groups
                ]
            )

        return duplicates

    def get_group_version(self, group):
        """Get the duplicated version of this tree for the given group

        Parameters
        ----------
        group : ``mtp.apps.accounts.models.Group`` instance
            The group we want the tree for

        Returns
        -------
        ``CompetencyTree``
            A ``CompetencyTree`` instance or None, if the tree does not exists

        """

        return self.all_duplicates.filter(group=group).first()

    def get_duplicate_url(self):
        """Get the url to duplicate the competency tree.

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

        """
        return reverse('assessments:competency-tree-duplicate', kwargs={
            'jobprofile_uuid': self.job_profile.pk
        })

    def get_competency_list_url(self):
        """Get the url to the competency list of the current competency tree.

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

        """
        return reverse('assessments:competency-list', kwargs={
            'jobprofile_uuid': self.original_job_profile.pk
        })

    def get_detail_url(self):
        """Get the url to the detail of the current competency tree.

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

        """
        return self.get_competency_list_url()

    def get_competency_sort_url(self):
        """Get the url to sort the competency list of the current competency tree.

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

        """
        return reverse('assessments:competency-sort', kwargs={
            'jobprofile_uuid': self.job_profile.pk
        })

    def get_competency_create_url(self):
        """Get the url to to the create a new competency in the current competency tree.

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

        """
        return reverse('assessments:competency-create', kwargs={
            'jobprofile_uuid': self.job_profile.pk
        })

    def get_curriculum_url(self):
        """Get the url to list the lessons of the curriculum linked to this
        competency tree. We are actually listing the topics and their attached
        lessons, hence the perhaps counter-intuitive name.

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

        """
        return reverse('assessments:curriculum', kwargs={
            'jobprofile_uuid': self.original_job_profile.pk
        })

    def get_certification_program_url(self):
        """Get the url to manage the certification program linked to this
        competency tree.

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

        """

        return reverse('assessments:certification-program', kwargs={
            'jobprofile_uuid': self.original_job_profile.pk
        })

    def get_certification_status_create_url(self):
        """Get the url to create a new certification status for the
        certification program linked to this competency tree.

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

        """
        return reverse('assessments:certification-status-create', kwargs={
            'jobprofile_uuid': self.job_profile.pk
        })

    def get_certification_results_url(self):
        """Get the url to manage the certification results linked to this
        competency tree.

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

        """
        return reverse('assessments:certification-results-list', kwargs={
            'jobprofile_uuid': self.original_job_profile.pk
        })

    @cached_property
    def dimensions(self):
        """Get the dimensions associated with this competency tree.

        Returns
        -------
        queryset of ``mtp.apps.assessments.models.competencies.Dimension``

        """
        return Dimension.objects.filter(competency__tree=self)

    def _get_competencies_list(self):
        """Get a list of competencies associated with this competency tree.

        Returns
        -------
        list of dicts
            Each dict contains the following keys:
             - ``id``: the identifier of the competency
             - ``name``: the name of the competency

        """
        return self.competencies.non_deleted().values(
            'id', 'name'
        ).order_by(
            'sort_order'
        )

    def _get_dimensions_list(self):
        """Get a list of dimensions associated with this competency tree.

        Returns
        -------
        list of dicts
            Each dict contains the following keys:
             - ``id``: the identifier of the dimension
             - ``name``: the name of the dimension
             - ``weight``: the weight of this dimension
             - ``competency_id``: the identifier of this parent competency
                of this dimension

        """
        dimensions = self.dimensions.non_deleted()

        dimensions_list = dimensions.values('id', 'competency_id', 'name')

        # We need to know the weight of each dimension, but it is not
        # directly available as a model field, rather as a method.
        for dimension in dimensions:
            dimension_dict = get_dict_in_list(dimensions_list, 'id', dimension.id)
            dimension_dict.update({
                'weight': dimension.actual_weight(),
            })

        return dimensions_list

    def _get_activities_list(self):
        """Get a list of activities associated with this competency tree.

        Returns
        -------
        list of dicts
            Each dict contains the following keys:
             - ``id``: the identifier of the activity
             - ``name``: the name of the activity
             - ``weight``: the weight of this activity

        """
        activities_list = []

        # We fetch the list of assessable activity classes.
        from mtp.apps.trainings.models import TrainingActivity

        activities = TrainingActivity.objects.non_deleted().filter(
            training=self._session.training, is_assessing=True
        )

        for activity in activities:
            content_type = ContentType.objects.get_for_model(activity.__class__)
            activities_list.append({
                'id': activity.id,
                'type_id': content_type.id,
                'name': activity.name,
                'order': activity.sort_order,
                'weight': activity.weight,
                'max_points_yield': activity.max_points_yield,
            })

        # Finally we sort the list to make its order
        # deterministic across multiple calls.
        activities_list = sort_list_of_dicts(activities_list, 'order')

        return activities_list

    def _get_activity_parts_list(self):
        """Get a list of activity parts associated with this competency tree.

        Returns
        -------
        list of dicts
            Each dict contains the following keys:
             - ``id``: the identifier of the activity part
             - ``name``: the name of the activity part
             - ``activity_id``: the identifier of the parent activity
                of the activity part
             - ``dimension_ids``: the list of the identifiers of the dimensions
                associated with the activity part

        """
        parts_list = []

        # We fetch the list of assessable activity part classes.
        part_class_names = AssessableActivityPart.get_model_classes()

        # We iterate over the parts of each of these classes.
        for part_class_name in part_class_names:
            part_class = apps.get_model(part_class_name)
            parts = part_class.objects.filter(
                **{part_class.versionable_link[1]: self._session.training.pk}
            )

            # We consider only those that should be assessed.
            assessed_parts = [part for part in parts if part.should_be_assessed]

            for part in assessed_parts:

                # We record only these properties.
                parts_list.append({
                    'id': part.id,
                    'name': repr(part),
                    'activity_id': part.activity_id,
                    'dimension_ids': [dim.id for dim in part.dimensions.all()],
                })

        return parts_list

    def _compute_activity_scores(self, activities_list, parts_list, part_user_scores):
        """Compute the score of the trainee for each activity of this session.

        These scores are recorded as ``AssessableActivityUserScore`` instances.

        Parameters
        ----------
        activities_list : list of dicts
            The list of activities for which scores must be computed.
            Computed by the ``CompetencyTree._get_activities_list`` method.
        parts_list : list of dicts
            The list of activity parts to consider to compute the scores.
            Computed by the ``CompetencyTree._get_activity_parts_list`` method.
        parts_user_scores : list of dicts
            The list of scores earned by the trainee during the activities.
            Fetched in the ``CompetencyTree.compute_scores`` method.
            Each dict contains the following keys:
             - ``activity_part_id``: the identifier of the activity part
             - ``score``: the score for the activity part

        """
        for activity in activities_list:
            activity_id = activity.get('id')
            activity_type_id = activity.get('type_id')
            activity_order = activity.get('order')
            activity_points = 0

            # We iterate over all the parts of this activity for which the
            # trainee has participated.
            activity_parts_list = filter_list_of_dicts(parts_list, 'activity_id', activity_id)
            for activity_part in activity_parts_list:
                activity_part_id = activity_part.get('id')

                # We try to fetch a user score for this part.
                try:
                    part_score = get_dict_in_list(
                        part_user_scores, 'activity_part_id', activity_part_id
                    )
                # If there was none, or several, too bad. Let's move on.
                except (ObjectDoesNotExist, MultipleObjectsReturned):
                    continue

                # We increment the activity points by the amount of points
                # earned for this part.
                part_points = part_score.get('points', 0)
                if part_points:
                    activity_points += part_points

            # We compute the normalized percentage value...
            try:
                activity_score = round(activity_points / activity.get('max_points_yield') * 100.0)
            except ZeroDivisionError:
                continue

            # ... and finally we record the score in the database.
            AssessableActivityUserScore.objects.update_or_create(
                user=self._trainee,
                session=self._session,
                activity_id=activity_id,
                activity_type_id=activity_type_id,
                activity_order=activity_order,
                defaults={
                    'points': activity_points,
                    'score': activity_score,
                }
            )

    def _compute_dimension_scores(self, dimensions_list, activities_list,
                                  parts_list, part_user_scores):
        """Compute the score of the trainee for each dimension of this tree.

        This score is recorded as a ``DimensionUserScore`` object.

        Parameters
        ----------
        dimensions_list : list of dicts
            See ``_get_dimensions_list`` above
        activities_list : list of dicts
            See ``_get_activities_list`` above
        parts_list : list of dicts
            See ``_get_activity_parts_list`` above
        part_user_scores : list of dicts
            Each dict contains the following keys:
             - ``activity_part_id``: the identifier of the activity part
             - ``score``: the score for the activity part

        Returns
        -------
        list of dicts
            The input dimensions list, with an extra ``score`` key in each dict
            containing the computed score for this dimension.

        """

        # We compute the score for each dimension.
        for dimension in dimensions_list:
            dimension_id = dimension.get('id')

            # We iterate through the activity parts related to this dimension.
            dimension_parts = [part for part in parts_list
                               if dimension_id in part.get('dimension_ids')]

            # We can therefore isolate the corresponding activities...
            # To do so, we use a common Python trick: `list(set(iterable))`.
            # See: http://stackoverflow.com/questions/7961363/python-removing-duplicates-in-lists
            dimension_activity_ids = list(set([
                part.get('activity_id') for part in dimension_parts
            ]))
            dimension_activities = [act for act in activities_list
                                    if act.get('id') in dimension_activity_ids]

            # ... and annotate the parts and dimension with their
            # respective weights.
            for activity in dimension_activities:
                activity_id = activity.get('id')
                activity_weight = activity.get('weight')
                for part in dimension_parts:
                    if part.get('activity_id') == activity_id:
                        part.update({'weight': activity_weight})

            weighted_part_scores = []
            dimension_total_weight = 0
            for part in dimension_parts:
                try:
                    part_user_score = get_dict_in_list(
                        part_user_scores, 'activity_part_id', part.get('id')
                    )
                except (ObjectDoesNotExist, MultipleObjectsReturned):

                    # Maybe there is actually no user score for this part.
                    # Like for a qualitative roleplay criterion, for example.
                    continue

                part_score = part_user_score.get('score')
                part_weight = part.get('weight', 1)
                weighted_part_score = part_score * part_weight
                weighted_part_scores.append(weighted_part_score)
                dimension_total_weight += part_weight

            # We can now compute the score...
            try:
                dimension_score = round(sum(weighted_part_scores) / dimension_total_weight)
            except ZeroDivisionError:
                dimension_score = 0

            dimension.update({
                'score': dimension_score
            })

            # ... and record it in the database.
            DimensionUserScore.objects.update_or_create(
                user=self._trainee, session=self._session, dimension_id=dimension_id,
                defaults={'score': dimension_score},
            )

        return dimensions_list

    def _compute_competency_scores(self, competencies_list, dimensions_list):
        """Compute the score of the trainee for each competency of this tree.

        This score is recorded as a ``CompetencyUserScore`` object.

        Parameters
        ----------
        competencies_list : list of dicts
            See ``_get_competencies_list`` above
        dimensions_list : list of dicts
            See ``_get_dimensions_list`` above

        Returns
        -------
        list of dicts
            The input competencies list, with an extra ``score`` key in each
            dict containing the computed score for this competency.

        """

        # We have the score and weight for each dimension in the given list.
        # We can now compute the score for each competency.
        for competency in competencies_list:
            competency_id = competency.get('id')

            # We isolate the dimensions associated to this competency.
            competency_dimensions = filter_list_of_dicts(
                dimensions_list, 'competency_id', competency_id
            )

            competency_total_weight = 0
            dimensions_total_score = 0

            # The competency score is basically the weighted average
            # of all the scores of its dimensions.
            for dimension in competency_dimensions:
                dimension_score = dimension.get('score')
                dimension_weight = dimension.get('weight')
                try:
                    dimensions_total_score += dimension_score * dimension_weight
                    competency_total_weight += dimension_weight
                except TypeError:

                    # Will happen if a dimension has no score.
                    continue

            # We can now compute the score...
            try:
                competency_score = round(dimensions_total_score / competency_total_weight)
            except ZeroDivisionError:
                competency_score = 0

            competency.update({
                'score': competency_score
            })

            # ... and record it in the database.
            CompetencyUserScore.objects.update_or_create(
                user=self._trainee, session=self._session, competency_id=competency_id,
                defaults={'score': competency_score},
            )

        return competencies_list

    def _compute_tree_score(self, competencies_list):
        """Compute the score of the trainee for each competency of this tree.

        This score is recorded as a ``CompetencyTreeUserScore`` object.

        Parameters
        ----------
        competencies_list : list of dicts
            See ``_get_competencies_list`` above

        Returns
        -------
        int
            The computed score

        """

        # We now have the score for each competency.
        # We can now finally compute the score for the whole tree.
        competency_scores = [
            competency.get('score', 0) for competency in competencies_list
        ]
        total_competencies_score = sum(competency_scores)
        num_scoring_competencies = len(competency_scores)

        # If we haven't found any competency score, it would be meaningless
        # to compute a tree score. We need to record one though, to make this
        # case different from "the score hasn't been computed yet". We shall,
        # therefore, record a score of -1, which is what the frontend app
        # expects in that case.
        try:
            tree_score = round(
                total_competencies_score / num_scoring_competencies
            )
        except (TypeError, ZeroDivisionError):
            tree_score = -1

        # ... and record it in the database.
        CompetencyTreeUserScore.objects.update_or_create(
            user=self._trainee, session=self._session, tree=self,
            defaults={'score': tree_score},
        )

        return tree_score

    @classmethod
    def compute_scores(cls, trainee, session, force_recompute=False):
        """Compute all the scores associated with this tree, for the given
        training session undertaken by the given trainee.

        Parameters
        ----------
        trainee : instance of ``mtp.apps.accounts.models.MTPUser``
        session : instance of ``mtp.apps.trainings.models.TrainingSession``
        force_recompute : boolean
            If this is True, the scores will be computed even if they already
            were.

        Notes
        -----
        The scores are actually computed in the ``_compute_*_score`` helper
        methods above, this is mostly an entry point.

        """
        tree = CompetencyTree.objects.get_for_session(session)
        if not tree:
            raise RuntimeError("No competency tree for this training session.")

        tree._trainee = trainee

        # Was the score already computed?
        already_computed = CompetencyTreeUserScore.objects.filter(
            tree=tree, user=trainee, session=session
        ).exists()
        if already_computed and not force_recompute:
            return

        # We fetch all the necessary database records.
        competencies_list = tree._get_competencies_list()
        dimensions_list = tree._get_dimensions_list()
        activities_list = tree._get_activities_list()
        parts_list = tree._get_activity_parts_list()

        part_ids = [part.get('id') for part in parts_list]
        part_user_scores = AssessableActivityPartUserScore.objects.filter(
            user=trainee, session=session, activity_part_id__in=part_ids,
        ).values('activity_part_id', 'points', 'score')

        # And we compute the scores.
        tree._compute_activity_scores(
            activities_list, parts_list, part_user_scores,
        )
        dimensions_list = tree._compute_dimension_scores(
            dimensions_list, activities_list,
            parts_list, part_user_scores,
        )
        competencies_list = tree._compute_competency_scores(
            competencies_list, dimensions_list
        )
        tree._compute_tree_score(competencies_list)

    def _prepare_debrief_graphs(self):
        """Prepare the necessary data to draw the competencies
        and dimensions spider-graphs.

        Return
        ------
        list of dicts

        See also
        --------
        ``mtp.apps.api.views.AssessmentDebrief``
            for the description of the whole data structure

        """
        debrief_graphs = []

        def _extract_axis_data(scores):
            """
            Helper sub-function to extract the necessary data
            to build one graph.
            """
            axis_data = []
            for idx, score in enumerate(scores):
                idx += 1  # Captions should be 1-based.
                axis_data.append({
                    'name': '%d' % idx,
                    'description': '%d - %s' % (
                        idx, score.axis_name(
                            group=self._session.training.business_unit
                        )
                    ),
                    'score': score.score,
                })
            return axis_data

        # First we compute the global graph.
        competency_scores = CompetencyUserScore.objects.filter(
            user=self._trainee, session=self._session,
        ).order_by(
            'competency__sort_order'
        ).select_related(
            'competency'
        )

        debrief_graphs.append({
            'title': ugettext("Competencies map"),
            'data': _extract_axis_data(competency_scores),
        })

        # Then we compute all the competency graphs.
        competencies = [score.competency for score in competency_scores]

        for competency in competencies:

            dimension_scores = DimensionUserScore.objects.filter(
                user=self._trainee, session=self._session,
                dimension__competency=competency,
            ).order_by(
                'dimension__sort_order'
            ).select_related(
                'dimension'
            )

            debrief_graphs.append({
                'title': ugettext("Dimensions map for competency %(competency_name)s") % {
                    'competency_name': competency.name
                },
                'data': _extract_axis_data(dimension_scores)
            })

        # We're done here.
        return debrief_graphs

    def _prepare_debrief_activities(self):
        """Prepare the necessary data to describe the performance
        of this trainee during this session's activities.

        Return
        ------
        list of dicts

        See also
        --------
        ``mtp.apps.api.views.AssessmentDebrief``
            for the description of the whole data structure

        """
        debrief_activities = []
        from mtp.apps.trainings.models import TrainingActivity

        activities = TrainingActivity.objects.filter(
            training=self._session.training, is_assessing=True,
        ).non_deleted()

        debrief_activities.extend([
            activity.prepare_debrief(self._trainee, self._session)
            for activity in activities
            if activity.should_be_debriefed
        ])

        return debrief_activities

    def _prepare_debrief_suggestions(self):
        """TODO"""

        debrief_suggestions = []

        # TODO: No clear specs at the moment.

        return debrief_suggestions

    @classmethod
    def compute_debrief_data(cls, trainee, session):
        """Compute all the required data to feed the debrief API view,
        for the assessment of the given training session of the given trainee.

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

        Return
        ------
        dict
            A dict containing the following keys:
             - ``score``: the trainee's global score for this session
             - ``graphs``: the necessary data to draw the competencies and
                dimensions spider-graphs
             - ``activities``: the necessary data to describe the trainee's
                performance during this session's activities
             - ``suggestions``: the necessary data to provide improvement
                ideas to the trainee (not currently implemented)

        Raises
        ------
        RuntimeError
            If the assessment scores haven't been computed yet

        Notes
        -----
        The data is actually computed in the ``__prepare_debrief_*`` helper
        methods above, this is mostly an entry point.

        See also
        --------
        ``mtp.apps.api.views.AssessmentDebrief``
            for the description of the whole data structure

        """
        tree = CompetencyTree.objects.get_for_session(session)

        tree._trainee = trainee

        # We fetch the trainee's global score...
        try:
            CompetencyTreeUserScore.objects.get(
                tree=tree, user=trainee, session=session
            ).score
        except CompetencyTreeUserScore.DoesNotExist:
            raise RuntimeError("These assessment scores haven't been computed yet.")

        # ... and delegate most of the remaining work to helper methods.
        return {

            'score': -1,  # FIXME: We should have a choice to display
                          #         that score or not.

            'graphs': tree._prepare_debrief_graphs(),
            'activities': tree._prepare_debrief_activities(),
            'suggestions': tree._prepare_debrief_suggestions(),
        }


class CompetencyTreeUserScore(UserScoreMixin, UUIDPkModel):
    """
    Django model representing the current score of a trainee
    for a given competency tree.

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

    Attributes
    ----------
    tree : django ``ForeignKey``
        Foreign key to the ``CompetencyTree`` for which this
        score is recorded.

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

    """
    tree = models.ForeignKey(
        'CompetencyTree',
        related_name='user_scores',
        on_delete=models.CASCADE
    )

    class Meta:
        verbose_name = _("competency tree score")
        verbose_name_plural = _("competency tree scores")


class Competency(Dependency, Orderable, Syncable):
    """
    Django model representing a competency.

    Attributes
    ----------
    name : django ``CharField``
        The name of the competency.
    program : django ``ForeignKey``
        Foreign key to the ``CompetencyTree`` this competency
        belongs to.
    is_ghost : django ``BooleanField``
        True if this is a "ghost competency", only visible by the admins.

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

    """
    name = models.CharField(max_length=255)
    code = models.CharField(max_length=255)
    tree = models.ForeignKey(
        'CompetencyTree',
        related_name='competencies',
        on_delete=models.CASCADE
    )
    description = models.TextField(blank=True)

    is_ghost = models.BooleanField(default=False)

    objects = CompetencyManager()

    versionable_link = (CompetencyTree, 'tree')
    non_lockable_fields = Dependency.non_lockable_fields + Syncable.non_lockable_fields

    class Meta:
        ordering = ('sort_order',)
        verbose_name = _("competency")
        verbose_name_plural = _("competencies")
        unique_together = (
            ('name', 'tree'),
            ('code', 'tree'),
        )

    def __str__(self):
        return "%s :: %s" % (self.tree, self.name)

    @cached_property
    def job_profile(self):
        return self.tree.job_profile

    def group_version(self, group):
        """Get the group instance of the current competency

        Parameters
        ----------
        group : ``mtp.apps.accounts.models.Group`` instance
            The group we want the competency instance for

        Returns
        -------
        ``Competency``
            The ``Competency`` instance for this group

        """

        # First, get the tree version for this group
        group_tree = self.tree.get_group_version(group=group)

        # If we don't have a tree for this group, abort
        if not group_tree:
            return self

        try:
            return self.all_duplicates.get(tree=group_tree)
        except (ObjectDoesNotExist, MultipleObjectsReturned):
            # If we got no duplicate matching, abort
            return self

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

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

        """
        return reverse('assessments:competency-edit', kwargs={
            'jobprofile_uuid': self.job_profile.pk,
            'competency_uuid': self.pk
        })

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

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

        """
        return reverse('assessments:competency-delete', kwargs={
            'jobprofile_uuid': self.job_profile.pk,
            'competency_uuid': self.pk
        })

    def get_dimension_create_url(self):
        """ Get the url to create a dimension in the current competency.

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

        """
        return reverse('assessments:dimension-create', kwargs={
            'competency_uuid': self.pk
        })

    def get_dimension_sort_url(self):
        """ Get the url to sort the dimensions associated with the current competency.

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

        """
        return reverse('assessments:dimension-sort', kwargs={
            'competency_uuid': self.pk
        })


class CompetencyUserScore(UserScoreMixin, UUIDPkModel):
    """
    Django model representing the current score of a trainee
    for a given competency.

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

    Attributes
    ----------
    competency : django ``ForeignKey``
        Foreign key to the ``Competency`` for which this
        score is recorded.

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

    """
    competency = models.ForeignKey(
        'Competency',
        related_name='user_scores',
        on_delete=models.CASCADE
    )

    class Meta:
        verbose_name = _("competency score")
        verbose_name_plural = _("competency scores")

    def axis_name(self, group):
        """Get the name of the group version of the competency

        Parameters
        ----------
        group: ``mtp.apps.accounts.models.MTPUser``
            The group we want the version's name for

        Returns
        -------
        ``str``
            The name of the competency group's version

        """

        return self.competency.group_version(group=group).name

    @cached_property
    def reference_certification_threshold(self):
        """Get the reference certification threshold for this competency.

        Returns
        -------
        CertificationCompetencyThreshold
            The reference certification competency threshold.

        """

        # To avoid cyclic-import :)
        from mtp.apps.assessments.models.certifications import CertificationStatus

        program = self.competency.tree.certification_program
        reference_status = CertificationStatus.references.filter(program=program).first()
        certification_thresholds = self.competency.certification_thresholds

        if reference_status:
            return certification_thresholds.filter(status=reference_status).first()

        return None


class Dimension(Dependency, Orderable, Syncable):
    """
    Django model representing a dimension, i.e. a sub-element
    of a competency.

    Attributes
    ----------
    name : django ``CharField``
        The name of the status.
    code : django ``CharField``
        A unique code to identify this dimension. Used mostly to
        describe object relationships in import/export files.
    competency : django ``ForeignKey``
        Foreign key to the ``Competency`` this dimension
        belongs to.
    description : django ``TextField``
        A short textual description of this dimension.
        Displayed in the frontend app.
    default_weight : django ``PositiveIntegerField``
        The default weight of this dimension in its parent
        competency.

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

    """
    name = models.CharField(max_length=255)
    code = models.CharField(max_length=255)
    competency = models.ForeignKey(
        'Competency',
        related_name='dimensions',
        on_delete=models.CASCADE
    )
    description = models.TextField(blank=True)
    default_weight = models.PositiveIntegerField(default=1)

    objects = DimensionManager()

    versionable_link = (CompetencyTree, 'competency__tree')
    non_lockable_fields = Dependency.non_lockable_fields + Syncable.non_lockable_fields

    class Meta:
        ordering = ('competency__sort_order', 'sort_order')
        verbose_name = _("dimension")
        verbose_name_plural = _("dimensions")
        unique_together = (
            ('name', 'competency'),
            ('code', 'competency'),
        )

    non_duplicable_relations = [
        'training_set',  # M2M from ``Training`` to ``Dimension``
        'criterion_set',  # M2M from `Criterion` to ``Dimension``, from parent
                          # ``AssessableActivityPart``
        'workshopstepquestion_set',  # M2M from ``Criterion`` to ``Dimension``, from parent
                                     # ``AssessableActivityPart``
    ]

    # We don't want to duplicate the activity-part relation on the through table on the
    # ``dimensions`` M2M defined on ``Criterion`` and ``WorkshopStepQuestion`` (from their parent
    # ``AssessableActivityPart``
    non_duplicable_through_relations = {  # pylint: disable=invalid-name
        'criterion_set': ['criterion'],
        'workshopstepquestion_set': ['workshopstepquestion'],
    }

    def __str__(self):
        return "%s :: %s[%s]" % (self.competency, self.code, self.name)

    def group_version(self, group):
        """Get the group instance of the current dimension

        Parameters
        ----------
        group : ``mtp.apps.accounts.models.Group`` instance
            The group we want the dimension instance for

        Returns
        -------
        ``Dimension``
            The ``Dimension`` for this group

        """

        # First, get the tree version for this group
        group_tree = self.competency.tree.get_group_version(group=group)

        # If we don't have a tree for this group, abort
        if not group_tree:
            return self

        try:
            return self.all_duplicates.get(competency__tree=group_tree)
        except (ObjectDoesNotExist, MultipleObjectsReturned):
            # If we got no duplicate matching, abort
            return self

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

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

        """
        return reverse('assessments:dimension-edit', kwargs={
            'competency_uuid': self.competency.pk,
            'dimension_uuid': self.pk
        })

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

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

        """
        return reverse('assessments:dimension-delete', kwargs={
            'competency_uuid': self.competency.pk,
            'dimension_uuid': self.pk
        })

    def actual_weight(self, training=None):
        """Get the weight that should actually be used to compute the scores
        for this dimension, since the default weight recorded in a ``Dimension``
        instance can be overriden in a ``DimensionTrainingWeight`` object for
        a specific training.

        Parameters
        ----------
        training : ``mtp.apps.trainings.Training`` instance

        Returns
        -------
        int
            The weight that should actually be used

        """
        try:
            return DimensionTrainingWeight.objects.filter(
                dimension=self, training=training)[0].weight
        except IndexError:
            return self.default_weight


class DimensionUserScore(UserScoreMixin, UUIDPkModel):
    """
    Django model representing the current score of a trainee
    for a given dimension.

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

    Attributes
    ----------
    dimension : django ``ForeignKey``
        Foreign key to the ``Dimension`` for which this
        score is recorded.

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

    """
    dimension = models.ForeignKey(
        'Dimension',
        related_name='user_scores',
        on_delete=models.CASCADE
    )

    objects = DimensionUserScoreManager.from_queryset(DimensionUserScoreQuerySet)()

    class Meta:
        verbose_name = _("dimension score")
        verbose_name_plural = _("dimension scores")

    def axis_name(self, group):
        """Get the name of the group version of the dimension

        Parameters
        ----------
        group: ``mtp.apps.accounts.models.MTPUser``
            The group we want the dimension version's name for

        Returns
        -------
        str: The name of the dimension group's version

        """

        return self.dimension.group_version(group=group).name


class DimensionTrainingWeight(Syncable):
    """
    Django model allowing to override the default weight of
    a dimension with regards to a specific training.

    Attributes
    ----------
    dimension : django ``ForeignKey``
        Foreign key to the ``Dimension`` of which
        this weight is overriden.
    training : django ``ForeignKey``
        Foreign key to the ``trainings.Training`` for which
        this weight is overriden.
    weight : django ``PositiveIntegerField``
        The overriden weight.

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

    """
    dimension = models.ForeignKey('Dimension', on_delete=models.CASCADE)
    training = models.ForeignKey('trainings.Training', on_delete=models.CASCADE)
    weight = models.PositiveIntegerField(null=True, blank=True)

    class Meta:
        verbose_name = _("dimension training weight")
        verbose_name_plural = _("dimension training weights")
