"""This module contains the saba api client"""

import json
import logging

from requests import api
from requests.exceptions import HTTPError

from django.utils.timezone import now

from constance import config

from .errors import SabaErrorMixin


logger = logging.getLogger('saba.client')  # pylint: disable=invalid-name


class SabaApiException(SabaErrorMixin, Exception):
    """Exception used in the ``SabaApiClient`` class

    Attributes
    ----------
    http_status: int
        The response status code
    error_message: str
        The Saba error message

    """

    def __init__(self, http_status, error_message, *args, **kwargs):
        self.http_status = http_status
        self.error_message = error_message

        super().__init__(*args, **kwargs)

    def __str__(self):
        return 'http status: %(http_status)s - %(error_message)s' % {
            'http_status': self.http_status,
            'error_message': self.error_message,
        }


class SabaApiClient:
    """SABA API client"""

    DELIVERY_TYPES = {
        'exempt': 'eqcat000000000001671'
    }

    def __init__(self, api_base_url, username, password):
        """Create a Saba client API object.

        Build the base url prefix and get the auth certificate to use.

        Parameters
        ----------
        api_base_url: str
            The base url of the api (ex:https://nissanacademysb-api.sabacloud.com)
        username: str
            The Saba username to authenticate.
        password: str
            The password for that username.

        """

        self._session = api
        self.api_base_url = api_base_url
        self._certificate = self._get_certificate(username, password)

    def _internal_call(self, method, url, headers, params=None, payload=None):
        """Call the specified endpoint of the api

        Parameters
        ----------
        method: str
            HTTP method to use ("GET", "POST", etc)
        url: str
            Endpoint to call ("/v1/login", etc)
        headers: dict
            The request's headers
        params: dict
            The request's params
        payload: dict
            The request's payload

        Returns
        -------
        json
            The json response object

        Raises
        ------
        SabaApiException
            If the response got a wrong status code

        """

        args = dict(params=params)
        url = self.api_base_url + url

        if payload:
            # If we got a payload (json), update the data and the headers accordingly
            args["data"] = json.dumps(payload)
            headers['Content-type'] = 'application/json'

        response = self._session.request(method, url, headers=headers, **args)
        logger.debug('REQUEST: [%s]%s %s', method, url, payload)

        try:
            response.raise_for_status()
        except HTTPError:

            # Do we have an error message ?
            try:
                error_message = response.json()['errorMessage']
            except (ValueError, IndexError):
                error_message = 'Empty error message'

            # Raise a nice exception with the response from Saba
            raise SabaApiException(
                response.status_code,
                error_message,
            )

        if len(response.text) > 0:
            results = response.json()

            logger.debug('RESPONSE: %s', results)

            return results
        else:
            return None

    def _authenticated_call(self, method, url, params=None, payload=None):
        """Call the specified endpoint of the api with an authenticated call

        Parameters
        ----------
        method: str
            HTTP method to use (ex: "GET", "POST", etc)
        url: str
            Endpoint to call (ex: "/v1/login")
        params: dict
            The request's params
        payload: dict
            The request's payload

        """

        headers = {
            "SabaCertificate": self._certificate,
        }

        return self._internal_call(method, url, headers, params, payload)

    def _get_certificate(self, username, password):
        """Get the certificate to authenticate our requests amongst the SABA Api

        This method is called when instantiating a new ``SabaApiClient``.

        Parameters
        ----------
        username: str
            The Saba user's username.
        password: str
            The Saba user's password.

        Returns
        -------
        str
            The Saba certificate

        """

        headers = {
            'user': username,
            'password': password
        }

        return self._internal_call('GET', '/v1/login', headers)['certificate']

    def user_details(self, username, fields, user_type='external'):
        """Get the details of the user with the given username

        Parameters
        ----------
        username: str
            The user's username
        fields: list
            The fields of the user's profile we intend to extract
        user_type: str
            The type of user, can be 'external' or 'internal', default to 'external'

        Examples
        --------
        >>> saba_client.user_details('SED3049', ['id'])
        {'id': 'persn000000000018835'}

        """

        return self._authenticated_call(
            'GET',
            '/v1/people/username=%(username)s:(%(fields)s)?type=%(user_type)s' % {
                'username': username,
                'fields': ','.join(fields),
                'user_type': user_type
            }
        )

    def get_enrollment(self, username, course_id, date):
        """Get an enrollment for given user, course and date

        Parameters
        ----------
        username: str
            The user's username
        course_id: str
            The course of the class
        date: str
            The expected date of the class of this enrollment, eg 2016-05-30

        Notes
        -----
        - A ``Course`` is like our ``Training``.
        - A ``Class`` is like our ``TrainingSession``.
        - An ``Enrollment`` is an inscription of a user in a ``Class``.

        Raises
        ------
        SabaApiException
            If no enrollments or multiple enrollments was found for the given criteria

        Returns
        -------
        json
            A json representation of an enrollment

        Examples
        --------
        >>> d = date(2015, 1, 12)
        >>> saba_client.get_enrollment('persn000000000018835', 'cours000000000005983', d)
        {'Current_Count': None,
         'Max_Count': None,
         'class_id': {'displayName': 'ASSESSMENTS_2016_0027',
          'id': 'class000000000025819'},
         'delivery_id': {'displayName': 'Instructor-Led',
          'id': 'eqcat000000000000004'},
         'elements_to_complete': 0,
         'end_date': '2016-01-19',
         'id': 'regdw000000000343961',
         'is_no_show': False,
         'is_walk_in': False,
         'offering_action_id': {'displayName': '00804061',
          'id': 'ofapr000000000832402'},
         'offering_temp_id': {'displayName': 'NE Sales Assessment',
          'id': 'cours000000000005983'},
         'offering_temp_name': 'NE Sales Assessment',
         'offering_temp_no': '2015-11-sale-assess',
         'offering_temp_version': '',
         'reg_no': '00313581',
         'start_date': '2016-01-19',
         'status': '100',
         'status_description': 'Registered',
         'student_email': '',
         'student_id': {'displayName': 'Theodoros Adamidis',
          'id': 'persn000000000018835'},
         'student_number': 'SED3049',
         'student_username': 'SED3049',
         'wlist_on': None,
         'wlist_priority': 5}

        """

        response = self._authenticated_call(
            'GET',
            '/v1/learning/enroll/search',
            {
                'q': '(student_id==%s,offering_temp_no==%s,start_date==%s)' % (
                    username, course_id, date
                ),
                'includeDetails': 'true'
            }
        )

        enrollments = response['results']

        # We should have only one enrollment
        if not enrollments:
            raise SabaApiException(
                404,
                'Could not find enrollment for user %s / course %s taking on %s' % (
                    username, course_id, date
                ),
                user_message='The user is not registered to a class in NAO. \
                    Register the person to the class',
            )
        if len(enrollments) > 1:
            raise SabaApiException(
                404,
                'Found Multiple enrollments for user %s / course %s taking on %s' % (
                    username, course_id, date
                ),
                user_message='The user is registered to multiple classes in NAO. \
                    Make sure your user is registered to a single class in NAO',
            )

        return enrollments[0]

    def get_enrollment_details(self, enrollment_id):
        """Get an enrollment details from it's SABA internal id

        Parameters
        ----------
        enrollment_id: str
            The SABA internal id of the enrollement, eg. regdw000000000368483,
            as returned by ``get_enrollment``.

        Returns
        -------
        dict:
            The details of the enrollment.

        """

        return self._authenticated_call(
            'GET',
            '/v1/enrollments/%s/sections:regdetail' % enrollment_id,
        )

    def mark_attendance(self, user_id, offering_id):
        """Mark the attendance of a learner

        Parameters
        ----------
        user_id: str
            The SABA user id we want to mark the attendance
        offering_id: str
            The SABA offering id we want to mark the user attendance for

        """

        self._authenticated_call(
            'POST',
            '/v1/attendance',
            payload={
                'offering': {'id': offering_id},
                'learner': {'id': user_id},
                'session_no': 1,
            }
        )

    def complete_enrollment(self, registration_id, status, score):
        """Mark an enrollment complete

        Parameters
        ----------
        registration_id: str
            The registration id
        status: int
            The status of the trainee regarding this enrollment
            Must be one of the following: 200 (Successful), 400 (Unsuccessful)
        score: int
            The score obtained by the trainee during this enrollment

        """

        self._authenticated_call(
            'PUT',
            '/v1/learning/enroll/%s/status' % registration_id,
            payload={
                '@type': 'com.saba.learning.services.registration.OfferingCompletionInfo',
                'status': status,
                'score': score
            }
        )

    def create_transcript(self, user_id, course_id, delivery_type):
        """Create a new transcript

        Parameters
        ----------
        user_id: str
            The SABA user id of this new transcript
        course_id: str
            The SABA course id of this new transcript
        delivery_type: str, one of the ``DELIVERY_TYPE``
            The delivery type of the new transcript

        Returns
        -------
        json
            The SABA decoded response body

        """

        response = self._authenticated_call(
            'POST',
            '/v1/transcript/',
            payload={
                '@type': 'com.saba.offering.adhoclearning.AdHocLearningTranscriptDetail',
                'deliveryType': self.DELIVERY_TYPES[delivery_type],
                'courseId': course_id,
                'deliveredBy': config.SABA_API_USERNAME_INTERNAL_ID,
                'learners': [
                    'java.util.ArrayList',
                    [
                        {
                            '@type': 'com.saba.offering.adhoclearning.LearnerInfo',
                            'learnerId': user_id,
                            'grade': None,
                            'score': [
                                'java.math.BigDecimal',
                                0
                            ],
                            'completedOnDate': {
                                '@type': 'date',
                                'time': str(now().date())
                            },
                            'addedToProfileOn': {
                                '@type': "date",
                                'time': str(now().date())
                            }
                        }
                    ]
                ]
            }
        )

        return response

    def _get_paginated_course_activities(self, user_id, page):
        """Get activities for a given user and a page index

        Parameters
        ----------
        user_id: str
            The user's id in saba
        page: int
            The page to grab

        Returns
        -------
        tuple(bool, list)
            A tuple with two entries:
            has_more_results: bool
                True if another page exists after the one that has been request
            results: list
                The list of course activities

        """

        response = self._authenticated_call(
            'GET',
            '/v1/courseactivity',
            {
                'q': '(assignee==%s)' % user_id,
                'startPage': page,
                'includeDetails': 'true'
            }
        )

        return response['hasMoreResults'], response['results']

    def get_course_activities(self, user_id, course_id):
        """Get all activities for the given user and the given course

        Parameters
        ----------
        user_id: str
            The user's id in saba
        course_id: str
            The course of the activities

        Yields
        ------
        dict
            A dict representation of a course activity

        """

        page_index = 0
        has_more_results = True

        while has_more_results:
            has_more_results, course_activities = self._get_paginated_course_activities(
                user_id,
                page_index
            )
            for course_activity in course_activities:
                if course_activity.get('activityDefinition')['id'] == course_id:
                    yield course_activity
            page_index += 1

    def delete_course_activity(self, activity_id):
        """Delete a course activity

        Parameters
        ----------
        activity_id: str
            The course activity's SABA id we want to delete

        """

        self._authenticated_call(
            'DELETE',
            '/v1/courseactivity/%s' % activity_id
        )

    def course_activity(self, source_user_id, assignee_user_id, course_id):
        """Assign the given course to the given user

        Parameters
        ----------
        source_user_id: str
            The id of the Saba user the assignment come from
        assignee_user_id: str
            The id of the Saba user to assign to
        course_id: str
            The id of the Saba course to assign

        """

        self._authenticated_call(
            'POST',
            '/v1/courseactivity/',
            payload={
                'source': {
                    'id': source_user_id
                },
                'assignee': {
                    'id': assignee_user_id
                },
                'activityDefinition': {
                    'id': course_id
                }
            }
        )

    def get_course_by_internal_id(self, course_internal_id):
        """Get the details of a course by internal_id

        Parameters
        ----------
        course_internal_id: str
            The course_internal_id of the Saba course to fetch
                eg. "cours000000000007782"

        """

        response = self._authenticated_call(
            'GET',
            '/v1/course/%s' % course_internal_id
        )

        if response.get('id') != course_internal_id:
            raise SabaApiException(
                500,
                'Bad response for course internal_id %s (id not found or mismatch)'
                % course_internal_id,
                user_message='Wrong course details. Please contact Magency support.',
            )

        return response

    def get_course_details(self, course_external_id):
        """Get the details of a course by external_id

        It has a basic support for versionning: if multiple courses are returned
        for one ``course_external_id``, it will attempt to return the latest version
        using ``self.get_course_by_internal_id``.

        Parameters
        ----------
        course_id: str
            The course_external_id of the Saba course to fetch
                eg. "2015-11-SALE-ASSESS"

        Notes
        -----
        The version attribute returned by ``get_course_by_internal_id`` is a string or None.

        """

        def version_sorter(element):
            """Helper to sort by a string attr named version that can be None

            Parameters
            ----------
            element : dict
                One of the element of the list to be sorted, should contain a string
                or None attribute ``version``.

            """

            return element['version'] if element.get('version') else ''

        response = self._authenticated_call(
            'GET',
            '/v1/course/?q=(course_no==%(course_external_id)s)' % {
                'course_external_id': course_external_id
            },
        )

        courses = response['results']

        if not courses:
            raise SabaApiException(
                404,
                'Could not find course for external_id %s' % course_external_id,
                user_message='The course id is not recognized in NAO. \
                    Please contact Magency support.',
            )

        if len(courses) > 1:
            # build a list of courses augmented w/ version "number"
            courses_w_version = []
            for course in courses:
                course_id = course.get('id')
                if not course_id:
                    raise SabaApiException(
                        500,
                        'No ``course.id`` found for course %s' % course,
                        user_message='No course id found. Please contact Magency support.',
                    )
                course_details = self.get_course_by_internal_id(course_id)
                courses_w_version.append({
                    'course': course,
                    'version': course_details.get('version')
                })
            # sort it by version (desc) and choose the first one
            sorted_courses = sorted(courses_w_version, key=version_sorter, reverse=True)
            last_version_course = sorted_courses[0]['course']
        else:
            last_version_course = courses[0]

        return last_version_course

    def get_transcripts(self, username, course_id=None):
        """Get the transcripts matching the given username and course

        Parameters
        ----------
        username: str
            The user's username
        course_id: str
            The course of the class, optional

        Returns
        -------
        list
            A list of transcripts

        """

        params = {'includeDetails': 'true'}

        if course_id:
            params['q'] = '(course_no==%s)' % course_id

        response = self._authenticated_call(
            'GET',
            '/v1/people/%s/transcripts/search' % username,
            params
        )

        return response['results']

    def delete_transcript(self, transcript_id):
        """Delete a given transcript

        Parameters
        ----------
        transcript_id: str
            The transcript's SABA id we want to delete

        """

        self._authenticated_call(
            'DELETE',
            '/v1/transcripts/%s' % transcript_id
        )
