# eopayment - online payment library
# Copyright (C) 2011-2020 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import hashlib
import uuid
from decimal import ROUND_HALF_UP, Decimal
from urllib import parse as urlparse

from .common import (
    ACCEPTED,
    CANCELLED,
    DENIED,
    ERROR,
    FORM,
    ORDERID_TRANSACTION_SEPARATOR,
    PAID,
    WAITING,
    Form,
    PaymentCommon,
    PaymentResponse,
    ResponseError,
    _,
    force_byte,
    force_text,
)

ENVIRONMENT_TEST = 'TEST'
ENVIRONMENT_TEST_URL = 'https://secure.ogone.com/ncol/test/orderstandard.asp'
ENVIRONMENT_PROD = 'PROD'
ENVIRONMENT_PROD_URL = 'https://secure.ogone.com/ncol/prod/orderstandard.asp'
ENVIRONMENT = [ENVIRONMENT_TEST, ENVIRONMENT_PROD]

# https://payment-services.ingenico.com/ogone/support/~/media/kdb/integration%20guides/sha-in_params.ashx
SHA_IN_PARAMS = """
ACCEPTANCE
ACCEPTURL
ADDMATCH
ADDRMATCH
AIACTIONNUMBER
AIAGIATA
AIAIRNAME
AIAIRTAX
AIBOOKIND*XX*
AICARRIER*XX*
AICHDET
AICLASS*XX*
AICONJTI
AIDEPTCODE
AIDESTCITY*XX*
AIDESTCITYL*XX*
AIEXTRAPASNAME*XX*
AIEYCD
AIFLDATE*XX*
AIFLNUM*XX*
AIGLNUM
AIINVOICE
AIIRST
AIORCITY*XX*
AIORCITYL*XX*
AIPASNAME
AIPROJNUM
AISTOPOV*XX*
AITIDATE
AITINUM
AITINUML*XX*
AITYPCH
AIVATAMNT
AIVATAPPL
ALIAS
ALIASOPERATION
ALIASPERSISTEDAFTERUSE
ALIASUSAGE
ALLOWCORRECTION
AMOUNT
AMOUNT*XX*
AMOUNTHTVA
AMOUNTTVA
ARP_TRN
BACKURL
BATCHID
BGCOLOR
BLVERNUM
BIC
BIN
BRAND
BRANDVISUAL
BUTTONBGCOLOR
BUTTONTXTCOLOR
CANCELURL
CARDNO
CATALOGURL
CAVV_3D
CAVVALGORITHM_3D
CERTID
CHECK_AAV
CIVILITY
CN
COM
COMPLUS
CONVCCY
COSTCENTER
COSTCODE
CREDITCODE
CREDITDEBIT
CUID
CURRENCY
CVC
CVCFLAG
DATA
DATATYPE
DATEIN
DATEOUT
DBXML
DCC_COMMPERC
DCC_CONVAMOUNT
DCC_CONVCCY
DCC_EXCHRATE
DCC_EXCHRATETS
DCC_INDICATOR
DCC_MARGINPERC
DCC_REF
DCC_SOURCE
DCC_VALID
DECLINEURL
DELIVERYDATE
DEVICE
DISCOUNTRATE
DISPLAYMODE
ECI
ECI_3D
ECOM_BILLTO_COMPANY
ECOM_BILLTO_POSTAL_CITY
ECOM_BILLTO_POSTAL_COUNTRYCODE
ECOM_BILLTO_POSTAL_COUNTY
ECOM_BILLTO_POSTAL_NAME_FIRST
ECOM_BILLTO_POSTAL_NAME_LAST
ECOM_BILLTO_POSTAL_NAME_PREFIX
ECOM_BILLTO_POSTAL_POSTALCODE
ECOM_BILLTO_POSTAL_STREET_LINE1
ECOM_BILLTO_POSTAL_STREET_LINE2
ECOM_BILLTO_POSTAL_STREET_LINE3
ECOM_BILLTO_POSTAL_STREET_NUMBER
ECOM_BILLTO_TELECOM_MOBILE_NUMBER
ECOM_BILLTO_TELECOM_PHONE_NUMBER
ECOM_CONSUMERID
ECOM_CONSUMER_GENDER
ECOM_CONSUMEROGID
ECOM_CONSUMERORDERID
ECOM_CONSUMERUSERALIAS
ECOM_CONSUMERUSERPWD
ECOM_CONSUMERUSERID
ECOM_ESTIMATEDDELIVERYDATE
ECOM_ESTIMATEDELIVERYDATE
ECOM_PAYMENT_CARD_EXPDATE_MONTH
ECOM_PAYMENT_CARD_EXPDATE_YEAR
ECOM_PAYMENT_CARD_NAME
ECOM_PAYMENT_CARD_VERIFICATION
ECOM_SHIPMETHOD
ECOM_SHIPMETHODDETAILS
ECOM_SHIPMETHODSPEED
ECOM_SHIPMETHODTYPE
ECOM_SHIPTO_COMPANY
ECOM_SHIPTO_DOB
ECOM_SHIPTO_ONLINE_EMAIL
ECOM_SHIPTO_POSTAL_CITY
ECOM_SHIPTO_POSTAL_COUNTRYCODE
ECOM_SHIPTO_POSTAL_COUNTY
ECOM_SHIPTO_POSTAL_NAME_FIRST
ECOM_SHIPTO_POSTAL_NAME_LAST
ECOM_SHIPTO_POSTAL_NAME_PREFIX
ECOM_SHIPTO_POSTAL_POSTALCODE
ECOM_SHIPTO_POSTAL_STATE
ECOM_SHIPTO_POSTAL_STREET_LINE1
ECOM_SHIPTO_POSTAL_STREET_LINE2
ECOM_SHIPTO_POSTAL_STREET_NUMBER
ECOM_SHIPTO_TELECOM_FAX_NUMBER
ECOM_SHIPTO_TELECOM_MOBILE_NUMBER
ECOM_SHIPTO_TELECOM_PHONE_NUMBER
ECOM_SHIPTO_TVA
ED
EMAIL
EXCEPTIONURL
EXCLPMLIST
EXECUTIONDATE*XX*
FACEXCL*XX*
FACTOTAL*XX*
FIRSTCALL
FLAG3D
FONTTYPE
FORCECODE1
FORCECODE2
FORCECODEHASH
FORCEPROCESS
FORCETP
FP_ACTIV
GENERIC_BL
GIROPAY_ACCOUNT_NUMBER
GIROPAY_BLZ
GIROPAY_OWNER_NAME
GLOBORDERID
GUID
HDFONTTYPE
HDTBLBGCOLOR
HDTBLTXTCOLOR
HEIGHTFRAME
HOMEURL
HTTP_ACCEPT
HTTP_USER_AGENT
INCLUDE_BIN
INCLUDE_COUNTRIES
INITIAL_REC_TRN
INVDATE
INVDISCOUNT
INVLEVEL
INVORDERID
ISSUERID
IST_MOBILE
ITEM_COUNT
ITEMATTRIBUTES*XX*
ITEMCATEGORY*XX*
ITEMCOMMENTS*XX*
ITEMDESC*XX*
ITEMDISCOUNT*XX*
ITEMFDMPRODUCTCATEG*XX*
ITEMID*XX*
ITEMNAME*XX*
ITEMPRICE*XX*
ITEMQUANT*XX*
ITEMQUANTORIG*XX*
ITEMUNITOFMEASURE*XX*
ITEMVAT*XX*
ITEMVATCODE*XX*
ITEMWEIGHT*XX*
LANGUAGE
LEVEL1AUTHCPC
LIDEXCL*XX*
LIMITCLIENTSCRIPTUSAGE
LINE_REF
LINE_REF1
LINE_REF2
LINE_REF3
LINE_REF4
LINE_REF5
LINE_REF6
LIST_BIN
LIST_COUNTRIES
LOGO
MANDATEID
MAXITEMQUANT*XX*
MERCHANTID
MODE
MTIME
MVER
NETAMOUNT
OPERATION
ORDERID
ORDERSHIPCOST
ORDERSHIPMETH
ORDERSHIPTAX
ORDERSHIPTAXCODE
ORIG
OR_INVORDERID
OR_ORDERID
OWNERADDRESS
OWNERADDRESS2
OWNERCTY
OWNERTELNO
OWNERTELNO2
OWNERTOWN
OWNERZIP
PAIDAMOUNT
PARAMPLUS
PARAMVAR
PAYID
PAYMETHOD
PM
PMLIST
PMLISTPMLISTTYPE
PMLISTTYPE
PMLISTTYPEPMLIST
PMTYPE
POPUP
POST
PSPID
PSWD
RECIPIENTACCOUNTNUMBER
RECIPIENTDOB
RECIPIENTLASTNAME
RECIPIENTZIP
REF
REFER
REFID
REFKIND
REF_CUSTOMERID
REF_CUSTOMERREF
REGISTRED
REMOTE_ADDR
REQGENFIELDS
RNPOFFERT
RTIMEOUT
RTIMEOUTREQUESTEDTIMEOUT
SCORINGCLIENT
SEQUENCETYPE
SETT_BATCH
SID
SIGNDATE
STATUS_3D
SUBSCRIPTION_ID
SUB_AM
SUB_AMOUNT
SUB_COM
SUB_COMMENT
SUB_CUR
SUB_ENDDATE
SUB_ORDERID
SUB_PERIOD_MOMENT
SUB_PERIOD_MOMENT_M
SUB_PERIOD_MOMENT_WW
SUB_PERIOD_NUMBER
SUB_PERIOD_NUMBER_D
SUB_PERIOD_NUMBER_M
SUB_PERIOD_NUMBER_WW
SUB_PERIOD_UNIT
SUB_STARTDATE
SUB_STATUS
TAAL
TAXINCLUDED*XX*
TBLBGCOLOR
TBLTXTCOLOR
TID
TITLE
TOTALAMOUNT
TP
TRACK2
TXTBADDR2
TXTCOLOR
TXTOKEN
TXTOKENTXTOKENPAYPAL
TXSHIPPING
TXSHIPPINGLOCATIONPROFILE
TXURL
TXVERIFIER
TYPE_COUNTRY
UCAF_AUTHENTICATION_DATA
UCAF_PAYMENT_CARD_CVC2
UCAF_PAYMENT_CARD_EXPDATE_MONTH
UCAF_PAYMENT_CARD_EXPDATE_YEAR
UCAF_PAYMENT_CARD_NUMBER
USERID
USERTYPE
VERSION
WBTU_MSISDN
WBTU_ORDERID
WEIGHTUNIT
WIN3DS
WITHROOT
XDL
""".split()

# https://payment-services.ingenico.com/ogone/support/~/media/kdb/integration%20guides/sha-out_params.ashx
SHA_OUT_PARAMS = """
AAVADDRESS
AAVCHECK
AAVMAIL
AAVNAME
AAVPHONE
AAVZIP
ACCEPTANCE
ALIAS
AMOUNT
BIC
BIN
BRAND
CARDNO
CCCTY
CN
COLLECTOR_BIC
COLLECTOR_IBAN
COMPLUS
CREATION_STATUS
CREDITDEBIT
CURRENCY
CVCCHECK
DCC_COMMPERCENTAGE
DCC_CONVAMOUNT
DCC_CONVCCY
DCC_EXCHRATE
DCC_EXCHRATESOURCE
DCC_EXCHRATETS
DCC_INDICATOR
DCC_MARGINPERCENTAGE
DCC_VALIDHOURS
DEVICEID
DIGESTCARDNO
ECI
ED
EMAIL
ENCCARDNO
FXAMOUNT
FXCURRENCY
IP
IPCTY
MANDATEID
MOBILEMODE
NBREMAILUSAGE
NBRIPUSAGE
NBRIPUSAGE_ALLTX
NBRUSAGE
NCERROR
ORDERID
PAYID
PAYIDSUB
PAYMENT_REFERENCE
PM
SCO_CATEGORY
SCORING
SEQUENCETYPE
SIGNDATE
STATUS
SUBBRAND
SUBSCRIPTION_ID
TRXDATE
VC
""".split()


class Payment(PaymentCommon):
    # See http://payment-services.ingenico.com/fr/fr/ogone/support/guides/integration%20guides/e-commerce
    description = {
        'caption': _('Ogone / Ingenico Payment System e-Commerce'),
        'parameters': [
            {
                'name': 'normal_return_url',
                'caption': _('Normal return URL'),
                'default': '',
                'required': True,
            },
            {
                'name': 'automatic_return_url',
                'caption': _('Automatic return URL (ignored, must be set in Ogone backoffice)'),
                'required': False,
            },
            {
                'name': 'environment',
                'default': ENVIRONMENT_TEST,
                'caption': 'Environnement',
                'choices': ENVIRONMENT,
            },
            {
                'name': 'pspid',
                'caption': "Nom d'affiliation dans le système",
                'required': True,
            },
            {
                'name': 'language',
                'caption': _('Language'),
                'default': 'fr_FR',
                'choices': (('fr_FR', 'français'),),
            },
            {
                'name': 'encoding',
                'caption': _('Characters encoding'),
                'default': 'utf-8',
                'choices': [
                    ('iso-8859-1', 'Latin1 (ISO-8859-1)'),
                    ('utf-8', 'Unicode (UTF-8)'),
                ],
            },
            {
                'name': 'hash_algorithm',
                'caption': 'Algorithme de hachage',
                'default': 'sha1',
            },
            {
                'name': 'sha_in',
                'caption': 'Clé SHA-IN',
                'required': True,
            },
            {
                'name': 'sha_out',
                'caption': 'Clé SHA-OUT',
                'required': True,
            },
            {
                'name': 'currency',
                'caption': 'Monnaie',
                'default': 'EUR',
                'choices': ('EUR',),
            },
        ],
    }

    def __init__(self, options, logger=None):
        # retro-compatibility with old default of latin1
        options.setdefault('encoding', 'iso-8859-1')
        super().__init__(options, logger=logger)

    def sha_sign(self, algo, key, params, keep, encoding='iso-8859-1'):
        '''Ogone signature algorithm of query string'''
        values = params.items()
        values = [(a.upper(), b) for a, b in values]
        values = sorted(values)
        values = ['%s=%s' % (a, b) for a, b in values if a in keep and b]
        tosign = key.join(values)
        tosign += key
        tosign = force_byte(tosign, encoding=encoding)
        hashing = getattr(hashlib, algo)
        return hashing(tosign).hexdigest().upper()

    def sha_sign_in(self, params, encoding='iso-8859-1'):
        return self.sha_sign(self.hash_algorithm, self.sha_in, params, SHA_IN_PARAMS, encoding=encoding)

    def sha_sign_out(self, params, encoding='iso-8859-1'):
        return self.sha_sign(self.hash_algorithm, self.sha_out, params, SHA_OUT_PARAMS, encoding=encoding)

    def get_request_url(self):
        if self.environment == ENVIRONMENT_TEST:
            return ENVIRONMENT_TEST_URL
        if self.environment == ENVIRONMENT_PROD:
            return ENVIRONMENT_PROD_URL
        raise NotImplementedError('unknown environment %s' % self.environment)

    def request(
        self,
        amount,
        orderid=None,
        name=None,
        email=None,
        language=None,
        description=None,
        transaction_id=None,
        **kwargs,
    ):
        # use complus for transmitting and receiving the transaction_id see
        # https://epayments-support.ingenico.com/fr/integration/all-sales-channels/integrate-with-e-commerce/guide#variable-feedback-parameters
        # orderid is now only used for unicity of payments check (it's
        # garanteed that no payment for the same ORDERID can happen during a 45
        # days window, see
        # https://epayments-support.ingenico.com/fr/integration/all-sales-channels/integrate-with-e-commerce/guide#form-parameters)
        complus = transaction_id or uuid.uuid4().hex
        if not orderid:
            orderid = complus

        language = language or self.language
        # convertir en centimes
        amount = Decimal(amount) * 100
        # arrondi comptable francais
        amount = amount.quantize(Decimal('1.'), rounding=ROUND_HALF_UP)
        params = {
            'AMOUNT': force_text(amount),
            'ORDERID': orderid,
            'PSPID': self.pspid,
            'LANGUAGE': language,
            'CURRENCY': self.currency,
            'COMPLUS': complus,
        }
        if self.normal_return_url:
            params['ACCEPTURL'] = self.normal_return_url
            params['BACKURL'] = self.normal_return_url
            params['CANCELURL'] = self.normal_return_url
            params['DECLINEURL'] = self.normal_return_url
            params['EXCEPTIONURL'] = self.normal_return_url
        if name:
            params['CN'] = name
        if email:
            params['EMAIL'] = email
        if description:
            params['COM'] = description
        for key, value in kwargs.items():
            params[key.upper()] = value
        params['SHASIGN'] = self.sha_sign_in(params)
        # uniformize all values to UTF-8 string
        for key in params:
            params[key] = force_text(params[key])
        url = self.get_request_url()
        form = Form(
            url=url,
            method='POST',
            fields=[{'type': 'hidden', 'name': key, 'value': params[key]} for key in params],
        )
        return complus, FORM, form

    def response(self, query_string, **kwargs):
        params = urlparse.parse_qs(query_string, True, encoding=self.encoding)
        params = {key.upper(): params[key][0] for key in params}
        if not set(params) >= {'ORDERID', 'PAYID', 'STATUS', 'NCERROR'}:
            raise ResponseError('missing ORDERID, PAYID, STATUS or NCERROR')

        # py2: decode binary strings in query-string
        for key in params:
            params[key] = force_text(params[key], self.encoding)
        orderid = params['ORDERID']
        complus = params.get('COMPLUS')
        transaction_id = params['PAYID']
        status = params['STATUS']
        error = params['NCERROR']
        signed = False
        if self.sha_in:
            signature = params.get('SHASIGN')
            # check signature against both encoding
            for encoding in ('iso-8859-1', 'utf-8'):
                expected_signature = self.sha_sign_out(params, encoding=encoding)
                signed = signature == expected_signature
                if signed:
                    break
        if status == '1':
            result = CANCELLED
        elif status == '2':
            result = DENIED
        elif status == '5':
            result = ACCEPTED
        elif status in ('9', '95'):
            result = PAID
        elif len(status) == 2 and status[1] == '1':
            # Statuses with two digits represent either ‘intermediary'
            # situations or abnormal events. When the second digit is:
            # 1, this means the payment processing is on hold. (e.g.
            # status 91: payment waiting/pending)
            result = WAITING
        else:
            self.logger.error(
                'response STATUS=%s NCERROR=%s NCERRORPLUS=%s', status, error, params.get('NCERRORPLUS', '')
            )
            result = ERROR
        # extract reference from received order id

        return PaymentResponse(
            result=result,
            signed=signed,
            bank_data=params,
            order_id=complus or orderid,
            transaction_id=transaction_id,
        )

    @classmethod
    def guess(self, *, method=None, query_string=None, body=None, headers=None, backends=(), **kwargs):
        for content in [query_string, body]:
            if isinstance(content, bytes):
                try:
                    content = content.decode()
                except UnicodeDecodeError:
                    pass
            if isinstance(content, str):
                fields = {key.upper(): values for key, values in urlparse.parse_qs(content).items()}
                if not set(fields) >= {'ORDERID', 'PAYID', 'STATUS', 'NCERROR'}:
                    continue
                orderid = fields.get('ORDERID')
                complus = fields.get('COMPLUS')
                if complus:
                    return complus[0]
                return orderid[0]
        return None
