"""This module holds the api-related views of the ``trainings`` app"""

from itertools import chain
import json

from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.serializers.json import DjangoJSONEncoder
from django.db.models import Q
from django.http import Http404
from django.shortcuts import render_to_response, get_object_or_404
from django.template import RequestContext
from django.utils.translation import pgettext

from rest_framework import generics, status
from rest_framework.generics import ListCreateAPIView
from rest_framework.renderers import JSONRenderer
from rest_framework.response import Response
from rest_framework.views import APIView

from mtp.apps.accounts.constants import INTERNAL_GROUP_TRAINEE
from mtp.apps.accounts.api.serializers import UserSerializer
from mtp.apps.accounts.models import GroupRole
from mtp.apps.api.authentication import CsrfExemptSessionAuthentication
from mtp.apps.documents.serializers import NoteSerializer
from mtp.apps.brainstormers.models import Brainstormer, BrainstormerActivity
from mtp.apps.common.message_broker import send_data_to_topic, MESSAGE_BROKER_TOPICS
from mtp.apps.common.views_utils import CacheMixin
from mtp.apps.documents.models import BackgroundExecution, Document, Note
from mtp.apps.evaluations.models import Evaluation
from mtp.apps.metaplans.models import Metaplan
from mtp.apps.roleplays.models import Roleplay
from mtp.apps.surveys.models import Survey
from mtp.apps.workshops.models import Workshop
from mtp.apps.workshops.serializers import CompatibilityWorkshopSerializer

from ..models import Training, TrainingSession
from ..tasks import compute_assessment_scores

from .serializers import (
    TrainingSerializer, TrainingSessionReadSerializer,
    TrainingSessionWriteSerializer, SessionChallengeScoreSerializer
)


class SessionList(ListCreateAPIView):
    """
    List all sessions, or create a new session.

    Notes
    -----
    We add the annotate before testing the type of user
    because the serializer is expecting the attribute
    `is_attendee`` even for the `trainers`.

    The trainer see only his own `TrainingSession`.
    The trainee see the session he's in and the
    session who are not `restricted`

    """
    serializer_class = TrainingSessionReadSerializer

    def get_queryset(self):
        """Filter the training sessions according the current user.

        Since we join on multiple tables (using the ``is_attendee`` annotation), we can't use
        the ``distinct`` clause, since it's messing up the order of the QuerySet.
        So, we are making two queries using the ``values_list`` to remove the duplicates.

        Returns
        -------
        QuerySet(TrainingSession)
            The filtered queryset.

        """

        user = self.request.user
        sessions_qs = TrainingSession.objects.annotate_is_attendee(user)

        # We need this to tranform the `INNER JOIN` made by the
        # annotate to a `LEFT OUTER JOIN`.
        # We do that to avoid excluding sessions without trainees.
        sessions_qs.query.promote_joins(
            [TrainingSession.users.through._meta.db_table]
        )

        # Exclude the closed ones
        sessions_qs = sessions_qs.exclude(state=TrainingSession.STATE_CHOICES.CLOSED)

        if user.is_staff:
            # Since this query is annotated with a LEFT JOIN, we need a ``distinct`` to avoid
            # potential duplicates.
            return sessions_qs.filter(trainer=user).distinct()
        else:
            session_ids = sessions_qs.exclude(
                state=TrainingSession.STATE_CHOICES.NOT_YET_OPEN
            ).filter(
                ~Q(permissions=TrainingSession.PERMISSION_CHOICES.PRIVATE) | Q(users=user)
            ).values_list('id', flat=True).distinct()

            return TrainingSession.objects.filter(id__in=session_ids)

    def perform_create(self, serializer):
        serializer.save(trainer=self.request.user)

    def create(self, request):

        user = request.user
        if not user.is_staff:
            return Response({}, status=status.HTTP_403_FORBIDDEN)

        serializer = TrainingSessionWriteSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save(trainer=request.user)
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


class SessionDetail(APIView):
    """Retrieve a single session instance"""

    def get_training_session(self, pk):
        """Retrieve the training session from its PK

        Parameters
        ----------
        pk: str
            The PK of the training session to retrieve

        Returns
        -------
        ``mtp.apps.trainings.models.TrainingSession``

        Raises
        ------
        Http404
            If the training session cannot be found

        """

        try:
            return TrainingSession.objects.get(pk=pk)
        except TrainingSession.DoesNotExist:
            raise Http404

    # pylint: disable=unused-argument,redefined-builtin
    def get(self, request, session_uuid, format=None):
        """Serialize the training session and return it"""

        session = self.get_training_session(session_uuid)
        serializer = TrainingSessionReadSerializer(session)

        return Response(serializer.data)

    def put(self, request, session_uuid, format=None):
        """Update a ``TrainingSession``

        If the state changed to the ``CLOSED`` one, start the post-session async tasks.

        Parameters
        ----------
        session_uuid: str
            The PK of the training session to update

        Returns
        -------
        ``Response``
            An Http response with the updated and serialized ``TrainingSession``

        """

        session = self.get_training_session(session_uuid)
        if session.trainer != request.user:
            return Response(status=status.HTTP_403_FORBIDDEN)

        if "users" not in request.data:
            users = session.users.all()
        else:
            users = None

        session_current_state = session.state

        serializer = TrainingSessionWriteSerializer(
            session, data=request.data, partial=True
        )
        if serializer.is_valid():
            if users:
                serializer.save(users=users)
            else:
                serializer.save()

            # The session is now closed, we need to manually call the post-session tasks
            if (session_current_state != session.state and
                    session.state == TrainingSession.STATE_CHOICES.CLOSED and
                    settings.INSTANCE_IS_MASTER):

                if session.is_assessing:
                    compute_assessment_scores.delay(session.pk, force_recompute=True)

                session.send_end_mail()

            return Response(serializer.data)

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


class SessionUGC(APIView):
    """Retrieve contents generated by user"""

    def get_object(self, pk):
        try:
            return TrainingSession.objects.get(pk=pk)
        except TrainingSession.DoesNotExist:
            raise Http404

      # pylint: disable=unused-argument,redefined-builtin
    def get(self, request, session_uuid, format=None):
        session = self.get_object(session_uuid)

        notes = NoteSerializer(
            Note.objects.filter(user=request.user, session=session),
            many=True
        )

        ugc = {
            "notes": notes.data,
        }

        response = Response(
            ugc,
            status=status.HTTP_200_OK
        )
        return response


class SessionContent(CacheMixin, APIView):
    """
    Retrieve a session content
    """

    template_name = ""
    cache_timeout = 15  # seconds

    def get_object(self, pk):
        try:
            return TrainingSession.objects.get(pk=pk)
        except TrainingSession.DoesNotExist:
            raise Http404

    def convert_survey_to_workshop(self, survey_data):
        return [{
            'title': pgettext(context='Title for API', message='Survey'),
            'has_score': False,
            'order': 1,
            'questions': [
                {
                    'options': [],
                    'label': question['title'],
                    'description': '',  # not used but entry is expected
                    'order': question_index + 1,
                    'id': question['id'],
                    'type': 'quizz',
                    'is_multiple_choices': question['type'] == 'MULTIPLE_CHOICES',
                    '_real_type': question['type'],
                    'answers': [
                        {
                            'points': 0,
                            'order': choice_index + 1,
                            'id': choice['id'],
                            'label': choice['label'],
                            'good': False,
                            'color': '',
                        }
                        for choice_index, choice in enumerate(question['choices'])
                    ],

                }
                for question_index, question in enumerate(chain.from_iterable(
                    (pool['questions'] for pool in survey_data['pools'])
                ))
            ],
            'groups': [INTERNAL_GROUP_TRAINEE],
            'id': survey_data['id'],
            'type': 'survey',
            'module': 'survey',
            'preview': 'workshop-preview.png',
        }]

    # FIXME : mettre une explication du fixme
    # pylint: disable=unused-argument,redefined-builtin
    def get(self, request, session_uuid, format=None):
        session = self.get_object(session_uuid)

        # The content of the session being asked, we open the session
        if not session.is_closed:
            session.open()

            # We also need to immediately mark the session as ``is_live``
            # So the trainees can access it without waiting for the first "ping" from the trainer
            if self.request.user == session.trainer:
                session.trainer.keep_alive(session_id=session_uuid)

        # Metaplan Version
        version = request.GET.get('version', None)
        # Fixme: remove the line below line when old api is deprecated
        current_session = session
        training = session.training
        trainer = session.trainer

        workshops = Workshop.objects.filter(training=training).non_deleted()
        serialized_workshop = CompatibilityWorkshopSerializer(workshops, many=True)
        workshops_json = JSONRenderer().render(serialized_workshop.data)

        survey, __ = Survey.objects.get_or_create_for_training(
            session.training, session.trainer
        )

        survey_json = json.dumps(
            self.convert_survey_to_workshop(survey.serialize()),
            cls=DjangoJSONEncoder
        ) if survey else None

        documents = Document.objects.filter(training=training).non_deleted()
        documents_objects = chain.from_iterable(
            document.compatibility_serialize()
            for document in documents
        )
        documents_json = JSONRenderer().render(documents_objects)

        downloads = []

        evaluations = Evaluation.objects.filter(
            training=training, is_deleted=False, hidden=False
        )
        bg_tasks = BackgroundExecution.objects.all()

        metaplans = Metaplan.objects.for_training_session(session).non_deleted()

        message_broker_settings = {
            'url': settings.MESSAGE_BROKER_WEBSOCKET_HOST,
            'port': settings.MESSAGE_BROKER_WEBSOCKET_PORT,
        }

        brainstormers = Brainstormer.objects.for_training_session(session).non_deleted()

        activities = BrainstormerActivity.objects.non_deleted()

        roleplays = Roleplay.objects.non_deleted().filter(training=training)

        return render_to_response(
            'api/json/current.json', locals(),
            content_type='application/json',
            context_instance=RequestContext(request),
        )
    # EMXIF


class TrainingList(generics.ListAPIView):
    """List all trainings the current trainer can access.

    Notes
    -----
    We only want the favorites trainings to be returned.

    See also
    --------
    ``mtp.apps.trainings.models.TrainingQueryset``

    """

    serializer_class = TrainingSerializer

    def get_queryset(self):
        return Training.objects.non_deleted().available_for_user(self.request.user)


class SessionUserList(APIView):
    """List all session's Users or add a new one."""

    authentication_classes = [CsrfExemptSessionAuthentication]

    def get_object(self, pk):
        try:
            return TrainingSession.objects.get(pk=pk)
        except TrainingSession.DoesNotExist:
            raise Http404

    def get_user(self, pk):
        try:
            return get_user_model().objects.get(pk=pk)
        except get_user_model().DoesNotExist:
            raise Http404

    # pylint: disable=unused-argument,redefined-builtin
    def get(self, request, session_uuid, format=None):
        """Get the list of users of this ``TrainingSession``

        Parameters
        ----------
        session_uuid: str
            The UUID of the session we want the users from

        Returns
        -------
        ``Response``
            An http response containing a serialized user list

        Notes
        -----
        An explanation about the "WTF Zone" comment below:

        - Someone added this hack in the first place
        - Someone came and removed this hack
        - Someone re-added this hack on the 2.5
        - Someone want the hack back on the 3.0 also

        "Legacy API, my ass !"
        © Metamatik 2015

        """

        session = self.get_object(pk=session_uuid)
        users = list(session.users.non_deleted())

        # WTF Zone
        users.append(session.trainer)

        serializer = UserSerializer(users, many=True)
        return Response(serializer.data)

    # pylint: disable=unused-argument,redefined-builtin
    def post(self, request, session_uuid, format=None):
        """
        Check if user belong to the session. Else, add the user
        """

        user = request.user
        session = self.get_object(pk=session_uuid)
        # add comment and put it in TrainingSession model method
        if user.is_staff:
            if session.trainer != user:
                return Response({}, status=status.HTTP_403_FORBIDDEN)
            if "user_id" not in request.data:
                return Response({}, status=status.HTTP_400_BAD_REQUEST)

            user_to_add = self.get_user(pk=request.data["user_id"])
            session.users.add(user_to_add)
            for group in session.training.default_group_set.mgroups.exclude(
                    pk__in=user_to_add.mgroups.values_list('pk', flat=True)):
                GroupRole.objects.create(user=user_to_add, group=group)
            session.save()
            user_to_add.save()
            send_data_to_topic(json.dumps(
                {
                    "action": "join",
                    "module_name": "waiting_room",
                    "module_id": "",
                    "sender_id": session.trainer.id,
                    "recepient_id": user_to_add.id,
                    "session_id": session.id,
                    "first_name": user.first_name,
                    "last_name": user.last_name,
                    "training_name": session.training.name,
                }
            ), MESSAGE_BROKER_TOPICS["waitingroom"])

            return Response({})
        else:
            if user in session.users.all():
                return Response({})
            if session.permissions == TrainingSession.PERMISSION_CHOICES.PUBLIC:
                session.users.add(user)
                # We cannot use ``bulk_create`` because it does not call the ``save`` method and the
                # ``Syncable`` models need to call the ``save`` method for each update.
                for group in session.training.default_group_set.mgroups.exclude(
                        pk__in=user.mgroups.values_list('pk', flat=True)):
                    GroupRole.objects.create(user=user, group=group)
                session.save()
                user.save()
                return Response({})
            elif session.permissions == TrainingSession.PERMISSION_CHOICES.APPROVAL_NEEDED:
                send_data_to_topic(json.dumps(
                    {
                        "action": "join_request",
                        "module_name": "waiting_room",
                        "module_id": "",
                        "sender_id": user.id,
                        "recepient_id": session.trainer.id,
                        "session_id": session.id,
                        "first_name": user.first_name,
                        "last_name": user.last_name,
                        "training_name": session.training.name
                    }
                ), MESSAGE_BROKER_TOPICS["trainer"](session.trainer.id))

                return Response({}, status=status.HTTP_402_PAYMENT_REQUIRED)
            else:
                return Response({}, status=status.HTTP_403_FORBIDDEN)


class SessionChallengeScoreListView(APIView):
    """Get the challenge scores of the given training session

    Example
    -------
    The data is returned in that format:

    >>> [
    ...     {
    ...     'score': 0,
    ...     'trainee': {
    ...         'id': '3df8a6b6-e8e6-4956-820c-0e0067a0fec5',
    ...         'username': 'trainee-02',
    ...         'first_name': 'Trai-02',
    ...         'last_name': 'Nee-02',
    ...         'email': "trainee-02@magency.fr",
    ...         'natural_language': "en",
    ...         'photo': None,
    ...         'photos': [],
    ...         'groups': [ 'trainee' ]
    ...        }
    ...     }
    >>> ]

    See also
    --------
    ``mtp.apps.trainings.models.TrainingSession.debrief_challenge_scores`` method.

    """

    def get(self, request, session_uuid):  # pylint: disable=unused-argument
        """Get the score, serialize them and return the result"""

        training_session = get_object_or_404(TrainingSession, pk=session_uuid)

        challenge_scores = training_session.debrief_challenge_scores()
        serialized_challenge_scores = SessionChallengeScoreSerializer(
            challenge_scores, many=True)

        return Response(serialized_challenge_scores.data)
