"""
Views for the credit Django app.
"""
import json
import datetime
import logging

import dateutil
import pytz

from django.http import (
    HttpResponse,
    HttpResponseBadRequest,
    HttpResponseForbidden,
    Http404
)
from django.views.decorators.http import require_POST
from django.conf import settings

from opaque_keys.edx.keys import CourseKey
from opaque_keys import InvalidKeyError

from util.json_request import JsonResponse
from openedx.core.djangoapps.credit import api
from openedx.core.djangoapps.credit.signature import signature, get_shared_secret_key
from openedx.core.djangoapps.credit.exceptions import CreditApiBadRequest, CreditRequestNotFound


log = logging.getLogger(__name__)


@require_POST
def create_credit_request(request, provider_id):
    """
    Initiate a request for credit in a course.

    This end-point will get-or-create a record in the database to track
    the request.  It will then calculate the parameters to send to
    the credit provider and digitially sign the parameters, using a secret
    key shared with the credit provider.

    The user's browser is responsible for POSTing these parameters
    directly to the credit provider.

    **Example Usage:**

        POST /api/credit/v1/provider/hogwarts/request/
        {
            "username": "ron",
            "course_key": "edX/DemoX/Demo_Course"
        }

        Response: 200 OK
        Content-Type: application/json
        {
            "url": "http://example.com/request-credit",
            "method": "POST",
            "parameters": {
                request_uuid: "557168d0f7664fe59097106c67c3f847"
                timestamp: "2015-05-04T20:57:57.987119+00:00"
                course_org: "ASUx"
                course_num: "DemoX"
                course_run: "1T2015"
                final_grade: 0.95,
                user_username: "john",
                user_email: "john@example.com"
                user_full_name: "John Smith"
                user_mailing_address: "",
                user_country: "US",
                signature: "cRCNjkE4IzY+erIjRwOQCpRILgOvXx4q2qvx141BCqI="
            }
        }

    **Parameters:**

        * username (unicode): The username of the user requesting credit.

        * course_key (unicode): The identifier for the course for which the user
            is requesting credit.

    **Responses:**

        * 200 OK: The request was created successfully.  Returned content
            is a JSON-encoded dictionary describing what the client should
            send to the credit provider.

        * 400 Bad Request:
            - The provided course key did not correspond to a valid credit course.
            - The user already has a completed credit request for this course and provider.

        * 403 Not Authorized:
            - The username does not match the name of the logged in user.
            - The user is not eligible for credit in the course.

        * 404 Not Found:
            - The provider does not exist.

    """
    response, parameters = _validate_json_parameters(request.body, ["username", "course_key"])
    if response is not None:
        return response

    try:
        course_key = CourseKey.from_string(parameters["course_key"])
    except InvalidKeyError:
        return HttpResponseBadRequest(
            u'Could not parse "{course_key}" as a course key'.format(
                course_key=parameters["course_key"]
            )
        )

    # Check user authorization
    if not (request.user and request.user.username == parameters["username"]):
        log.warning(
            u'User with ID %s attempted to initiate a credit request for user with username "%s"',
            request.user.id if request.user else "[Anonymous]",
            parameters["username"]
        )
        return HttpResponseForbidden("Users are not allowed to initiate credit requests for other users.")

    # Initiate the request
    try:
        credit_request = api.create_credit_request(course_key, provider_id, parameters["username"])
    except CreditApiBadRequest as ex:
        return HttpResponseBadRequest(ex)
    else:
        return JsonResponse(credit_request)


@require_POST
def credit_provider_callback(request, provider_id):
    """
    Callback end-point used by credit providers to approve or reject
    a request for credit.

    **Example Usage:**

        POST /api/credit/v1/provider/{provider-id}/callback
        {
            "request_uuid": "557168d0f7664fe59097106c67c3f847",
            "status": "approved",
            "timestamp": "2015-05-04T20:57:57.987119+00:00",
            "signature": "cRCNjkE4IzY+erIjRwOQCpRILgOvXx4q2qvx141BCqI="
        }

        Response: 200 OK

    **Parameters:**

        * request_uuid (string): The UUID of the request.

        * status (string): Either "approved" or "rejected".

        * timestamp (string): The datetime at which the POST request was made, in ISO 8601 format.
            This will always include time-zone information.

        * signature (string): A digital signature of the request parameters,
            created using a secret key shared with the credit provider.

    **Responses:**

        * 200 OK: The user's status was updated successfully.

        * 400 Bad request: The provided parameters were not valid.
            Response content will be a JSON-encoded string describing the error.

        * 403 Forbidden: Signature was invalid or timestamp was too far in the past.

        * 404 Not Found: Could not find a request with the specified UUID associated with this provider.

    """
    response, parameters = _validate_json_parameters(request.body, [
        "request_uuid", "status", "timestamp", "signature"
    ])
    if response is not None:
        return response

    # Validate the digital signature of the request.
    # This ensures that the message came from the credit provider
    # and hasn't been tampered with.
    response = _validate_signature(parameters, provider_id)
    if response is not None:
        return response

    # Validate the timestamp to ensure that the request is timely.
    response = _validate_timestamp(parameters["timestamp"], provider_id)
    if response is not None:
        return response

    # Update the credit request status
    try:
        api.update_credit_request_status(parameters["request_uuid"], provider_id, parameters["status"])
    except CreditRequestNotFound:
        raise Http404
    except CreditApiBadRequest as ex:
        return HttpResponseBadRequest(ex)
    else:
        return HttpResponse()


def _validate_json_parameters(params_string, expected_parameters):
    """
    Load the request parameters as a JSON dictionary and check that
    all required paramters are present.

    Arguments:
        params_string (unicode): The JSON-encoded parameter dictionary.
        expected_parameters (list): Required keys of the parameters dictionary.

    Returns: tuple of (HttpResponse, dict)

    """
    try:
        parameters = json.loads(params_string)
    except (TypeError, ValueError):
        return HttpResponseBadRequest("Could not parse the request body as JSON."), None

    if not isinstance(parameters, dict):
        return HttpResponseBadRequest("Request parameters must be a JSON-encoded dictionary."), None

    missing_params = set(expected_parameters) - set(parameters.keys())
    if missing_params:
        msg = u"Required parameters are missing: {missing}".format(missing=u", ".join(missing_params))
        return HttpResponseBadRequest(msg), None

    return None, parameters


def _validate_signature(parameters, provider_id):
    """
    Check that the signature from the credit provider is valid.

    Arguments:
        parameters (dict): Parameters received from the credit provider.
        provider_id (unicode): Identifier for the credit provider.

    Returns:
        HttpResponseForbidden or None

    """
    secret_key = get_shared_secret_key(provider_id)
    if secret_key is None:
        log.error(
            (
                u'Could not retrieve secret key for credit provider with ID "%s".  '
                u'Since no key has been configured, we cannot validate requests from the credit provider.'
            ), provider_id
        )
        return HttpResponseForbidden("Credit provider credentials have not been configured.")

    if signature(parameters, secret_key) != parameters["signature"]:
        log.warning(u'Request from credit provider with ID "%s" had an invalid signature', parameters["signature"])
        return HttpResponseForbidden("Invalid signature.")


def _validate_timestamp(timestamp_str, provider_id):
    """
    Check that the timestamp of the request is recent.

    Arguments:
        timestamp_str (str): ISO-8601 datetime formatted string.
        provider_id (unicode): Identifier for the credit provider.

    Returns:
        HttpResponse or None

    """
    # If we can't parse the datetime string, reject the request.
    try:
        # dateutil's parser has some counter-intuitive behavior:
        # for example, given an empty string or "a" it always returns the current datetime.
        # It is the responsibility of the credit provider to send a valid ISO-8601 datetime
        # so we can validate it; otherwise, this check might not take effect.
        # (Note that the signature check ensures that the timestamp we receive hasn't
        # been tampered with after being issued by the credit provider).
        timestamp = dateutil.parser.parse(timestamp_str)
    except ValueError:
        msg = u'"{timestamp}" is not an ISO-8601 formatted datetime'.format(timestamp=timestamp_str)
        log.warning(msg)
        return HttpResponseBadRequest(msg)

    # Check that the timestamp is recent
    elapsed_seconds = (datetime.datetime.now(pytz.UTC) - timestamp).total_seconds()
    if elapsed_seconds > settings.CREDIT_PROVIDER_TIMESTAMP_EXPIRATION:
        log.warning(
            (
                u'Timestamp %s is too far in the past (%s seconds), '
                u'so we are rejecting the notification from the credit provider "%s".'
            ),
            timestamp_str, elapsed_seconds, provider_id,
        )
        return HttpResponseForbidden(u"Timestamp is too far in the past.")
