### Implementation of support for the Cybersource Credit card processor
### The name of this file should be used as the key of the dict in the CC_PROCESSOR setting
### Implementes interface as specified by __init__.py

import time
import hmac
import binascii
import re
import json
from collections import OrderedDict, defaultdict
from decimal import Decimal, InvalidOperation
from hashlib import sha1
from textwrap import dedent
from django.conf import settings
from django.utils.translation import ugettext as _
from mitxmako.shortcuts import render_to_string
from shoppingcart.models import Order
from shoppingcart.processors.exceptions import *


def process_postpay_callback(params):
    """
    The top level call to this module, basically
    This function is handed the callback request after the customer has entered the CC info and clicked "buy"
    on the external Hosted Order Page.
    It is expected to verify the callback and determine if the payment was successful.
    It returns {'success':bool, 'order':Order, 'error_html':str}
    If successful this function must have the side effect of marking the order purchased and calling the
    purchased_callbacks of the cart items.
    If unsuccessful this function should not have those side effects but should try to figure out why and
    return a helpful-enough error message in error_html.
    """
    try:
        verify_signatures(params)
        result = payment_accepted(params)
        if result['accepted']:
            # SUCCESS CASE first, rest are some sort of oddity
            record_purchase(params, result['order'])
            return {'success': True,
                    'order': result['order'],
                    'error_html': ''}
        else:
            return {'success': False,
                    'order': result['order'],
                    'error_html': get_processor_decline_html(params)}
    except CCProcessorException as error:
        return {'success': False,
                'order': None,  # due to exception we may not have the order
                'error_html': get_processor_exception_html(error)}


def processor_hash(value):
    """
    Performs the base64(HMAC_SHA1(key, value)) used by CyberSource Hosted Order Page
    """
    shared_secret = settings.CC_PROCESSOR['CyberSource'].get('SHARED_SECRET', '')
    hash_obj = hmac.new(shared_secret.encode('utf-8'), value.encode('utf-8'), sha1)
    return binascii.b2a_base64(hash_obj.digest())[:-1]  # last character is a '\n', which we don't want


def sign(params, signed_fields_key='orderPage_signedFields', full_sig_key='orderPage_signaturePublic'):
    """
    params needs to be an ordered dict, b/c cybersource documentation states that order is important.
    Reverse engineered from PHP version provided by cybersource
    """
    merchant_id = settings.CC_PROCESSOR['CyberSource'].get('MERCHANT_ID', '')
    order_page_version = settings.CC_PROCESSOR['CyberSource'].get('ORDERPAGE_VERSION', '7')
    serial_number = settings.CC_PROCESSOR['CyberSource'].get('SERIAL_NUMBER', '')

    params['merchantID'] = merchant_id
    params['orderPage_timestamp'] = int(time.time() * 1000)
    params['orderPage_version'] = order_page_version
    params['orderPage_serialNumber'] = serial_number
    fields = u",".join(params.keys())
    values = u",".join([u"{0}={1}".format(i, params[i]) for i in params.keys()])
    fields_sig = processor_hash(fields)
    values += u",signedFieldsPublicSignature=" + fields_sig
    params[full_sig_key] = processor_hash(values)
    params[signed_fields_key] = fields

    return params


def verify_signatures(params, signed_fields_key='signedFields', full_sig_key='signedDataPublicSignature'):
    """
    Verify the signatures accompanying the POST back from Cybersource Hosted Order Page

    returns silently if verified

    raises CCProcessorSignatureException if not verified
    """
    signed_fields = params.get(signed_fields_key, '').split(',')
    data = u",".join([u"{0}={1}".format(k, params.get(k, '')) for k in signed_fields])
    signed_fields_sig = processor_hash(params.get(signed_fields_key, ''))
    data += u",signedFieldsPublicSignature=" + signed_fields_sig
    returned_sig = params.get(full_sig_key, '')
    if processor_hash(data) != returned_sig:
        raise CCProcessorSignatureException()


def render_purchase_form_html(cart):
    """
    Renders the HTML of the hidden POST form that must be used to initiate a purchase with CyberSource
    """
    return render_to_string('shoppingcart/cybersource_form.html', {
        'action': get_purchase_endpoint(),
        'params': get_signed_purchase_params(cart),
    })

def get_signed_purchase_params(cart):
    return sign(get_purchase_params(cart))

def get_purchase_params(cart):
    total_cost = cart.total_cost
    amount = "{0:0.2f}".format(total_cost)
    cart_items = cart.orderitem_set.all()
    params = OrderedDict()
    params['amount'] = amount
    params['currency'] = cart.currency
    params['orderPage_transactionType'] = 'sale'
    params['orderNumber'] = "{0:d}".format(cart.id)

    return params

def get_purchase_endpoint():
    return settings.CC_PROCESSOR['CyberSource'].get('PURCHASE_ENDPOINT', '')

def payment_accepted(params):
    """
    Check that cybersource has accepted the payment
    params: a dictionary of POST parameters returned by CyberSource in their post-payment callback

    returns: true if the payment was correctly accepted, for the right amount
             false if the payment was not accepted

    raises: CCProcessorDataException if the returned message did not provide required parameters
            CCProcessorWrongAmountException if the amount charged is different than the order amount

    """
    #make sure required keys are present and convert their values to the right type
    valid_params = {}
    for key, key_type in [('orderNumber', int),
                          ('orderCurrency', str),
                          ('decisio
n', str)]:
        if key not in params:
            raise CCProcessorDataException(
                _("The payment processor did not return a required parameter: {0}".format(key))
            )
        try:
            valid_params[key] = key_type(params[k
ey])
        except ValueError:
            raise CCProcessorDataException(
                _("The payment processor returned a badly-typed value {0} for param {1}.".format(params[key], key))
            )

    try:
        order = Order.objects.get(id=valid_params['orderNumber'])
    except Order.DoesNotExist:
        raise CCProcessorDataException(_("The payment processor accepted an order whose number is not in our system."))

    if valid_params['decision'] == 'ACCEPT':
        try:
            # Moved reading of charged_amount here from the valid_params loop above because
            # only 'ACCEPT' messages have a 'ccAuthReply_amount' parameter
            charged_amt = Decimal(params['ccAuthReply_amount'])
        except InvalidOperation:
            raise CCProcessorDataException(
                _("The payment processor returned a badly-typed value {0} for param {1}.".format(
                    params['ccAuthReply_amount'], 'ccAuthReply_amount'))
            )

        if charged_amt == order.total_cost and valid_params['orderCurrency'] == order.currency:
            return {'accepted': True,
                    'amt_charged': charged_amt,
                    'currency': valid_params['orderCurrency'],
                    'order': order}
        else:
            raise CCProcessorWrongAmountException(
                _("The amount charged by the processor {0} {1} is different than the total cost of the order {2} {3}."
                    .format(charged_amt, valid_params['orderCurrency'],
                            order.total_cost, order.currency))
            )
    else:
        return {'accepted': False,
                'amt_charged': 0,
                'currency': 'usd',
                'order': order}


def record_purchase(params, order):
    """
    Record the purchase and run purchased_callbacks
    """
    ccnum_str = params.get('card_accountNumber', '')
    m = re.search("\d", ccnum_str)
    if m:
        c