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

import pandas as pd

from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
from django.db import models
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _

from django_extensions.db.fields import UUIDField
from polymorphic.models import PolymorphicModel

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

from mtp.apps.broker_models_hooks.models import BrokerHookMixin, BrokerHookM2MFromMixin
from mtp.apps.common.models import Orderable, NonAbstractChildOrderable
from mtp.apps.common.utils import upload_to_generator
from mtp.apps.data_export.constants import EXCEL_STYLE_CHOICES
from mtp.apps.sync.models import Syncable
from mtp.apps.trainings.models import (
    Training,
    TrainingActivity,
    TrainingSession,
)

from .data_exporters import QuestionnaireResultsToExcel
from .managers import (
    AnswerManager,
    AnswerQuerySet,
    DependencyChoiceManager,
    DependencyChoiceQuerySet,
    DependencyQuestionManager,
    DependencyQuestionQuerySet,
    QuizManager
)


class Questionnaire(QuestionnaireResultsToExcel, TrainingActivity):
    """Django abstract model defining the base fields for the ``Questionnaire`` subclasses"""

    questions = GenericRelation(
        'Question',
        content_type_field='questionnaire_content_type',
        object_id_field='questionnaire_id',
    )

    class Meta:
        abstract = True

    @cached_property
    def questions_type_and_create_url(self):
        """Get the verbose question type and their related create url

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

        """

        question_types = [
            {
                'klass': cls.__name__.lower(),
                'create_url': cls.get_create_url(questionnaire=self),
                'verbose_type': cls.verbose_question_type
            }
            for cls in self.available_question_types()
        ]

        return sorted(question_types, key=lambda k: k['verbose_type'])

    def available_question_types(self):
        """Get the available question type for this ``Questionnaire``

        Returns
        -------
        list(Model)

        """

        return Question.__subclasses__()

    @cached_property
    def is_scoring(self):
        """Get the "scoring" status of the questionnaire

        A ``Questionnaire`` is considered as a "scoring" one, if it's a subclass
        of the ``WithScoreMixin`` mixin.

        Returns
        -------
        bool
            True if the current questionnaire is a "scoring" one,
            False otherwise

        """

        return isinstance(self, WithScoreMixin)

    @cached_property
    def is_live(self):
        """Get the "live" status of the questionnaire

        A ``Questionnaire`` is considered as a "live" one, if it's an instance of
        ``LivePolling`` or ``LiveQuiz``.

        Returns
        -------
        bool
            True if the current questionnaire is a "live" one,
            False otherwise

        """

        return isinstance(self, (LivePolling, LiveQuiz,))

    def get_questions_sort_url(self):
        """ Get the url to sort the questions of current questionnaire.

        Returns
        -------
        string

        """

        return reverse('questionnaires:questions-sort', kwargs={'questionnaire_uuid': self.pk})

    def get_questions_delete_url(self):
        """Get the url to delete the questions of this questionnaire.

        Returns
        -------
        str

        """

        return reverse('questionnaires:questions-delete', kwargs={'questionnaire_uuid': self.pk})


class WithScoreMixin(models.Model):
    """Abstract model for the questionnaires with scores

    Attributes
    ----------
    is_score_displayed: django ``BooleanField``, default to False
        If True, we will show the obtained score to the user after completing the questionnaire

    """

    is_score_displayed = models.BooleanField(
        default=False,
        verbose_name=_('Display score'),
        help_text=_('Display the score at the end of the questionnaire')
    )

    class Meta:
        abstract = True

    def get_trainee_session_score(self, user, session):
        """Get the score of this questionnaire for the given session and trainee

        The score of the questionnaire is the sum of all the points the trainee got
        from the questionnaire ``ChoicesQuestion``.

        Parameters
        ----------
        user: MTPUser
            The user for which we want the score
        session: TrainingSession
            The session for which we want the score

        Returns
        -------
        int
            The trainee's score for this workshop

        """

        return sum([
            question.get_trainee_session_score(user, session)
            for question in self.questions.non_deleted().instance_of(ChoicesQuestionMixin)
        ])


class MaybeMandatoryQuestionsMixin(models.Model):
    """Abstract model for the questionnaires with maybe mandatory questions

    Attributes
    ----------
    are_questions_mandatory: django ``BooleanField``, default to False
        If True, the user will have to answer all the questions in this questionnaire

    """

    are_questions_mandatory = models.BooleanField(
        default=False,
        verbose_name=_('Questions are mandatory'),
        help_text=_('Does the user have to answer all the questions ?')
    )

    class Meta:
        abstract = True


class Quiz(Questionnaire, WithScoreMixin, MaybeMandatoryQuestionsMixin):
    """Django model representing a quiz

    Attributes
    ----------
    is_questions_order_random: django ``BooleanField``, default to False
        If True, the ordering of the questions linked to this quiz will be random

    """

    is_questions_order_random = models.BooleanField(
        default=False,
        verbose_name=_('Randomize the questions order')
    )

    verbose_activity_name = _('Quiz')
    objects = QuizManager()

    @property
    def serializer_class(self):
        """Return the serializer to use for this model"""

        from .apiv2.serializers import QuizSerializer

        return QuizSerializer

    def get_absolute_url(self):
        """Get the url of the current quiz.

        Returns
        -------
        str

        """

        return self.get_detail_url()

    def get_detail_url(self):
        """Get the url to view the current quiz.

        Returns
        -------
        str

        """

        return reverse('questionnaires:quiz-detail', kwargs={'quiz_uuid': self.pk})

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

        Returns
        -------
        str

        """

        return reverse('questionnaires:quiz-update', kwargs={'quiz_uuid': self.pk})

    @classmethod
    def get_create_url(cls):
        """Get the url to create a new quiz

        Returns
        -------
        str

        """

        return reverse('questionnaires:quiz-create')

    @classmethod
    def get_training_create_url(cls, training):
        """Get the url to create a new quiz for the provided training

        Parameters
        ----------
        training: Training
            The training for which we want to create a new quiz.

        Returns
        -------
        str

        """

        return reverse('trainings:quiz-create', kwargs={'training_uuid': training.pk})


class LiveQuiz(Questionnaire, WithScoreMixin):
    """Django model representing a live quiz"""

    verbose_activity_name = _('Live quiz')

    def available_question_types(self):
        """Get the available question type for this ``Questionnaire``

        Returns
        -------
        list(Model)

        """

        return [
            question_type for question_type in super().available_question_types()
            if question_type is not OpenQuestion
        ]

    @property
    def serializer_class(self):
        """Return the serializer to use for this model"""

        from .apiv2.serializers import LiveQuizSerializer

        return LiveQuizSerializer

    def get_absolute_url(self):
        """Get the url of the current live quiz.

        Returns
        -------
        str

        """

        return self.get_detail_url()

    def get_detail_url(self):
        """Get the url to view the current live quiz.

        Returns
        -------
        str

        """

        return reverse('questionnaires:livequiz-detail', kwargs={'livequiz_uuid': self.pk})

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

        Returns
        -------
        str

        """

        return reverse('questionnaires:livequiz-update', kwargs={'livequiz_uuid': self.pk})

    @classmethod
    def get_create_url(cls):
        """Get the url to create a new live quiz

        Returns
        -------
        str

        """

        return reverse('questionnaires:livequiz-create')

    @classmethod
    def get_training_create_url(cls, training):
        """Get the url to create a new live quiz for the provided training

        Parameters
        ----------
        training: Training
            The training for which we want to create a new live quiz.

        Returns
        -------
        str

        """

        return reverse('trainings:livequiz-create', kwargs={'training_uuid': training.pk})


class Polling(Questionnaire, MaybeMandatoryQuestionsMixin):
    """Django model representing a polling"""

    verbose_activity_name = _('Polling')

    @property
    def serializer_class(self):
        """Return the serializer to use for this model"""

        from .apiv2.serializers import PollingSerializer

        return PollingSerializer

    def get_absolute_url(self):
        """Get the url of the current polling.

        Returns
        -------
        str

        """

        return self.get_detail_url()

    def get_detail_url(self):
        """Get the url to view the current polling.

        Returns
        -------
        str

        """

        return reverse('questionnaires:polling-detail', kwargs={'polling_uuid': self.pk})

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

        Returns
        -------
        str

        """

        return reverse('questionnaires:polling-update', kwargs={'polling_uuid': self.pk})

    @classmethod
    def get_create_url(cls):
        """Get the url to create a new polling

        Returns
        -------
        str

        """

        return reverse('questionnaires:polling-create')

    @classmethod
    def get_training_create_url(cls, training):
        """Get the url to create a new polling for the provided training

        Parameters
        ----------
        training: Training
            The training for which we want to create a new polling

        Returns
        -------
        str

        """

        return reverse('trainings:polling-create', kwargs={'training_uuid': training.pk})


class LivePolling(Questionnaire):
    """Django model representing a live polling"""

    verbose_activity_name = _('Live polling')

    @property
    def serializer_class(self):
        """Return the serializer to use for this model"""

        from .apiv2.serializers import LivePollingSerializer

        return LivePollingSerializer

    def available_question_types(self):
        """Get the available question type for this ``Questionnaire``

        Returns
        -------
        list(Model)

        """

        return [
            question_type for question_type in super().available_question_types()
            if question_type is not OpenQuestion
        ]

    def get_absolute_url(self):
        """Get the url of the current live polling.

        Returns
        -------
        str

        """

        return self.get_detail_url()

    def get_detail_url(self):
        """Get the url to view the live polling.

        Returns
        -------
        str

        """

        return reverse('questionnaires:livepolling-detail', kwargs={'livepolling_uuid': self.pk})

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

        Returns
        -------
        str

        """

        return reverse('questionnaires:livepolling-update', kwargs={'livepolling_uuid': self.pk})

    @classmethod
    def get_create_url(cls):
        """Get the url to create a new live polling

        Returns
        -------
        str

        """

        return reverse('questionnaires:livepolling-create')

    @classmethod
    def get_training_create_url(cls, training):
        """Get the url to create a new live polling for the provided training

        Parameters
        ----------
        training: Training
            The training for which we want to create a new live polling.

        Returns
        -------
        str

        """

        return reverse('trainings:livepolling-create', kwargs={'training_uuid': training.pk})


class Question(Syncable, Dependency, PolymorphicModelForDependency, NonAbstractChildOrderable):
    """Django model representing a question

    Attributes
    ----------
    label: django ``CharField``
        The label of this question
    points: django ``PositiveSmallIntegerField``
        The number of points this question can give
    questionnaire: django ``ForeignKey`` to ``QuestionnaireActivity``
        The questionnaire this question is linked to
    choices: django ``GenericRelation`` to ``Choice``
        The available choices for this question

    """

    label = models.CharField(
        max_length=280,
        verbose_name=_('Label')
    )
    points = models.PositiveSmallIntegerField(
        default=1,
        verbose_name=_('Points')
    )

    questionnaire_content_type = models.ForeignKey(
        ContentType,
        on_delete=models.CASCADE
    )
    questionnaire_id = UUIDField()
    questionnaire = GenericForeignKey(
        'questionnaire_content_type',
        'questionnaire_id'
    )

    choices = GenericRelation(
        'Choice',
        content_type_field='question_content_type',
        object_id_field='question_id',
    )

    objects = DependencyQuestionManager.from_queryset(DependencyQuestionQuerySet)()

    versionable_link = (Training, 'questionnaire__training')
    non_lockable_fields = Dependency.non_lockable_fields + Syncable.non_lockable_fields

    class Meta:
        unique_together = ('questionnaire_id', 'questionnaire_content_type', 'sort_order',)
        ordering = ['sort_order']

    def get_unique_fields(self):
        """Overwrite the ``get_unique_fields`` of ``Orderable``

        ``Orderable`` add an "_id" suffix to each field, and we don't want that here

        Parameters
        ----------
        For the parameters, see ``Orderable``

        """

        return ['questionnaire_id', 'questionnaire_content_type']


class ChoicesQuestionMixin(models.Model):
    """A mixin to handle the answer on a ``Question`` subclasses with choices

    Attributes
    ----------
    is_multiple_choices: django ``BooleanField`` field
        If True, the user could select multiple choices for this question

    """

    is_multiple_choices = models.BooleanField(
        default=False,
        verbose_name=_('Multiple answers available'),
        help_text=_('The user can select multiple answers for this question')
    )

    question_type = 'choices'

    class Meta:
        abstract = True

    def create_or_update_user_answer(self, session, user, choices):
        """Create or update a ``ChoicesAnswer`` for this question

        Parameters
        ----------
        session: TrainingSession
            The training session we want to record the user answer into
        user: MTPUser
            The user who created this answer
        choices: list(str)
            The choices chosen by the user

        Returns
        -------
        ChoicesAnswer

        """

        answer, created = ChoicesAnswer.objects.get_or_create(
            question=self,
            session=session,
            user=user,
        )

        if not created:
            answer.choices.clear()

        answer.choices.add(*choices)

        answer.save()

        return answer

    def get_trainee_session_score(self, user, session):
        """Get the score for this question for the given session and trainee

        Parameters
        ----------
        user: MTPUser
            The user for which we want the score
        session: TrainingSession
            The training session in which the user participates.

        Returns
        -------
        int
            The score obtained on this question by the provided user

        """

        try:
            trainee_choices_ids = ChoicesAnswer.objects.get(
                user=user,
                session=session,
                question=self
            ).choices.values_list('id', flat=True)
        except ObjectDoesNotExist:
            return 0

        valid_choice_ids = self.choices.non_deleted().filter(is_valid=True).values_list(
            'id',
            flat=True
        )

        # The given user got all (and only) the valid answers for this question
        if set(trainee_choices_ids) == set(valid_choice_ids):
            return self.points
        else:
            return 0

    def get_results_excel_data(self, session):
        """Get the session users answers for this question

        Parameters
        ----------
        session: TrainingSession
            The training session we want to get the results from

        Returns
        -------
        list((panda.Series, styles,))
            A list of tuple with two entries:
            - A pandas ``Series`` containing the details of the each users answers
            - One of the ``EXCEL_STYLE_CHOICES`` choice. The style to apply to this column.

        """

        results_data = list()

        for choice in self.choices.non_deleted():

            if choice.is_valid:
                choice_style = EXCEL_STYLE_CHOICES.WORKSHOP_STEP_ANSWER_VALID
            else:
                choice_style = EXCEL_STYLE_CHOICES.DEFAULT

            results_data.append(
                (
                    pd.Series(
                        [
                            'x' if choice.is_selected(user, session) else ''
                            for user in session.users.non_deleted()
                        ],
                        name=choice.label
                    ),
                    choice_style
                )
            )

        return results_data


class ChoicesQuestion(Question, ChoicesQuestionMixin):
    """Django model representing a choice question"""

    verbose_question_type = _('Choices question')

    @property
    def serializer_class(self):
        """Get the serializer class to use for this model

        This is a property to avoid recursive import problem.

        """

        from .apiv2.serializers import ChoicesQuestionSerializer

        return ChoicesQuestionSerializer

    @classmethod
    def get_create_url(cls, questionnaire):
        """Get the url to create a new choices question.

        Parameters
        ----------
        questionnaire: Questionnaire
            The questionnaire for which we want to create a new choices question.

        Returns
        -------
        str

        """

        return reverse('questionnaires:choicesquestion-create', kwargs={
            'questionnaire_uuid': questionnaire.pk
        })

    def get_absolute_url(self):
        """Get the url of the current choices question.

        Returns
        -------
        str

        """

        return self.get_edit_url()

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

        Returns
        -------
        str

        """

        return reverse('questionnaires:choicesquestion-update', kwargs={
            'questionnaire_uuid': self.questionnaire.pk,
            'question_uuid': self.pk
        })


class OpenQuestion(Question):
    """Django model representing an open question"""

    verbose_question_type = _('Open question')

    question_type = 'open'

    @property
    def serializer_class(self):
        """Get the serializer class to use for this model

        This is a property to avoid recursive import problem.

        """

        from .apiv2.serializers import OpenQuestionSerializer

        return OpenQuestionSerializer

    @classmethod
    def get_create_url(cls, questionnaire):
        """Get the url to create a new choices question.

        Parameters
        ----------
        questionnaire: Questionnaire
            The questionnaire for which we want to create a new choices question.

        Returns
        -------
        str

        """

        return reverse('questionnaires:openquestion-create', kwargs={
            'questionnaire_uuid': questionnaire.pk
        })

    def get_absolute_url(self):
        """Get the url of the current image choices question.

        Returns
        -------
        str

        """

        return self.get_edit_url()

    def get_edit_url(self):
        """Get the url to edit the current image choices question.

        Returns
        -------
        str

        """

        return reverse('questionnaires:openquestion-update', kwargs={
            'questionnaire_uuid': self.questionnaire.pk,
            'question_uuid': self.pk
        })

    def create_or_update_user_answer(self, session, user, content):
        """Create or update a ``OpenAnswer`` for this question

        Parameters
        ----------
        session: TrainingSession
            The training session we want to record the user answer into
        user: MTPUser
            The user who created this answer
        content: str
            The content of the user answer

        Returns
        -------
        OpenAnswer

        """

        answer, __ = OpenAnswer.objects.update_or_create(
            question=self,
            session=session,
            user=user,
            defaults={
                'content': content
            }
        )

        return answer

    def get_trainee_session_score(self, user, session):  # pylint: disable=unused-argument
        """Get the score for this question for the given session and trainee

        Parameters
        ----------
        user: MTPUser
            The user for which we want the score
        session: TrainingSession
            The training session in which the user participates.

        Returns
        -------
        int
            Always 0 !

        Notes
        -----
        This method is just here for the consistency as there is no score on the ``OpenQuestion``.

        """

        return 0

    def get_results_excel_data(self, session):
        """Get the session users answers for this question

        Parameters
        ----------
        session: TrainingSession
            The training session we want to get the results from

        Returns
        -------
        list((panda.Series, styles,))
            A list of tuple with two entries:
            - A pandas ``Series`` containing the details of the each users answers
            - One of the ``EXCEL_STYLE_CHOICES`` choice. The style to apply to this column.

        """

        trainee_answers = []

        for user in session.users.non_deleted():
            try:
                answer_content = OpenAnswer.objects.get(
                    question=self,
                    session=session,
                    user=user
                ).content
            except (ObjectDoesNotExist, MultipleObjectsReturned):
                answer_content = ''

            trainee_answers.append(answer_content)

        return [
            (
                pd.Series(trainee_answers, name=str(_('Verbatim')),),
                EXCEL_STYLE_CHOICES.DEFAULT
            )
        ]


class ImageChoicesQuestion(Question, ChoicesQuestionMixin):
    """Django model representing a question with an image

    Attributes
    ----------
    image: django ``ImageField``
        The image of this question

    """

    image = models.ImageField(
        upload_to=upload_to_generator,
        verbose_name=_('Image')
    )

    verbose_question_type = _('Choices question with image')

    @property
    def serializer_class(self):
        """Get the serializer class to use for this model

        This is a property to avoid recursive import problem.

        """

        from .apiv2.serializers import ImageChoicesQuestionSerializer

        return ImageChoicesQuestionSerializer

    @classmethod
    def get_create_url(cls, questionnaire):
        """Get the url to create a new image choices question.

        Parameters
        ----------
        questionnaire: Questionnaire
            The questionnaire for which we want to create a new image choices question.

        Returns
        -------
        str

        """

        return reverse('questionnaires:imagechoicesquestion-create', kwargs={
            'questionnaire_uuid': questionnaire.pk
        })

    def get_absolute_url(self):
        """Get the url of the current image choices question.

        Returns
        -------
        str

        """

        return self.get_edit_url()

    def get_edit_url(self):
        """Get the url to edit the current image choices question.

        Returns
        -------
        str

        """

        return reverse('questionnaires:imagechoicesquestion-update', kwargs={
            'questionnaire_uuid': self.questionnaire.pk,
            'question_uuid': self.pk
        })


class Choice(Dependency, Syncable, Orderable):
    """Django model representing a choice

    Attributes
    ----------
    label: django ``CharField``
        The label of this choice
    is_valid: django ``BooleanField``, default to False
        If True, this choice will be considered as a valid one
    question: django ``ForeignKey`` to ``ChoicesQuestion``
        The choices question this choice belongs to

    """

    label = models.CharField(
        max_length=280,
        verbose_name=_('Label')
    )

    is_valid = models.BooleanField(
        verbose_name=_('Is valid'),
        default=False
    )

    question_content_type = models.ForeignKey(
        ContentType,
        on_delete=models.CASCADE
    )
    question_id = UUIDField()
    question = GenericForeignKey(
        'question_content_type',
        'question_id'
    )

    objects = DependencyChoiceManager.from_queryset(DependencyChoiceQuerySet)()

    versionable_link = (Training, 'question__questionnaire__training')
    non_lockable_fields = Dependency.non_lockable_fields + Syncable.non_lockable_fields

    class Meta:
        unique_together = ('question_id', 'question_content_type', 'sort_order',)
        ordering = ['sort_order']

    def get_unique_fields(self):
        """Overwrite the ``get_unique_fields`` of ``Orderable``

        ``Orderable`` add an "_id" suffix to each field, and we don't want that here

        Parameters
        ----------
        For the parameters, see ``Orderable``

        """

        return ['question_id', 'question_content_type']

    def is_selected(self, user, session):
        """Check if this choice was selected by the given user during the given session

        Parameters
        ----------
        user: MTPUser
            The user for which we want to check
        session: TrainingSession
            The session for which we want to check

        Returns
        -------
        bool
            True if the choice was selected,
            False otherwise

        """

        return ChoicesAnswer.objects.filter(
            user=user,
            session=session,
            choices__id__in=[self.id]
        ).exists()


class Answer(PolymorphicModel, Syncable, BrokerHookMixin):
    """Django model representing an answer

    Attributes
    ----------
    user: django ``ForeignKey`` to ``AUTH_USER_MODEL``
        The user who gave this answer
    training_session: django ``ForeignKey`` to ``TrainingSession``
        The training session during this answer was given
    question: django ``ForeignKey`` to ``Question``
        The question this answer was given for

    """

    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        related_name='questionnaires_answers',
        on_delete=models.CASCADE
    )

    session = models.ForeignKey(
        TrainingSession,
        related_name='questionnaires_answers',
        on_delete=models.CASCADE
    )

    question = models.ForeignKey(
        Question,
        related_name='answers',
        on_delete=models.CASCADE
    )

    objects = AnswerManager.from_queryset(AnswerQuerySet)()

    class Meta:
        unique_together = ('user', 'session', 'question',)

    def get_broker_topic(self):
        """Get the broker topic to publish changes of this model

        Returns
        -------
        str

        """

        return '%s/questionnaires/%s/answers' % (
            self.session_id,
            self.question.questionnaire_id
        )

    def get_serialized_representation(self):
        """Get the serialized representation of the current answer

        Returns
        -------
        dict

        """

        return self.serializer_class(self).data


class ChoicesAnswer(BrokerHookM2MFromMixin, Answer):
    """Django model representing an answer linked with a one or multiple ``Choice``s

    Attributes
    ----------
    choices: django ``ManyToManyField`` to ``Choice``
        The choices selected by the user when answering the question

    """

    choices = models.ManyToManyField(
        Choice,
        related_name='anwers'
    )

    @property
    def serializer_class(self):
        """Get the serializer class to use for this model

        This is a property to avoid recursive import problem.

        """

        from .apiv2.serializers import ChoicesAnswerSerializer

        return ChoicesAnswerSerializer


class OpenAnswer(Answer):
    """Django model representing an open answer

    Attributes
    ----------
    content: django ``TextField``
        The text content of the user answer


    """

    content = models.TextField(
        verbose_name=_('Content'),
        blank=True
    )

    @property
    def serializer_class(self):
        """Get the serializer class to use for this model

        This is a property to avoid recursive import problem.

        """

        from .apiv2.serializers import OpenAnswerSerializer

        return OpenAnswerSerializer
