"""This module contains the managers for the ``questionnaires`` app"""

from collections import defaultdict
from mock import patch

from django.conf import settings
from django.db.models import Sum

from polymorphic.managers import PolymorphicManager, PolymorphicQuerySet

from mtp.discrete_apps.versionable.managers import (
    PolymorphicManagerForDependency,
    PolymorphicQuerySetForDependency,
)

from mtp.apps.sync.managers import SyncableManager, SyncableQuerySet
from mtp.apps.trainings.managers import TrainingActivityManager


class QuizManager(TrainingActivityManager):
    """Manager for the ``Quiz`` model"""

    use_in_migrations = True

    def migrate_workshops_to_quiz(self):
        """Migrate the ``Workshop`` objects to their relative ``Quiz`` subclasses

        This method will only create ``Quiz`` and ``LiveQuiz`` instances,
        since the workshops models are only compatible with theses.

        """

        from mtp.apps.questionnaires.models import Questionnaire
        from mtp.apps.trainings.models import TrainingActivity
        from mtp.apps.workshops.models import Workshop

        workshop_values_list = []
        workshop_count = Workshop.objects.count()

        start = 0
        while True:
            workshops = Workshop.objects.all()[start:100+start]

            for workshop in workshops:
                workshop_values_list.append(self.get_workshop_values(workshop))

            if start > workshop_count:
                break

            start += 100

        Workshop.objects.all().delete()

        duplicable_values_list = {}

        # Migrate the workshops
        for workshop_values in workshop_values_list:
            duplicable_values_list[
                workshop_values['id']
            ] = workshop_values.pop('duplicables_values')
            self.migrate_workshop(workshop_values)

        # Set the original and parent version
        questionnaire_queryset = TrainingActivity.objects.instance_of(Questionnaire).filter(
            id__in=duplicable_values_list.keys()
        )
        for questionnaire in questionnaire_queryset:
            questionnaire.parent_version_id = duplicable_values_list[
                questionnaire.id
            ]['parent_version_id']
            questionnaire.original_version_id = duplicable_values_list[
                questionnaire.id
            ]['original_version_id']
            questionnaire.save(mtp_update=True)

    def migrate_workshop(self, workshop_values):
        """Migrate a list of given ``Workshop`` to their equivalent ``Questionnaire`` subclass

        Parameters
        ----------
        workshop_values: list(dict)
            The workshop values used to create the new ``Questionnaire``

        """

        steps_values = workshop_values.pop('questions')
        questionnaire_klass = workshop_values.pop('klass')
        del workshop_values['is_broadcastable']

        questionnaire = questionnaire_klass(**workshop_values)
        questionnaire.save(mtp_update=True)

        self.migrate_workshop_steps(questionnaire, steps_values)

    @staticmethod
    def migrate_workshop_steps(questionnaire, steps_values):
        """Migrate a list of step values to their equivalent ``Question`` subclass

        Parameters
        ----------
        questionnaire: Questionnaire
            The ``Questionnaire`` instance theses steps should be linked to
        steps_values: list(dict)
            A list of dict containing the values of each steps to migrate

        """

        from mtp.apps.questionnaires.models import Choice

        for step_values in steps_values:
            answers_values = step_values.pop('answers', [])
            user_answers_values = step_values.pop('user_answers', None)
            question_klass = step_values.pop('klass')

            question = question_klass(
                **dict(
                    questionnaire=questionnaire,
                    **step_values
                )
            )
            question.save(mtp_update=True)

            for answer_values in answers_values:
                choice = Choice(
                    **dict(
                        question=question,
                        **answer_values
                    )
                )
                choice.save(mtp_update=True)

            if user_answers_values:
                QuizManager.migrate_workshop_user_answers(user_answers_values)

    @staticmethod
    def migrate_workshop_user_answers(user_answers_values):
        """Migrate a list of user answers values to their equivalent ``Answer`` subclass

        Parameters
        ----------
        user_answers_values: list(dict)
            The list of user answers values to migrate

        """

        from mtp.apps.questionnaires.models import (
            ChoicesAnswer,
            OpenAnswer,
        )

        with patch('mtp.apps.broker_models_hooks.models._publish_to_broker'):
            if isinstance(user_answers_values, list):
                for user_answer_values in user_answers_values:
                    OpenAnswer.objects.create(**user_answer_values)
            else:
                for user_answer_values in user_answers_values.values():
                    choices = user_answer_values.pop('choices')
                    choice_answer = ChoicesAnswer.objects.create(**user_answer_values)
                    choice_answer.choices.add(*choices)

    def get_workshop_values(self, workshop):
        """Get values for a ``Workshop``

        Parameters
        ----------
        workshop: Workshop
            A Workshop instance

        Returns
        -------
        dict
            A dict containing the values of the workshop

        """

        from mtp.apps.questionnaires.models import LiveQuiz, Quiz

        steps_values = [
            self.get_step_values(step)
            for step in workshop.steps.order_by('sort_order')
        ]

        steps_values = self.merge_steps_values(steps_values)

        workshop_values = {
            'id': workshop.id,
            'name': workshop.name,
            'description': workshop.description,
            'is_deleted': workshop.is_deleted,
            'training_id': workshop.training_id,
            'klass': LiveQuiz if workshop.is_broadcastable else Quiz,
            'is_score_displayed': workshop.enable_score,
            'is_broadcastable': workshop.is_broadcastable,
            'sort_order': workshop.sort_order,
            'questions': steps_values,
            'duplicables_values': {
                'original_version_id': workshop.original_version_id,
                'parent_version_id': workshop.parent_version_id,
            }
        }

        if not workshop_values['is_broadcastable']:
            workshop_values.update({
                'is_questions_order_random': workshop.is_steps_order_random,
                'are_questions_mandatory': workshop.are_questions_mandatory,
            })

        return workshop_values

    @staticmethod
    def get_step_values(step):
        """Get values for a ``Step``

        Parameters
        ----------
        step: WorkshopStep
            The workshop step for which we want the values

        Returns
        -------
        dict
            A dict containing the values of the step

        """

        from mtp.apps.workshops.models import WorkshopStepQuestion, WorkshopStepImage
        from mtp.apps.questionnaires.models import (
            ChoicesQuestion,
            ImageChoicesQuestion,
            OpenQuestion,
        )

        step_values = {
            'id': step.id,
            'label': step.title,
            'sort_order': step.sort_order,
            'is_deleted': step.is_deleted,
            'points': 0
        }

        if isinstance(step, WorkshopStepImage):
            step_values.update({
                'klass': ImageChoicesQuestion,
                'image': step.image.name
            })

        if isinstance(step, WorkshopStepQuestion):
            # choices question
            if step.answers.count() > 0:
                step_values.update({
                    'klass': ChoicesQuestion,
                    'is_multiple_choices': step.is_multiple_choices,
                    'points': step.answers.non_deleted().filter(points__gte=0).aggregate(
                        Sum('points')
                    )['points__sum'] or 0,
                    'answers': [
                        {
                            'id': answer.id,
                            'label': answer.label,
                            'is_valid': answer.is_correct,
                            'is_deleted': answer.is_deleted,
                            'sort_order': answer.sort_order
                        }
                        for answer in step.answers.all()
                    ],
                    'user_answers': QuizManager.get_user_answer_values_for_choices_question(
                        step
                    )
                })
            # open question
            else:
                step_values.update({
                    'klass': OpenQuestion,
                    'user_answers': QuizManager.get_user_answers_values_for_open_question(step)
                })

        return step_values

    @staticmethod
    def get_user_answers_values_for_open_question(open_question):
        """Get the user answers values for a given open question

        Parameters
        ----------
        open_question: WorkshopStepQuestion
            The open question for which we want the user answers values

        Returns
        -------
        list(dict)

        """

        return [
            {
                'id': user_answer.id,
                'user_id': user_answer.user_id,
                'session_id': user_answer.training_session.id,
                'question_id': user_answer.question_id,
                'content': user_answer.text
            }
            for user_answer in open_question.user_answers.non_deleted()
        ]

    @staticmethod
    def get_user_answer_values_for_choices_question(choices_question):
        """Get the user answers values for a given open question

        Parameters
        ----------
        choices_question: WorkshopStepQuestion
            The choices question for which we want the user answers values

        Returns
        -------
        dict(dict)

        """

        grouped_answers_values = defaultdict(dict)

        user_answer_queryset = choices_question.user_answers.non_deleted()

        # On the slaves instances, we take only the locally updated answers
        if settings.INSTANCE_IS_SLAVE:
            user_answer_queryset = user_answer_queryset.filter(update_locally=True)

        for user_answer in user_answer_queryset:
            key = user_answer.user_id + user_answer.training_session.id
            if key not in grouped_answers_values.keys():
                grouped_answers_values[key] = {
                    'user_id': user_answer.user_id,
                    'session_id': user_answer.training_session.id,
                    'question_id': choices_question.id,
                    'choices': []
                }

            if user_answer.answer_id:
                grouped_answers_values[key]['choices'].append(user_answer.answer_id)

        return grouped_answers_values

    @staticmethod
    def merge_steps_values(steps_values):
        """Transform the image steps into choices step

        Parameters
        ----------
        steps_values: list(dict)
            A list of the step values for a workshop

        Returns
        -------
        list(dict)
            The updated step values

        """

        from mtp.apps.questionnaires.models import (
            ChoicesQuestion,
            ImageChoicesQuestion
        )

        for index, step in enumerate(steps_values):
            if step['klass'] == ImageChoicesQuestion:

                # If the step following the current step is a choice question
                next_index = index + 1
                if next_index < len(steps_values) and \
                        steps_values[next_index]['klass'] == ChoicesQuestion:

                    step.update(steps_values[next_index])
                    step['klass'] = ImageChoicesQuestion

                    steps_values.pop(next_index)
                else:
                    step.update({
                        'label': 'Image',
                        'is_multiple_choices': False,
                        'points': 0,
                        'answers': [
                            {
                                'id': None,
                                'label': 'Ok',
                                'is_valid': True,
                                'sort_order': 0
                            }
                        ]
                    })

        return steps_values


class DependencyQuestionQuerySet(SyncableQuerySet, PolymorphicQuerySetForDependency):
    """QuerySet for the ``Question`` model"""

    pass


class DependencyQuestionManager(SyncableManager, PolymorphicManagerForDependency):
    """Manager for the ``Question`` model"""

    pass


class DependencyChoiceQuerySet(SyncableQuerySet):
    """QuerySet for the ``Choice`` model"""

    pass


class DependencyChoiceManager(SyncableManager):
    """Manager for the ``Choice`` model"""

    pass


class AnswerQuerySet(SyncableQuerySet, PolymorphicQuerySet):
    """QuerySet for the ``Answer`` model"""

    pass


class AnswerManager(SyncableManager, PolymorphicManager):
    """Manager for the ``Answer`` model"""

    pass
