"""
HTTP end-points for the Bookmarks API.

For more information, see:
https://openedx.atlassian.net/wiki/display/TNL/Bookmarks+API
"""
import logging

from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_noop
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey, UsageKey
from rest_framework import permissions, status
from rest_framework.authentication import SessionAuthentication
from rest_framework.generics import ListCreateAPIView
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework_oauth.authentication import OAuth2Authentication

import eventtracking
from openedx.core.djangoapps.bookmarks.api import BookmarksLimitReachedError
from openedx.core.lib.api.paginators import DefaultPagination
from openedx.core.lib.api.permissions import IsUserInUrl
from openedx.core.lib.url_utils import unquote_slashes
from xmodule.modulestore.exceptions import ItemNotFoundError

from . import DEFAULT_FIELDS, OPTIONAL_FIELDS, api
from .serializers import BookmarkSerializer

log = logging.getLogger(__name__)

# Default error message for user
DEFAULT_USER_MESSAGE = ugettext_noop(u'An error has occurred. Please try again.')


class BookmarksPagination(DefaultPagination):
    """
    Paginator for bookmarks API.
    """
    page_size = 10
    max_page_size = 100

    def get_paginated_response(self, data):
        """
        Annotate the response with pagination information.
        """
        response = super(BookmarksPagination, self).get_paginated_response(data)

        # Add `current_page` value, it's needed for pagination footer.
        response.data["current_page"] = self.page.number

        # Add `start` value, it's needed for the pagination header.
        response.data["start"] = (self.page.number - 1) * self.get_page_size(self.request)

        return response


class BookmarksViewMixin(object):
    """
    Shared code for bookmarks views.
    """

    def fields_to_return(self, params):
        """
        Returns names of fields which should be included in the response.

        Arguments:
            params (dict): The request parameters.
        """
        optional_fields = params.get('fields', '').split(',')
        return DEFAULT_FIELDS + [field for field in optional_fields if field in OPTIONAL_FIELDS]

    def error_response(self, developer_message, user_message=None, error_status=status.HTTP_400_BAD_REQUEST):
        """
        Create and return a Response.

        Arguments:
            message (string): The message to put in the developer_message
                and user_message fields.
            status: The status of the response. Default is HTTP_400_BAD_REQUEST.
        """
        if not user_message:
            user_message = developer_message

        return Response(
            {
                "developer_message": developer_message,
                "user_message": _(user_message)  # pylint: disable=translation-of-non-string
            },
            status=error_status
        )


class BookmarksListView(ListCreateAPIView, BookmarksViewMixin):
    """
    **Use Case**

        * Get a paginated list of bookmarks for a user.

            The list can be filtered by passing parameter "course_id=<course_id>"
            to only include bookmarks from a particular course.

            The bookmarks are always sorted in descending order by creation date.

            Each page in the list contains 10 bookmarks by default. The page
            size can be altered by passing parameter "page_size=<page_size>".

            To include the optional fields pass the values in "fields" parameter
            as a comma separated list. Possible values are:

                * "display_name"
                * "path"

        * Create a new bookmark for a user.

            The POST request only needs to contain one parameter "usage_id".

            Http400 is returned if the format of the request is not correct,
            the usage_id is invalid or a block corresponding to the usage_id
            could not be found.

    **Example Requests**

        GET /api/bookmarks/v1/bookmarks/?course_id={course_id1}&fields=display_name,path

        POST /api/bookmarks/v1/bookmarks/
        Request data: {"usage_id": <usage-id>}

    **Response Values**

        * count: The number of bookmarks in a course.

        * next: The URI to the next page of bookmarks.

        * previous: The URI to the previous page of bookmarks.

        * num_pages: The number of pages listing bookmarks.

        * results:  A list of bookmarks returned. Each collection in the list
          contains these fields.

            * id: String. The identifier string for the bookmark: {user_id},{usage_id}.

            * course_id: String. The identifier string of the bookmark's course.

            * usage_id: String. The identifier string of the bookmark's XBlock.

            * display_name: String. (optional) Display name of the XBlock.

            * path: List. (optional) List of dicts containing {"usage_id": <usage-id>, display_name:<display-name>}
                for the XBlocks from the top of the course tree till the parent of the bookmarked XBlock.

            * created: ISO 8601 String. The timestamp of bookmark's creation.

    """
    authentication_classes = (OAuth2Authentication, SessionAuthentication)
    pagination_class = BookmarksPagination
    permission_classes = (permissions.IsAuthenticated,)
    serializer_class = BookmarkSerializer

    def get_serializer_context(self):
        """
        Return the context for the serializer.
        """
        context = super(BookmarksListView, self).get_serializer_context()
        if self.request.method == 'GET':
            context['fields'] = self.fields_to_return(self.request.query_params)
        return context

    def get_queryset(self):
        """
        Returns queryset of bookmarks for GET requests.

        The results will only include bookmarks for the request's user.
        If the course_id is specified in the request parameters,
        the queryset will only include bookmarks from that course.
        """
        course_id = self.request.query_params.get('course_id', None)

        if course_id:
            try:
                course_key = CourseKey.from_string(course_id)
            except InvalidKeyError:
                log.error(u'Invalid course_id: %s.', course_id)
                return []
        else:
            course_key = None

        return api.get_bookmarks(
            user=self.request.user, course_key=course_key,
            fields=self.fields_to_return(self.request.query_params), serialized=False
        )

    def paginate_queryset(self, queryset):
        """ Override GenericAPIView.paginate_queryset for the purpose of eventing """
        page = super(BookmarksListView, self).paginate_queryset(queryset)

        course_id = self.request.query_params.get('course_id')
        if course_id:
            try:
                CourseKey.from_string(course_id)
            except InvalidKeyError:
                return page

        event_data = {
            'list_type': 'all_courses',
            'bookmarks_count': self.paginator.page.paginator.count,
            'page_size': self.paginator.page.paginator.per_page,
            'page_number': self.paginator.page.number,
        }
        if course_id is not None:
            event_data['list_type'] = 'per_course'
            event_data['course_id'] = course_id

        eventtracking.tracker.emit('edx.bookmark.listed', event_data)

        return page

    def post(self, request):
        """
        POST /api/bookmarks/v1/bookmarks/
        Request data: {"usage_id": "<usage-id>"}
        """

        if not request.data:
            return self.error_response(ugettext_noop(u'No data provided.'), DEFAULT_USER_MESSAGE)

        usage_id = request.data.get('usage_id', None)
        if not usage_id:
            return self.error_response(ugettext_noop(u'Parameter usage_id not provided.'), DEFAULT_USER_MESSAGE)

        try:
            usage_key = UsageKey.from_string(unquote_slashes(usage_id))
        except InvalidKeyError:
            error_message = ugettext_noop(u'Invalid usage_id: {usage_id}.').format(usage_id=usage_id)
            log.error(error_message)
            return self.error_response(error_message, DEFAULT_USER_MESSAGE)

        try:
            bookmark = api.create_bookmark(user=self.request.user, usage_key=usage_key)
        except ItemNotFoundError:
            error_message = ugettext_noop(u'Block with usage_id: {usage_id} not found.').format(usage_id=usage_id)
            log.error(error_message)
            return self.error_response(error_message, DEFAULT_USER_MESSAGE)
        except BookmarksLimitReachedError:
            error_message = ugettext_noop(
                u'You can create up to {max_num_bookmarks_per_course} bookmarks.'
                u' You must remove some bookmarks before you can add new ones.'
            ).format(max_num_bookmarks_per_course=settings.MAX_BOOKMARKS_PER_COURSE)
            log.info(
                u'Attempted to create more than %s bookmarks',
                settings.MAX_BOOKMARKS_PER_COURSE
            )
            return self.error_response(error_message)

        return Response(bookmark, status=status.HTTP_201_CREATED)


class BookmarksDetailView(APIView, BookmarksViewMixin):
    """
    **Use Cases**

        Get or delete a specific bookmark for a user.

    **Example Requests**:

        GET /api/bookmarks/v1/bookmarks/{username},{usage_id}/?fields=display_name,path

        DELETE /api/bookmarks/v1/bookmarks/{username},{usage_id}/

    **Response for GET**

        Users can only delete their own bookmarks. If the bookmark_id does not belong
        to a requesting user's bookmark a Http404 is returned. Http404 will also be
        returned if the bookmark does not exist.

        * id: String. The identifier string for the bookmark: {user_id},{usage_id}.

        * course_id: String. The identifier string of the bookmark's course.

        * usage_id: String. The identifier string of the bookmark's XBlock.

        * display_name: (optional) String. Display name of the XBlock.

        * path: (optional) List of dicts containing {"usage_id": <usage-id>, display_name: <display-name>}
            for the XBlocks from the top of the course tree till the parent of the bookmarked XBlock.

        * created: ISO 8601 String. The timestamp of bookmark's creation.

    **Response for DELETE**

        Users can only delete their own bookmarks.

        A successful delete returns a 204 and no content.

        Users can only delete their own bookmarks. If the bookmark_id does not belong
        to a requesting user's bookmark a 404 is returned. 404 will also be returned
        if the bookmark does not exist.
    """
    authentication_classes = (OAuth2Authentication, SessionAuthentication)
    permission_classes = (permissions.IsAuthenticated, IsUserInUrl)

    serializer_class = BookmarkSerializer

    def get_usage_key_or_error_response(self, usage_id):
        """
        Create and return usage_key or error Response.

        Arguments:
            usage_id (string): The id of required block.
        """
        try:
            return UsageKey.from_string(usage_id)
        except InvalidKeyError:
            error_message = ugettext_noop(u'Invalid usage_id: {usage_id}.').format(usage_id=usage_id)
            log.error(error_message)
            return self.error_response(error_message, error_status=status.HTTP_404_NOT_FOUND)

    def get(self, request, username=None, usage_id=None):  # pylint: disable=unused-argument
        """
        GET /api/bookmarks/v1/bookmarks/{username},{usage_id}?fields=display_name,path
        """
        usage_key_or_response = self.get_usage_key_or_error_response(usage_id=usage_id)

        if isinstance(usage_key_or_response, Response):
            return usage_key_or_response

        try:
            bookmark_data = api.get_bookmark(
                user=request.user,
                usage_key=usage_key_or_response,
                fields=self.fields_to_return(request.query_params)
            )
        except ObjectDoesNotExist:
            error_message = ugettext_noop(
                u'Bookmark with usage_id: {usage_id} does not exist.'
            ).format(usage_id=usage_id)
            log.error(error_message)
            return self.error_response(error_message, error_status=status.HTTP_404_NOT_FOUND)

        return Response(bookmark_data)

    def delete(self, request, username=None, usage_id=None):  # pylint: disable=unused-argument
        """
        DELETE /api/bookmarks/v1/bookmarks/{username},{usage_id}
        """
        usage_key_or_response = self.get_usage_key_or_error_response(usage_id=usage_id)

        if isinstance(usage_key_or_response, Response):
            return usage_key_or_response

        try:
            api.delete_bookmark(user=request.user, usage_key=usage_key_or_response)
        except ObjectDoesNotExist:
            error_message = ugettext_noop(
                u'Bookmark with usage_id: {usage_id} does not exist.'
            ).format(usage_id=usage_id)
            log.error(error_message)
            return self.error_response(error_message, error_status=status.HTTP_404_NOT_FOUND)

        return Response(status=status.HTTP_204_NO_CONTENT)
