"""This module holds the forms for the ``assessments`` app"""

from django import forms
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _

from django_select2.forms import ModelSelect2MultipleWidget

from mtp.apps.accounts.forms import GroupSelectMultipleWidget
from mtp.apps.accounts.models import Group
from mtp.apps.common.utils import filter_list_of_dicts, get_dict_in_list

from .models import (
    CertificationCompetencyThreshold,
    CertificationStatus,
    Competency,
    CompetencyTree,
    Dimension,
    LessonDimensionWeight,
    LessonRecommendationThreshold,
    RecommendationLevel,
)


class DimensionSelectMultipleWidget(ModelSelect2MultipleWidget):
    """Define a Searchable multiple select widget for the ``Dimension`` instance"""

    search_fields = [
        'name__icontains',
        'code__icontains',
    ]


class AssessableMixin:
    """Mixin to improve the behavior of forms containing assessable objects."""

    def __init__(self, *args, **kwargs):
        """Ensure the form contains a properly initialized ``is_assessing`` field.

        Form creation context may be passed via ``kwargs``.
         - ``training``: instance of ``mtp.apps.trainings.Training``
         - ``activity``: instance of an ``mtp.apps.assessments.models.AssessableActivity``
                            subclass (a ``Workshop``, a ``Roleplay``, ...)
        """

        # We harvest the context from the kwargs.
        training = kwargs.pop('training', None)
        activity = kwargs.pop('activity', None)

        # We call the "real" constructor.
        super().__init__(*args, **kwargs)

        # We add the ``dimensions`` field if necessary.
        if training and activity:
            competency_tree = CompetencyTree.objects.non_deleted().filter(
                job_profile=training.job_profile
            ).first()
            self.fields['dimensions'] = forms.ModelMultipleChoiceField(
                required=True,
                queryset=Dimension.objects.non_deleted().filter(
                    competency__tree=competency_tree,
                )
            )

        # We add the ``is_assessing`` field if necessary.
        if 'is_assessing' not in self.fields:
            self.fields['is_assessing'] = forms.BooleanField(
                label=_("Assessing"),
                required=False,
            )

        assessing_field = self.fields['is_assessing']

        # Now we must determine its initial value.
        # At first, we don't know anything.
        is_assessing = None

        # If we are working on a previously existing instance...
        if self.instance and self.instance.pk:

            # ... we try to read a value in it. But this may fail, because
            # there may actually not be an ``is_assessing`` attribute!
            is_assessing = getattr(self.instance, 'is_assessing', None)

        # So, if we still don't know...
        if is_assessing is None:

            # ... we need to continue searching!
            is_assessing = False

            # A good initial value would be the "assessingness" of the parent
            # activity (ie. the quiz, roleplay, ...)
            if activity:
                is_assessing = activity.is_assessing

            # No luck. As a last resort, we use the "assessingness" of the
            # current training.
            elif training:
                is_assessing = training.is_assessing

        assessing_field.initial = is_assessing

    def clean(self):
        """Check the ``is_assessing`` field to set the mandatory state on ``dimensions``

        If the activity is not assessing, we make the dimensions field not mandatory.

        """

        cleaned_data = super().clean()

        if cleaned_data.get('is_assessing') == False and 'dimensions' in self.errors:
            del self.errors['dimensions']

        return cleaned_data


class CertificationThresholdsForm(forms.Form):
    """
    A form that allows editing all ``CertificationCompetencyThreshold`` objects
    pertaining to a given ``CertificationProgram``, as well as the ``global_threshold``
    field of all ``CertificationStatus`` objects.

    Fields are dynamically created for each (``Competency``, ``CertificationStatus``)
    couple, and values of the matching ``CertificationCompetencyThreshold``s
    are loaded and saved accordingly.

    """

    GLOBAL_COMPETENCY_ID ='global'

    program = None

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

    @cached_property
    def competencies(self):
        return Competency.objects.non_deleted().filter(tree=self.tree).all()

    @cached_property
    def statuses(self):
        return CertificationStatus.objects.non_deleted().filter(program=self.program).all()

    @cached_property
    def thresholds(self):
        return CertificationCompetencyThreshold.objects.non_deleted().filter(
            status__program=self.program
        ).values('id', 'competency_id', 'status_id', 'threshold')

    @cached_property
    def global_score_fields(self):
        """
        Return the list of fields used to manage the global thresholds.
        We need this to iterate over those fields to render the form template.

        """
        return [self['%s_%s' % (self.GLOBAL_COMPETENCY_ID, status.id)]
                for status in self.statuses]

    @cached_property
    def threshold_fields_by_competency(self):
        """
        Return the list of fields used to manage the competency thresholds.
        We need this to iterate over those fields to render the form template.
        The structure is a bit more complex than for the global score fields
        above:

        >>> [
        >>>    {
        >>>        'name': 'Foo',
        >>>        'fields': [field1, field2, ..., fieldN],
        >>>    },
        >>>    {
        >>>        'name': 'Bar',
        >>>        'fields': [field1, field2, ..., fieldN],
        >>>    },
        >>>    {
        >>>        'name': 'Qux',
        >>>        'fields': [field1, field2, ..., fieldN],
        >>>    },
        >>> ]

        """
        fields = []
        for competency in self.competencies:
            fields.append({
                'name': competency.name,
                'fields': [self['%s_%s' % (competency.id, status.id)]
                           for status in self.statuses]
            })
        return fields

    def __init__(self, *args, **kwargs):
        """Initialize the form.

        Form creation context MUST be passed via ``kwargs``, as follows:
         - ``program``: instance of ``mtp.apps.assessments.models.CertificationProgram``

        """

        # We get the certification program, and raise if none was passed.
        self.program = kwargs.pop('program')

        # We can now trigger the "real" initialization.
        super().__init__(*args, **kwargs)

        # Now, let's first create the "global score" fields.
        for status in self.statuses:

            # We build the name of the field...
            field_name = '%s_%s' % (self.GLOBAL_COMPETENCY_ID, status.id)

            # ... and we initialize it.
            self.fields[field_name] = forms.IntegerField(
                required=False, min_value=0, max_value=100,
                initial=status.global_threshold,
            )

        # Then, the "per-competency" fields.
        for competency in self.competencies:
            for status in self.statuses:

                # We build the name of the field...
                field_name = '%s_%s' % (competency.id, status.id)

                # ... we try to fetch an initial value...
                try:
                    existing_threshold = get_dict_in_list(
                        filter_list_of_dicts(self.thresholds, 'competency_id', competency.id),
                        'status_id',
                        status.id,
                    )
                    initial_value = existing_threshold.get('threshold', None)
                except (ObjectDoesNotExist, MultipleObjectsReturned):
                    initial_value = None

                # ... and we initialize the field.
                self.fields[field_name] = forms.IntegerField(
                    required=False, min_value=0, max_value=100,
                    initial=initial_value,
                )

    def save(self):
        """Save all the thresholds."""

        for key, value in self.cleaned_data.items():

            competency_id, status_id = key.split('_')

            if competency_id == self.GLOBAL_COMPETENCY_ID:

                # We want to record a global score threshold.
                # We know for sure that the database record already exists,
                # so we just need to update it.
                status = self.statuses.get(pk=status_id)
                status.global_threshold = value
                status.save()

            else:

                # We want to record a competency threshold.
                # The database record may or may not exist at that time.
                try:

                    # We try to find it among the list we previously fetched.
                    threshold_dct = get_dict_in_list(
                        filter_list_of_dicts(self.thresholds, 'competency_id', competency_id),
                        'status_id',
                        status_id,
                    )

                    # There, we have it. We can instantiate it now.
                    threshold = CertificationCompetencyThreshold.objects.get(
                        pk=threshold_dct.get('id')
                    )

                    # If the field has been left empty before submitting,
                    # that means the user wants to delete this threshold.
                    if not value:
                        threshold.delete()
                        continue

                except (ObjectDoesNotExist, MultipleObjectsReturned):

                    # If we haven't found an existing threshold, we initialize
                    # a new one.
                    threshold = CertificationCompetencyThreshold(
                        competency_id=competency_id,
                        status_id=status_id,
                    )

                # We can finally record the new value.
                if value is not None:
                    threshold.threshold = value
                    threshold.save()


class LessonDependencyForm(forms.ModelForm):

    _rel_class = None
    _rel_field_name = ''

    def __init__(self, *args, **kwargs):
        """ Hide the recommendation levels for which a threshold
        has already been defined for this lesson. """

        self.lesson = kwargs.pop('lesson', None)

        super().__init__(*args, **kwargs)

        create_mode = not kwargs.get('instance', None)

        if self.lesson:

            rel_field = self.fields[self._rel_field_name]
            rel_field.empty_label = None

            # If we are creating the relationship, we must hide the objects
            # which already have a defined relationship with the lesson.
            if create_mode:
                hidden_pks = self._get_create_hidden_rel_pks()

            # Otherwise, we must hide all the objects _except_ the one
            # whose relationship we are editing.
            else:
                instance = kwargs.get('instance')
                hidden_pks = self._get_update_hidden_rel_pks(instance)

            rel_field.queryset = self._rel_class.objects.non_deleted().exclude(
                pk__in=hidden_pks)

    def _get_create_hidden_rel_pks(self):
        return NotImplementedError

    def _get_update_hidden_rel_pks(self, instance):
        """ Return the list of the pks that should be hidden
        when updating the specified instance. """

        return list(
            self._rel_class.objects.exclude(
                pk=getattr(instance, self._rel_field_name).id
            ).values_list(
                'id', flat=True
            )
        )


class LessonRecommendationThresholdForm(LessonDependencyForm):

    _rel_class = RecommendationLevel
    _rel_field_name = 'level'

    class Meta:
        model = LessonRecommendationThreshold
        fields = ('level', 'threshold')

    def _get_create_hidden_rel_pks(self):
        """ Return the list of the pks of the recommendation levels
        that should be hidden when creating an instance. """

        return [
            threshold.level.id
            for threshold in self.lesson.recommendation_thresholds.non_deleted()
        ]


class LessonDimensionWeightForm(LessonDependencyForm):

    _rel_class = Dimension
    _rel_field_name = 'dimension'

    class Meta:
        model = LessonDimensionWeight
        fields = ('dimension', 'weight')

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

        self.tree = kwargs.pop('tree')

        super().__init__(*args, **kwargs)

        self.fields['dimension'].queryset = Dimension.objects.filter(
            competency__tree=self.tree
        )

    def _get_create_hidden_rel_pks(self):
        """ Return the list of the pks of the dimension weights
        that should be hidden when creating an instance. """

        return [
            weight.dimension.id
            for weight in self.lesson.dimension_weights.non_deleted()
        ]


class TreeDuplicateForm(forms.Form):
    """Form to duplicate a given ``CompetencyTree`` instance"""

    groups = forms.ModelMultipleChoiceField(
        widget=GroupSelectMultipleWidget(),
        queryset=Group.objects.non_deleted()
    )
