"""
Integration with Software Secure's proctoring system
"""

from __future__ import absolute_import

import base64
import binascii
import datetime
from hashlib import sha256
import hmac
import json
import logging
import unicodedata
import six

import requests

from django.conf import settings
from django.urls import reverse

from Cryptodome.Cipher import DES3

from edx_proctoring.backends.backend import ProctoringBackendProvider
from edx_proctoring import constants
from edx_proctoring.exceptions import (
    BackendProviderCannotRegisterAttempt,
    ProctoredExamSuspiciousLookup,
)
from edx_proctoring.statuses import SoftwareSecureReviewStatus

log = logging.getLogger(__name__)


SOFTWARE_SECURE_INVALID_CHARS = u'[]<>#:|!?/\'"*\\'


class SoftwareSecureBackendProvider(ProctoringBackendProvider):
    """
    Implementation of the ProctoringBackendProvider for Software Secure's
    RPNow product
    """
    verbose_name = u'RPNow'
    passing_statuses = SoftwareSecureReviewStatus.passing_statuses

    def __init__(self, organization, exam_sponsor, exam_register_endpoint,
                 secret_key_id, secret_key, crypto_key, software_download_url,
                 send_email=False):
        """
        Class initializer
        """
        # pylint: disable=no-member
        super(SoftwareSecureBackendProvider, self).__init__()
        self.organization = organization
        self.exam_sponsor = exam_sponsor
        self.exam_register_endpoint = exam_register_endpoint
        self.secret_key_id = secret_key_id
        self.secret_key = secret_key
        self.crypto_key = crypto_key
        self.timeout = 10
        self.software_download_url = software_download_url
        self.send_email = send_email

    def register_exam_attempt(self, exam, context):
        """
        Method that is responsible for communicating with the backend provider
        to establish a new proctored exam
        """

        attempt_code = context['attempt_code']

        data = self._get_payload(
            exam,
            context
        )

        headers = {
            "Content-Type": 'application/json'
        }
        http_date = datetime.datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S GMT")
        signature = self._sign_doc(data, 'POST', headers, http_date)

        status, response = self._send_request_to_ssi(data, signature, http_date)

        if status not in [200, 201]:
            err_msg = (
                u'Could not register attempt_code = {attempt_code}. '
                'HTTP Status code was {status_code} and response was {response}.'.format(
                    attempt_code=attempt_code,
                    status_code=status,
                    response=response
                )
            )
            log.error(err_msg)
            raise BackendProviderCannotRegisterAttempt(err_msg)

        # get the external ID that Software Secure has defined
        # for this attempt
        ssi_record_locator = json.loads(response)['ssiRecordLocator']

        return ssi_record_locator

    def start_exam_attempt(self, exam, attempt):  # pylint: disable=unused-argument
        """
        Called when the exam attempt has been created but not started
        """
        return None

    def stop_exam_attempt(self, exam, attempt):
        """
        Method that is responsible for communicating with the backend provider
        to establish a new proctored exam
        """
        return None

    def mark_erroneous_exam_attempt(self, exam, attempt):
        """
        Method that would be responsible for communicating with the
        backend provider to mark a proctored session as having
        encountered a technical error
        """
        return None

    def get_software_download_url(self):
        """
        Returns the URL that the user needs to go to in order to download
        the corresponding desktop software
        """
        return self.software_download_url

    def on_review_callback(self, attempt, payload):
        """
        Called when the reviewing 3rd party service posts back the results

        Documentation on the data format can be found from SoftwareSecure's
        documentation named "Reviewer Data Transfer"
        """
        received_id = payload['examMetaData']['ssiRecordLocator'].lower()
        match = (
            attempt['external_id'].lower() == received_id.lower() or
            settings.PROCTORING_SETTINGS.get('ALLOW_CALLBACK_SIMULATION', False)
        )
        if not match:
            err_msg = (
                'Found attempt_code {attempt_code}, but the recorded external_id did not '
                'match the ssiRecordLocator that had been recorded previously. Has {existing} '
                'but received {received}!'.format(
                    attempt_code=attempt['attempt_code'],
                    existing=attempt['external_id'],
                    received=received_id
                )
            )
            raise ProctoredExamSuspiciousLookup(err_msg)

        # redact the videoReviewLink from the payload
        if 'videoReviewLink' in payload:
            del payload['videoReviewLink']

        log_msg = (
            'Received callback from SoftwareSecure with review data: {payload}'.format(
                payload=payload
            )
        )
        log.info(log_msg)
        SoftwareSecureReviewStatus.validate(payload['reviewStatus'])
        review_status = SoftwareSecureReviewStatus.to_standard_status.get(payload['reviewStatus'], None)

        comments = []
        for comment in payload.get('webCamComments', []) + payload.get('desktopComments', []):
            comments.append({
                'start': comment['eventStart'],
                'stop': comment['eventFinish'],
                'duration': comment['duration'],
                'comment': comment['comments'],
                'status': comment['eventStatus']
                })

        converted = {
            'status': review_status,
            'comments': comments,
            'payload': payload,
            'reviewed_by': None,
        }
        return converted

    def on_exam_saved(self, exam):
        """
        Called after an exam is saved.
        """

    def _encrypt_password(self, key, pwd):
        """
        Encrypt the exam passwork with the given key
        """
        block_size = DES3.block_size

        def pad(text):
            """
            Apply padding
            """
            return (text + (block_size - len(text) % block_size) *
                    chr(block_size - len(text) % block_size)).encode('utf-8')
        cipher = DES3.new(key, DES3.MODE_ECB)
        encrypted_text = cipher.encrypt(pad(pwd))
        return base64.b64encode(encrypted_text).decode('ascii')

    def _split_fullname(self, full_name):
        """
        Utility to break Full Name to first and last name
        """
        first_name = ''
        last_name = ''
        name_elements = full_name.split(' ')
        first_name = name_elements[0]
        if len(name_elements) > 1:
            last_name = ' '.join(name_elements[1:])

        return (first_name, last_name)

    def _get_payload(self, exam, context):
        """
        Constructs the data payload that Software Secure expects
        """

        attempt_code = context['attempt_code']
        time_limit_mins = context['time_limit_mins']
        is_sample_attempt = context['is_sample_attempt']
        full_name = context['full_name']
        review_policy = context.get('review_policy', constants.DEFAULT_SOFTWARE_SECURE_REVIEW_POLICY)
        review_policy_exception = context.get('review_policy_exception')
        scheme = 'https' if getattr(settings, 'HTTPS', 'on') == 'on' else 'http'
        callback_url = '{scheme}://{hostname}{path}'.format(
            scheme=scheme,
            hostname=settings.SITE_NAME,
            path=reverse(
                'edx_proctoring:anonymous.proctoring_launch_callback.start_exam',
                args=[attempt_code]
            )
        )

        # compile the notes to the reviewer
        # this is a combination of the Exam Policy which is for all students
        # combined with any exceptions granted to the particular student
        reviewer_notes = review_policy
        if review_policy_exception:
            reviewer_notes = '{notes}; {exception}'.format(
                notes=reviewer_notes,
                exception=review_policy_exception
            )

        (first_name, last_name) = self._split_fullname(full_name)

        now = datetime.datetime.utcnow()
        start_time_str = now.strftime("%a, %d %b %Y %H:%M:%S GMT")
        end_time_str = (now + datetime.timedelta(minutes=time_limit_mins)).strftime("%a, %d %b %Y %H:%M:%S GMT")
        # remove all illegal characters from the exam name
        exam_name = exam['exam_name']
        exam_name = unicodedata.normalize('NFKD', exam_name).encode('ascii', 'ignore').decode('utf8')

        for character in SOFTWARE_SECURE_INVALID_CHARS:
            exam_name = exam_name.replace(character, '_')

        # if exam_name is blank because we can't normalize a potential unicode (like Chinese) exam name
        # into something ascii-like, then we have use a default otherwise
        # SoftwareSecure will fail on the exam registration API call
        if not exam_name:
            exam_name = u'Proctored Exam'

        org_extra = {
            "examStartDate": start_time_str,
            "examEndDate": end_time_str,
            "noOfStudents": 1,
            "examID": exam['id'],
            "courseID": exam['course_id'],
            "firstName": first_name,
            "lastName": last_name
        }
        if self.send_email:
            org_extra["email"] = context['email']

        return {
            "examCode": attempt_code,
            "organization": self.organization,
            "duration": time_limit_mins,
            "reviewedExam": not is_sample_attempt,
            # NOTE: we will have to allow these notes to be authorable in Studio
            # and then we will pull this from the exam database model
            "reviewerNotes": reviewer_notes,
            "examPassword": self._encrypt_password(self.crypto_key, attempt_code),
            "examSponsor": self.exam_sponsor,
            "examName": exam_name,
            "ssiProduct": 'rp-now',
            # need to pass in a URL to the LMS?
            "examUrl": callback_url,
            "orgExtra": org_extra
        }

    def _header_string(self, headers, date):
        """
        Composes the HTTP header string that SoftwareSecure expects
        """
        # Headers
        string = ""
        if 'Content-Type' in headers:
            string += headers.get('Content-Type')
            string += '\n'

        if date:
            string += date
            string += '\n'

        return string.encode('utf8')

    def _body_string(self, body_json, prefix=b""):
        """
        Serializes out the HTTP body that SoftwareSecure expects
        """
        keys = list(body_json.keys())
        keys.sort()
        string = b""
        for key in keys:
            value = body_json[key]
            if isinstance(value, bool):
                if value:
                    value = b'true'
                else:
                    value = b'false'
            key = key.encode('utf8')
            if isinstance(value, (list, tuple)):
                for idx, arr in enumerate(value):
                    pfx = b'%s.%d' % (key, idx)
                    if isinstance(arr, dict):
                        string += self._body_string(arr, pfx + b'.')
                    else:
                        string += b'%s:%s\n' % (pfx, six.text_type(arr).encode('utf8'))
            elif isinstance(value, dict):
                string += self._body_string(value, key + b'.')
            else:
                if value != "" and not value:
                    value = "null"
                string += b'%s%s:%s\n' % (prefix, key, six.text_type(value).encode('utf8'))

        return string

    def _sign_doc(self, body_json, method, headers, date):
        """
        Digitaly signs the datapayload that SoftwareSecure expects
        """
        body_str = self._body_string(body_json)

        method_string = method.encode('ascii') + b'\n\n'

        headers_str = self._header_string(headers, date)
        message = method_string + headers_str + body_str

        log_msg = (
            'About to send payload to SoftwareSecure: examCode: {examCode}, courseID: {courseID}'.
            format(examCode=body_json.get('examCode'), courseID=body_json.get('orgExtra').get('courseID'))
        )
        log.info(log_msg)

        hashed = hmac.new(self.secret_key.encode('ascii'), message, sha256)
        computed = binascii.b2a_base64(hashed.digest()).rstrip(b'\n')

        return b'SSI %s:%s' % (self.secret_key_id.encode('ascii'), computed)

    def _send_request_to_ssi(self, data, sig, date):
        """
        Performs the webservice call to SoftwareSecure
        """
        response = requests.post(
            self.exam_register_endpoint,
            headers={
                'Content-Type': 'application/json',
                "Authorization": sig,
                "Date": date
            },
            data=json.dumps(data),
            timeout=self.timeout
        )

        return response.status_code, response.text
