"""
Helper methods related to EdxNotes.
"""


import json
import logging
from datetime import datetime
from json import JSONEncoder
from uuid import uuid4

import requests
import six
from six.moves.urllib.parse import urlencode, urlparse, parse_qs
from dateutil.parser import parse as dateutil_parse
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.urls import reverse
from django.utils.translation import ugettext as _
from oauth2_provider.models import Application
from opaque_keys.edx.keys import UsageKey
from requests.exceptions import RequestException

from lms.djangoapps.courseware.access import has_access
from lms.djangoapps.courseware.courses import get_current_child
from edxnotes.exceptions import EdxNotesParseError, EdxNotesServiceUnavailable
from edxnotes.plugins import EdxNotesTab
from lms.lib.utils import get_parent_unit
from openedx.core.djangoapps.oauth_dispatch.jwt import create_jwt_for_user
from openedx.core.djangolib.markup import Text
from student.models import anonymous_id_for_user
from util.date_utils import get_default_time_display
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError

log = logging.getLogger(__name__)

DEFAULT_PAGE = 1
DEFAULT_PAGE_SIZE = 25


class NoteJSONEncoder(JSONEncoder):
    """
    Custom JSON encoder that encode datetime objects to appropriate time strings.
    """
    # pylint: disable=method-hidden
    def default(self, obj):
        if isinstance(obj, datetime):
            return get_default_time_display(obj)
        return json.JSONEncoder.default(self, obj)


def get_edxnotes_id_token(user):
    """
    Returns generated ID Token for edxnotes.
    """
    try:
        notes_application = Application.objects.get(name=settings.EDXNOTES_CLIENT_NAME)
    except Application.DoesNotExist:
        raise ImproperlyConfigured(
            u'OAuth2 Client with name [{}] does not exist.'.format(settings.EDXNOTES_CLIENT_NAME)
        )
    return create_jwt_for_user(
        user, secret=notes_application.client_secret, aud=notes_application.client_id
    )


def get_token_url(course_id):
    """
    Returns token url for the course.
    """
    return reverse("get_token", kwargs={
        "course_id": six.text_type(course_id),
    })


def send_request(user, course_id, page, page_size, path="", text=None):
    """
    Sends a request to notes api with appropriate parameters and headers.

    Arguments:
        user: Current logged in user
        course_id: Course id
        page: requested or default page number
        page_size: requested or default page size
        path: `search` or `annotations`. This is used to calculate notes api endpoint.
        text: text to search.

    Returns:
        Response received from notes api
    """
    url = get_internal_endpoint(path)
    params = {
        "user": anonymous_id_for_user(user, None),
        "course_id": six.text_type(course_id),
        "page": page,
        "page_size": page_size,
    }

    if text:
        params.update({
            "text": text,
            "highlight": True
        })

    try:
        response = requests.get(
            url,
            headers={
                "x-annotator-auth-token": get_edxnotes_id_token(user)
            },
            params=params,
            timeout=(settings.EDXNOTES_CONNECT_TIMEOUT, settings.EDXNOTES_READ_TIMEOUT)
        )
    except RequestException:
        log.error(u"Failed to connect to edx-notes-api: url=%s, params=%s", url, str(params))
        raise EdxNotesServiceUnavailable(_("EdxNotes Service is unavailable. Please try again in a few minutes."))

    return response


def delete_all_notes_for_user(user):
    """
    helper method to delete all notes for a user, as part of GDPR compliance

    :param user: The user object associated with the deleted notes
    :return: response (requests) object

    Raises:
        EdxNotesServiceUnavailable - when notes api is not found/misconfigured.
    """
    url = get_internal_endpoint('retire_annotations')
    headers = {
        "x-annotator-auth-token": get_edxnotes_id_token(user),
    }
    data = {
        "user": anonymous_id_for_user(user, None)
    }
    try:
        response = requests.post(
            url=url,
            headers=headers,
            data=data,
            timeout=(settings.EDXNOTES_CONNECT_TIMEOUT, settings.EDXNOTES_READ_TIMEOUT)
        )
    except RequestException:
        log.error(u"Failed to connect to edx-notes-api: url=%s, params=%s", url, str(headers))
        raise EdxNotesServiceUnavailable(_("EdxNotes Service is unavailable. Please try again in a few minutes."))

    return response


def preprocess_collection(user, course, collection):
    """
    Prepare `collection(notes_list)` provided by edx-notes-api
    for rendering in a template:
       add information about ancestor blocks,
       convert "updated" to date

    Raises:
        ItemNotFoundError - when appropriate module is not found.
    """
    # pylint: disable=too-many-statements

    store = modulestore()
    filtered_collection = list()
    cache = {}
    include_path_info = ('course_structure' not in settings.NOTES_DISABLED_TABS)
    with store.bulk_operations(course.id):
        for model in collection:
            update = {
                u"updated": dateutil_parse(model["updated"]),
            }

            model.update(update)
            usage_id = model["usage_id"]
            if usage_id in list(cache.keys()):
                model.update(cache[usage_id])
                filtered_collection.append(model)
                continue

            usage_key = UsageKey.from_string(usage_id)
            # Add a course run if necessary.
            usage_key = usage_key.replace(course_key=store.fill_in_run(usage_key.course_key))

            try:
                item = store.get_item(usage_key)
            except ItemNotFoundError:
                log.debug(u"Module not found: %s", usage_key)
                continue

            if not has_access(user, "load", item, course_key=course.id):
                log.debug(u"User %s does not have an access to %s", user, item)
                continue

            unit = get_parent_unit(item)
            if unit is None:
                log.debug(u"Unit not found: %s", usage_key)
                continue

            if include_path_info:
                section = unit.get_parent()
                if not section:
                    log.debug(u"Section not found: %s", usage_key)
                    continue
                if section.location in list(cache.keys()):
                    usage_context = cache[section.location]
                    usage_context.update({
                        "unit": get_module_context(course, unit),
                    })
                    model.update(usage_context)
                    cache[usage_id] = cache[unit.location] = usage_context
                    filtered_collection.append(model)
                    continue

                chapter = section.get_parent()
                if not chapter:
                    log.debug(u"Chapter not found: %s", usage_key)
                    continue
                if chapter.location in list(cache.keys()):
                    usage_context = cache[chapter.location]
                    usage_context.update({
                        "unit": get_module_context(course, unit),
                        "section": get_module_context(course, section),
                    })
                    model.update(usage_context)
                    cache[usage_id] = cache[unit.location] = cache[section.location] = usage_context
                    filtered_collection.append(model)
                    continue

            usage_context = {
                "unit": get_module_context(course, unit),
                "section": get_module_context(course, section) if include_path_info else {},
                "chapter": get_module_context(course, chapter) if include_path_info else {},
            }
            model.update(usage_context)
            if include_path_info:
                cache[section.location] = cache[chapter.location] = usage_context

            cache[usage_id] = cache[unit.location] = usage_context
            filtered_collection.append(model)

    return filtered_collection


def get_module_context(course, item):
    """
    Returns dispay_name and url for the parent module.
    """
    item_dict = {
        'location': six.text_type(item.location),
        'display_name': Text(item.display_name_with_default),
    }
    if item.category == 'chapter' and item.get_parent():
        # course is a locator w/o branch and version
        # so for uniformity we replace it with one that has them
        course = item.get_parent()
        item_dict['index'] = get_index(item_dict['location'], course.children)
    elif item.category == 'vertical':
        section = item.get_parent()
        chapter = section.get_parent()
        # Position starts from 1, that's why we add 1.
        position = get_index(six.text_type(item.location), section.children) + 1
        item_dict['url'] = reverse('courseware_position', kwargs={
            'course_id': six.text_type(course.id),
            'chapter': chapter.url_name,
            'section': section.url_name,
            'position': position,
        })
    if item.category in ('chapter', 'sequential'):
        item_dict['children'] = [six.text_type(child) for child in item.children]

    return item_dict


def get_index(usage_key, children):
    """
    Returns an index of the child with `usage_key`.
    """
    children = [six.text_type(child) for child in children]
    return children.index(usage_key)


def construct_pagination_urls(request, course_id, api_next_url, api_previous_url):
    """
    Construct next and previous urls for LMS. `api_next_url` and `api_previous_url`
    are returned from notes api but we need to transform them according to LMS notes
    views by removing and replacing extra information.

    Arguments:
        request: HTTP request object
        course_id: course id
        api_next_url: notes api next url
        api_previous_url: notes api previous url

    Returns:
        next_url: lms notes next url
        previous_url: lms notes previous url
    """
    def lms_url(url):
        """
        Create lms url from api url.
        """
        if url is None:
            return None

        keys = ('page', 'page_size', 'text')
        parsed = urlparse(url)
        query_params = parse_qs(parsed.query)

        encoded_query_params = urlencode({key: query_params.get(key)[0] for key in keys if key in query_params})
        return "{}?{}".format(request.build_absolute_uri(base_url), encoded_query_params)

    base_url = reverse("notes", kwargs={"course_id": course_id})
    next_url = lms_url(api_next_url)
    previous_url = lms_url(api_previous_url)

    return next_url, previous_url


def get_notes(request, course, page=DEFAULT_PAGE, page_size=DEFAULT_PAGE_SIZE, text=None):
    """
    Returns paginated list of notes for the user.

    Arguments:
        request: HTTP request object
        course: Course descriptor
        page: requested or default page number
        page_size: requested or default page size
        text: text to search. If None then return all results for the current logged in user.

    Returns:
        Paginated dictionary with these key:
            start: start of the current page
            current_page: current page number
            next: url for next page
            previous: url for previous page
            count: total number of notes available for the sent query
            num_pages: number of pages available
            results: list with notes info dictionary. each item in this list will be a dict
    """
    path = 'search' if text else 'annotations'
    response = send_request(request.user, course.id, page, page_size, path, text)

    try:
        collection = json.loads(response.content.decode('utf-8'))
    except ValueError:
        log.error(u"Invalid JSON response received from notes api: response_content=%s", response.content)
        raise EdxNotesParseError(_("Invalid JSON response received from notes api."))

    # Verify response dict structure
    expected_keys = ['total', 'rows', 'num_pages', 'start', 'next', 'previous', 'current_page']
    keys = list(collection.keys())
    if not keys or not all(key in expected_keys for key in keys):
        log.error(u"Incorrect data received from notes api: collection_data=%s", str(collection))
        raise EdxNotesParseError(_("Incorrect data received from notes api."))

    filtered_results = preprocess_collection(request.user, course, collection['rows'])
    # Notes API is called from:
    # 1. The annotatorjs in courseware. It expects these attributes to be named "total" and "rows".
    # 2. The Notes tab Javascript proxied through LMS. It expects these attributes to be called "count" and "results".
    collection['count'] = collection['total']
    del collection['total']
    collection['results'] = filtered_results
    del collection['rows']

    collection['next'], collection['previous'] = construct_pagination_urls(
        request,
        course.id,
        collection['next'],
        collection['previous']
    )

    return collection


def get_endpoint(api_url, path=""):
    """
    Returns edx-notes-api endpoint.

    Arguments:
        api_url (str): base url to the notes api
        path (str): path to the resource
    Returns:
        str: full endpoint to the notes api
    """
    try:
        if not api_url.endswith("/"):
            api_url += "/"

        if path:
            if path.startswith("/"):
                path = path.lstrip("/")
            if not path.endswith("/"):
                path += "/"

        return api_url + path
    except (AttributeError, KeyError):
        raise ImproperlyConfigured(_("No endpoint was provided for EdxNotes."))


def get_public_endpoint(path=""):
    """Get the full path to a resource on the public notes API."""
    return get_endpoint(settings.EDXNOTES_PUBLIC_API, path)


def get_internal_endpoint(path=""):
    """Get the full path to a resource on the private notes API."""
    return get_endpoint(settings.EDXNOTES_INTERNAL_API, path)


def get_course_position(course_module):
    """
    Return the user's current place in the course.

    If this is the user's first time, leads to COURSE/CHAPTER/SECTION.
    If this isn't the users's first time, leads to COURSE/CHAPTER.

    If there is no current position in the course or chapter, then selects
    the first child.
    """
    urlargs = {'course_id': six.text_type(course_module.id)}
    chapter = get_current_child(course_module, min_depth=1)
    if chapter is None:
        log.debug("No chapter found when loading current position in course")
        return None

    urlargs['chapter'] = chapter.url_name
    if course_module.position is not None:
        return {
            'display_name': Text(chapter.display_name_with_default),
            'url': reverse('courseware_chapter', kwargs=urlargs),
        }

    # Relying on default of returning first child
    section = get_current_child(chapter, min_depth=1)
    if section is None:
        log.debug("No section found when loading current position in course")
        return None

    urlargs['section'] = section.url_name
    return {
        'display_name': Text(section.display_name_with_default),
        'url': reverse('courseware_section', kwargs=urlargs)
    }


def generate_uid():
    """
    Generates unique id.
    """
    return uuid4().int


def is_feature_enabled(course, user):
    """
    Returns True if Student Notes feature is enabled for the course, False otherwise.
    """
    return EdxNotesTab.is_enabled(course, user)
