"""This module holds the apiv2 views of the ``trainings`` app."""

from django.shortcuts import get_object_or_404
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _

from rest_framework import filters, status, viewsets
from rest_framework.decorators import detail_route
from rest_framework.exceptions import APIException
from rest_framework.pagination import LimitOffsetPagination
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework_extensions.cache.mixins import cache_response
from rest_framework_extensions.mixins import NestedViewSetMixin

from mtp.apps.api.cache_key_constructors import ListWithGetParamsConstructor
from mtp.apps.accounts.apiv2.permissions import IsTrainerToCreate, IsTrainerToEdit
from mtp.apps.accounts.apiv2.serializers import MTPUserSerializer
from mtp.apps.accounts.models import MTPUser
from mtp.apps.api.mixins import SoftDestroyModelMixin
from mtp.apps.api.permissions import IsAuthenticatedOrValidOAuthToken
from mtp.apps.assessments.apiv2.serializers import (
    UserLessonRecommendationSerializer,
    UserSessionAssessmentScoresSerializer,
)
from mtp.apps.assessments.models import CertificationProgramUserStatus
from mtp.apps.brainstormers.api.serializers import BrainstormerSerializer
from mtp.apps.brainstormers.models import Brainstormer
from mtp.apps.documents.models import Note
from mtp.apps.documents.serializers import NoteSerializer

from ..models import Training, TrainingActivity, TrainingSession
from ..redis_models import TrainingSession as RedisTrainingSession

from .filters import SessionFilter
from .permissions import IsTrainer, IsTrainerToEditTraineesInSession
from .serializers import (
    SessionUserStatusSerializer,
    TokenSerializer,
    TrainingActivitySerializer,
    TrainingSerializer,
    TrainingSessionSerializer,
)


class TrainingViewSet(viewsets.ReadOnlyModelViewSet):
    """A ViewSet to list and retrieve ``Training`` model"""

    serializer_class = TrainingSerializer
    permission_classes = (IsAuthenticated, IsTrainer,)

    def get_queryset(self):
        """Filter the training available to the user

        Returns
        -------
        TrainingQuerySet
            A QuerySet containing the trainings available to the user

        """

        return Training.objects.non_deleted().available_for_user(
            self.request.user
        ).select_related(
            'theme',
            'business_unit',
            'job_profile'
        )


class SessionViewSet(NestedViewSetMixin, SoftDestroyModelMixin, viewsets.ModelViewSet):
    """A ViewSet for listing or retrieving sessions.

    Only a trainer can create or update a session.

    """

    ordering_fields = ('start_date',)
    filter_backends = (filters.DjangoFilterBackend, filters.OrderingFilter,)
    filter_class = SessionFilter
    pagination_class = LimitOffsetPagination
    permission_classes = (
        IsAuthenticatedOrValidOAuthToken,
        IsTrainerToCreate,
        IsTrainerToEdit
    )
    serializer_class = TrainingSessionSerializer

    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.

        """

        session_queryset = TrainingSession.objects.non_deleted().select_related(
            'trainer',
            'trainer__photo',
            'training',
            'training__theme',
        ).prefetch_related(
            'trainer__mgroups',
            'users',
            'training__business_unit',
            'training__job_profile',
        )

        # The request is not coming from a user, but from an third application (ie: ekimetrics)
        if not self.request.user or not self.request.user.is_authenticated():
            return session_queryset.all()

        if self.request.user.is_trainer:
            return session_queryset.filter(trainer=self.request.user)
        elif self.request.GET.get('state') == TrainingSession.STATE_CHOICES.CLOSED:
            return session_queryset.filter(users=self.request.user)
        else:
            return session_queryset.all()

    # Cached for 30 seconds
    @cache_response(30, cache_errors=False, key_func=ListWithGetParamsConstructor())
    def list(self, request, *args, **kwargs):
        """Override the list method to apply the cache decorator

        For the parameters, see ``rest_framework.mixins.ListModelMixin``
        """

        return super().list(request, *args, **kwargs)

    def perform_create(self, serializer):
        """We register the current user like trainer if no trainer is provided.

        For the parameters, see ``rest_framework.mixins.CreateModelMixin``"""

        if 'trainer' not in serializer.validated_data:
            serializer.save(trainer=self.request.user)
        else:
            super().perform_create(serializer)

    @detail_route()
    def assessment_scores(self, request, *args, **kwargs):  # pylint: disable=unused-argument
        """Retrieve a list of assessment scores, one per user of the session"""

        session = self.get_object()

        if not session.is_closed:
            raise APIException('Session must be closed.')

        data = []
        for user in session.users.non_deleted():
            try:
                data.append(UserSessionAssessmentScoresSerializer(
                    session,
                    context={'user': user},
                ).data)
            # ignore the user if he has not passed the session
            except CertificationProgramUserStatus.DoesNotExist:
                pass

        return Response(data)

    @detail_route()
    def activities(self, request, *args, **kwargs):  # pylint: disable=unused-argument
        """Retrieve a list of activities for a ``TrainingSession`` instance

        For the parameters, see ``rest_framework.mixins.ListModelMixin``

        """

        training_session = self.get_object()

        serialized_activities = TrainingActivitySerializer(
            TrainingActivity.objects.for_session(training_session).available().non_deleted(),
            many=True
        ).data

        # Extend the list with the list of brainstormer of this session
        serialized_activities.extend(
            BrainstormerSerializer(
                Brainstormer.objects.filter(session=training_session).non_deleted(),
                many=True
            ).data
        )

        return Response(serialized_activities)


class TokenViewSet(viewsets.mixins.RetrieveModelMixin, viewsets.GenericViewSet):
    """A ViewSet for retrieving sessions by token."""

    lookup_field = 'token'
    queryset = TrainingSession.objects.non_deleted()
    serializer_class = TokenSerializer
    permission_classes = (IsAuthenticatedOrValidOAuthToken,)

    def retrieve(self, request, *args, **kwargs):
        """We add the current user in the session participants if he is a trainee.

        For the parameters, see ``rest_framework.mixins.RetrieveModelMixin``"""

        instance = self.get_object()

        if self.request.user and self.request.user.is_authenticated():
            if request.user.is_trainee:
                instance.users.add(request.user)

        serializer = self.get_serializer(instance)

        return Response(serializer.data)


class TraineeViewSet(NestedViewSetMixin,
                     viewsets.mixins.CreateModelMixin,
                     viewsets.mixins.DestroyModelMixin,
                     viewsets.mixins.ListModelMixin,
                     viewsets.GenericViewSet):
    """Viewset for the ``MTPUser`` model nested in ``SessionViewSet``.

    Only create, destroy and list actions are available.

    """

    permission_classes = (IsAuthenticated, IsTrainerToEditTraineesInSession,)
    queryset = MTPUser.objects.non_deleted().select_related(
        'photo'
    ).prefetch_related(
        'mgroups'
    )
    serializer_class = MTPUserSerializer

    @cached_property
    def session(self):
        """Get the provided session."""

        return get_object_or_404(
            TrainingSession,
            id=self.get_parents_query_dict()['training_sessions']
        )

    def create(self, request, *args, **kwargs):
        """Override create action to allow the addition of a new trainee in the provided session.

        For the parameters, see ``rest_framework.mixins.CreateModelMixin``"""

        # If no user id is provided, it's the current user which will be added.
        try:
            user_id = request.data['id']
        except KeyError:
            new_user = request.user
        else:
            new_user = get_object_or_404(MTPUser, id=user_id)

        self.perform_add(new_user)
        return Response(status=status.HTTP_201_CREATED)

    def perform_add(self, instance):
        """Add the user in session users."""

        self.session.users.add(instance)

    def perform_destroy(self, instance):
        """Remove the user of session users.

        For the parameters, see ``rest_framework.mixins.DestroyModelMixin``"""

        self.session.users.remove(instance)

    @detail_route()
    def notes(self, request, *args, **kwargs):  # pylint: disable=unused-argument
        """Get the notes for the provided session and user."""

        serialized_notes = NoteSerializer(
            Note.objects.non_deleted().filter(session=self.session, user=self.get_object()),
            many=True
        )

        return Response(serialized_notes.data)

    @detail_route()
    def freenotes(self, request, *args, **kwargs):  # pylint: disable=unused-argument
        """Get the "free notes" for the provided session and user, as a .txt file"""

        free_notes = Note.objects.non_deleted().filter(
            session=self.session,
            user=self.get_object(),
            document=None,
        ).values_list('text', flat=True)

        if not free_notes:
            text_content = _('No notes taken')
        else:
            text_content = '\n'.join(free_notes)  # pylint: disable=redefined-variable-type

        return Response(
            text_content,
            content_type='text/plain'
        )

    @detail_route(permission_classes=(IsAuthenticatedOrValidOAuthToken, ))
    def recommendations(self, request, *args, **kwargs):  # pylint: disable=unused-argument
        """Get the lesson recommendations for the provided session and user"""

        user = self.get_object()

        if not self.session.is_closed:
            raise APIException('Session must be closed.')

        try:
            certification = CertificationProgramUserStatus.objects.non_deleted().get(
                user=user,
                session=self.session,
            )
        except (CertificationProgramUserStatus.DoesNotExist,
                CertificationProgramUserStatus.MultipleObjectsReturned):
            raise APIException('Could not find a suitable CertificationProgramUserStatus.')

        serializer = UserLessonRecommendationSerializer(
            certification.recommendations.non_deleted(),
            many=True,
        )

        return Response(serializer.data)


class SessionUsersStatusView(APIView):
    """View to set and get the users connection statuses in a training session

    Notes
    -----
    We don't use the ``SessionViewSet`` from above, because this view will be accessed
    frequently, and we don't want to perform any extra BDD queries.

    """

    def get_redis_instance(self, session_uuid):
        """Get the redis instance for the provided session uuid

        Parameters
        ----------
        session_uuid: str
            The uuid of the session we want the redis instance

        Returns
        -------
        RedisTrainingSession

        """

        redis_instance, __ = RedisTrainingSession.get_or_connect(pk=session_uuid)

        return redis_instance

    def patch(self, request, session_uuid, user_uuid, *args, **kwargs):  # pylint: disable=unused-argument
        """Set the status of the user in the given session

        Parameters
        ----------
        session_uuid: str
            The session we want for which we want the users's statuses.
        user_uuid: str
            The user we want to flag as "online".

        For the other parameters, see ``rest_framework.views.APIView``

        Returns
        -------
        Response
            An empty Response

        """

        self.get_redis_instance(session_uuid).set_user_online(user_uuid)

        return Response({})

    def get(self, request, session_uuid, user_uuid, *args, **kwargs):  # pylint: disable=unused-argument
        """Get the statuses of the users in this session

        Parameters
        ----------
        session_uuid: str
            The session we want for which we want the users's statuses.

        For the other parameters, see ``rest_framework.views.APIView``

        """

        last_pings_as_dict = [
            {
                'user_id': user_id,
                'timestamp': score
            }
            for user_id, score in self.get_redis_instance(session_uuid).last_pings.zrange(
                0, -1, withscores=True
            )
        ]

        serializer = SessionUserStatusSerializer(
            last_pings_as_dict,
            many=True
        )

        return Response(serializer.data)
