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

from django.conf import settings
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, PermissionDenied
from rest_framework.pagination import LimitOffsetPagination
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
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.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.documents.models import Note
from mtp.apps.documents.serializers import NoteSerializer

from ..models import TrainingSession

from .filters import SessionFilter
from .permissions import (
    IsTrainerToCreateSession,
    IsTrainerToEditSession,
    IsTrainerToEditTraineesInSession,
)
from .serializers import TokenSerializer, TrainingSessionSerializer


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,
        IsTrainerToCreateSession,
        IsTrainerToEditSession,
    )
    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.select_related(
            'trainer',
            'trainer__photo',
            'training',
        ).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 retrieve(self, request, *args, **kwargs):  # pylint: disable=unused-argument
        """Assign a token to the session if it don't exists when we access it.

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

        instance = self.get_object()
        instance.get_or_assign_token()
        serializer = self.get_serializer(instance)
        return Response(serializer.data)

    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(methods=['post'])
    def close(self, request, *args, **kwargs):  # pylint: disable=unused-argument
        """Close this current training session

        Notes
        -----
        A training session can only be closed on a "MASTER" instance.

        """

        if settings.INSTANCE_IS_SLAVE:
            raise PermissionDenied(detail='Cannot close a training session on "SLAVE" instance.')

        training_session = self.get_object()
        training_session.close()

        serialized_training_session = self.get_serializer(training_session)
        return Response(serialized_training_session.data)


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 = (IsAuthenticated,)

    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 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 photostories(self, request, *args, **kwargs):  # pylint: disable=unused-argument
        """Get the photostories image paths for the provided session and user."""

        return Response(
            self.session.get_photostory_user_image_paths(self.get_object(), absolute=False)
        )

    @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 = ''
        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)
