#!/usr/bin/env python
# coding: utf-8
"""
    __init__.py
    ~~~~~~~~~~

"""
import json
from datetime import datetime
from functools import partial

import hashlib
import OpenSSL
from Crypto.Hash import SHA, SHA256
from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5

from .compat import decodebytes, encodebytes, quote_plus, urlopen
from .exceptions import AliPayException, AliPayValidationError
from .utils import AliPayConfig
from .loggers import logger

# 常见加密算法
CryptoAlgSet = (
    b'rsaEncryption',
    b'md2WithRSAEncryption',
    b'md5WithRSAEncryption',
    b'sha1WithRSAEncryption',
    b'sha256WithRSAEncryption',
    b'sha384WithRSAEncryption',
    b'sha512WithRSAEncryption'
)


class BaseAliPay:
    @property
    def appid(self):
        return self._appid

    @property
    def sign_type(self):
        return self._sign_type

    @property
    def app_private_key(self):
        """签名用"""
        return self._app_private_key

    @property
    def alipay_public_key(self):
        """验证签名用"""
        return self._alipay_public_key

    def __init__(
            self,
            appid,
            app_notify_url,
            app_private_key_string=None,
            alipay_public_key_string=None,
            sign_type="RSA2",
            debug=False,
            verbose=False,
            config=None
    ):
        """
        初始化:
        alipay = AliPay(
          appid="",
          app_notify_url="http://example.com",
          sign_type="RSA2"
        )
        """
        self._appid = str(appid)
        self._app_notify_url = app_notify_url
        self._app_private_key_string = app_private_key_string
        self._alipay_public_key_string = alipay_public_key_string
        self._verbose = verbose
        self._config = config or AliPayConfig()

        self._app_private_key = None
        self._alipay_public_key = None
        if sign_type not in ("RSA", "RSA2"):
            message = "Unsupported sign type {}".format(sign_type)
            raise AliPayException(None, message)
        self._sign_type = sign_type

        if debug:
            self._gateway = "https://openapi.alipaydev.com/gateway.do"
        else:
            self._gateway = "https://openapi.alipay.com/gateway.do"

        # load key file immediately
        self._load_key()

    def _load_key(self):
        # load private key
        content = self._app_private_key_string
        self._app_private_key = RSA.importKey(content)

        # load public key
        content = self._alipay_public_key_string
        self._alipay_public_key = RSA.importKey(content)

    def _sign(self, unsigned_string):
        """
        通过如下方法调试签名
        方法1
            key = rsa.PrivateKey.load_pkcs1(open(self._app_private_key_string).read())
            sign = rsa.sign(unsigned_string.encode(), key, "SHA-1")
            # base64 编码，转换为unicode表示并移除回车
            sign = base64.encodebytes(sign).decode().replace("\n", "")
        方法2
            key = RSA.importKey(open(self._app_private_key_string).read())
            signer = PKCS1_v1_5.new(key)
            signature = signer.sign(SHA.new(unsigned_string.encode()))
            # base64 编码，转换为unicode表示并移除回车
            sign = base64.encodebytes(signature).decode().replace("\n", "")
        方法3
            echo "abc" | openssl sha1 -sign alipay.key | openssl base64

        """
        # 开始计算签名
        key = self.app_private_key
        signer = PKCS1_v1_5.new(key)
        if self._sign_type == "RSA":
            signature = signer.sign(SHA.new(unsigned_string.encode()))
        else:
            signature = signer.sign(SHA256.new(unsigned_string.encode()))
        # base64 编码，转换为unicode表示并移除回车
        sign = encodebytes(signature).decode().replace("\n", "")
        return sign

    def _ordered_data(self, data):
        for k, v in data.items():
            if isinstance(v, dict):
                # 将字典类型的数据dump出来
                data[k] = json.dumps(v, separators=(',', ':'))
        return sorted(data.items())

    def build_body(self, method, biz_content, **kwargs):
        data = {
            "app_id": self._appid,
            "method": method,
            "charset": "utf-8",
            "sign_type": self._sign_type,
            "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
            "version": "1.0",
            "biz_content": biz_content
        }
        data.update(kwargs)

        if method in (
                "alipay.trade.app.pay", "alipay.trade.wap.pay",
                "alipay.trade.page.pay", "alipay.trade.pay",
                "alipay.trade.precreate", "alipay.trade.create"
        ) and not data.get("notify_url") and self._app_notify_url:
            data["notify_url"] = self._app_notify_url

        if self._verbose:
            logger.debug("data to be signed")
            logger.debug(data)
        return data

    def sign_data(self, data):
        # 排序后的字符串
        ordered_items = self._ordered_data(data)
        raw_string = "&".join("{}={}".format(k, v) for k, v in ordered_items)
        sign = self._sign(raw_string)
        unquoted_items = ordered_items + [('sign', sign)]

        # 获得最终的订单信息字符串
        signed_string = "&".join("{}={}".format(k, quote_plus(v)) for k, v in unquoted_items)
        if self._verbose:
            logger.debug("signed srtring")
            logger.debug(signed_string)
        return signed_string

    def _verify(self, raw_content, signature):
        # 开始计算签名
        key = self.alipay_public_key
        signer = PKCS1_v1_5.new(key)
        if self._sign_type == "RSA":
            digest = SHA.new()
        else:
            digest = SHA256.new()
        digest.update(raw_content.encode())
        return bool(signer.verify(digest, decodebytes(signature.encode())))

    def verify(self, data, signature):
        if "sign_type" in data:
            sign_type = data.pop("sign_type")
            if sign_type != self._sign_type:
                raise AliPayException(None, "Unknown sign type: {}".format(sign_type))
        # 排序后的字符串
        unsigned_items = self._ordered_data(data)
        message = "&".join(u"{}={}".format(k, v) for k, v in unsigned_items)
        return self._verify(message, signature)

    def client_api(self, api_name, biz_content, **kwargs):
        """
        alipay api without http request
        """
        data = self.build_body(api_name, biz_content, **kwargs)
        return self.sign_data(data)

    def server_api(self, api_name, biz_content, **kwargs):
        """
        alipay api with http request
        """
        data = self.build_body(api_name, biz_content, **kwargs)
        # alipay.trade.query => alipay_trade_query_response
        response_type = api_name.replace(".", "_") + "_response"
        return self.verified_sync_response(data, response_type)

    def api_alipay_trade_wap_pay(
            self, subject, out_trade_no, total_amount,
            return_url=None, notify_url=None, **kwargs
    ):
        biz_content = {
            "subject": subject,
            "out_trade_no": out_trade_no,
            "total_amount": total_amount,
            "product_code": "QUICK_WAP_PAY"
        }
        biz_content.update(kwargs)
        data = self.build_body(
            "alipay.trade.wap.pay",
            biz_content,
            return_url=return_url,
            notify_url=notify_url
        )
        return self.sign_data(data)

    def api_alipay_trade_app_pay(
            self, subject, out_trade_no, total_amount, notify_url=None, **kwargs
    ):
        biz_content = {
            "subject": subject,
            "out_trade_no": out_trade_no,
            "total_amount": total_amount,
            "product_code": "QUICK_MSECURITY_PAY"
        }
        biz_content.update(kwargs)
        data = self.build_body("alipay.trade.app.pay", biz_content, notify_url=notify_url)
        return self.sign_data(data)

    def api_alipay_trade_page_pay(self, subject, out_trade_no, total_amount,
                                  return_url=None, notify_url=None, **kwargs):
        biz_content = {
            "subject": subject,
            "out_trade_no": out_trade_no,
            "total_amount": total_amount,
            "product_code": "FAST_INSTANT_TRADE_PAY"
        }

        biz_content.update(kwargs)
        data = self.build_body(
            "alipay.trade.page.pay",
            biz_content,
            return_url=return_url,
            notify_url=notify_url
        )
        return self.sign_data(data)

    def api_alipay_trade_query(self, out_trade_no=None, trade_no=None):
        """
        response = {
          "alipay_trade_query_response": {
            "trade_no": "2017032121001004070200176844",
            "code": "10000",
            "invoice_amount": "20.00",
            "open_id": "20880072506750308812798160715407",
            "fund_bill_list": [
              {
                "amount": "20.00",
                "fund_channel": "ALIPAYACCOUNT"
              }
            ],
            "buyer_logon_id": "csq***@sandbox.com",
            "send_pay_date": "2017-03-21 13:29:17",
            "receipt_amount": "20.00",
            "out_trade_no": "out_trade_no15",
            "buyer_pay_amount": "20.00",
            "buyer_user_id": "2088102169481075",
            "msg": "Success",
            "point_amount": "0.00",
            "trade_status": "TRADE_SUCCESS",
            "total_amount": "20.00"
          },
          "sign": ""
        }
        """
        assert (out_trade_no is not None) or (trade_no is not None), \
            "Both trade_no and out_trade_no are None"

        biz_content = {}
        if out_trade_no:
            biz_content["out_trade_no"] = out_trade_no
        if trade_no:
            biz_content["trade_no"] = trade_no
        data = self.build_body("alipay.trade.query", biz_content)
        response_type = "alipay_trade_query_response"
        return self.verified_sync_response(data, response_type)

    def api_alipay_trade_pay(
            self, out_trade_no, scene, auth_code, subject, notify_url=None, **kwargs
    ):
        """
        eg:
            self.api_alipay_trade_pay(
                out_trade_no,
                "bar_code/wave_code",
                auth_code,
                subject,
                total_amount=12,
                discountable_amount=10
            )

        failed response = {
            "alipay_trade_pay_response": {
                "code": "40004",
                "msg": "Business Failed",
                "sub_code": "ACQ.INVALID_PARAMETER",
                "sub_msg": "",
                "buyer_pay_amount": "0.00",
                "invoice_amount": "0.00",
                "point_amount": "0.00",
                "receipt_amount": "0.00"
            },
            "sign": ""
        }
        succeeded response =
            {
              "alipay_trade_pay_response": {
                "trade_no": "2017032121001004070200176846",
                "code": "10000",
                "invoice_amount": "20.00",
                "open_id": "20880072506750308812798160715407",
                "fund_bill_list": [
                  {
                    "amount": "20.00",
                    "fund_channel": "ALIPAYACCOUNT"
                  }
                ],
                "buyer_logon_id": "csq***@sandbox.com",
                "receipt_amount": "20.00",
                "out_trade_no": "out_trade_no18",
                "buyer_pay_amount": "20.00",
                "buyer_user_id": "2088102169481075",
                "msg": "Success",
                "point_amount": "0.00",
                "gmt_payment": "2017-03-21 15:07:29",
                "total_amount": "20.00"
              },
              "sign": ""
            }
        """
        assert scene in ("bar_code", "wave_code"), 'scene not in ("bar_code", "wave_code")'

        biz_content = {
            "out_trade_no": out_trade_no,
            "scene": scene,
            "auth_code": auth_code,
            "subject": subject
        }
        biz_content.update(**kwargs)
        data = self.build_body("alipay.trade.pay", biz_content, notify_url=notify_url)
        response_type = "alipay_trade_pay_response"
        return self.verified_sync_response(data, response_type)

    def api_alipay_trade_refund(self, refund_amount, out_trade_no=None, trade_no=None, **kwargs):
        biz_content = {
            "refund_amount": refund_amount
        }
        biz_content.update(**kwargs)
        if out_trade_no:
            biz_content["out_trade_no"] = out_trade_no
        if trade_no:
            biz_content["trade_no"] = trade_no

        data = self.build_body("alipay.trade.refund", biz_content)
        response_type = "alipay_trade_refund_response"
        return self.verified_sync_response(data, response_type)

    def api_alipay_trade_cancel(self, out_trade_no=None, trade_no=None):
        """
        response = {
        "alipay_trade_cancel_response": {
            "msg": "Success",
            "out_trade_no": "out_trade_no15",
            "code": "10000",
            "retry_flag": "N"
          }
        }
        """

        assert (out_trade_no is not None) or (trade_no is not None), \
            "Both trade_no and out_trade_no are None"

        biz_content = {}
        if out_trade_no:
            biz_content["out_trade_no"] = out_trade_no
        if trade_no:
            biz_content["trade_no"] = trade_no

        data = self.build_body("alipay.trade.cancel", biz_content)
        response_type = "alipay_trade_cancel_response"
        return self.verified_sync_response(data, response_type)

    def api_alipay_trade_close(self, out_trade_no=None, trade_no=None, operator_id=None):
        """
        response = {
            "alipay_trade_close_response": {
                "code": "10000",
                "msg": "Success",
                "trade_no": "2013112111001004500000675971",
                "out_trade_no": "YX_001"a
            }
        }
        """

        assert (out_trade_no is not None) or (trade_no is not None), \
            "Both trade_no and out_trade_no are None"

        biz_content = {}
        if out_trade_no:
            biz_content["out_trade_no"] = out_trade_no
        if trade_no:
            biz_content["trade_no"] = trade_no
        if operator_id:
            biz_content["operator_id"] = operator_id

        data = self.build_body("alipay.trade.close", biz_content)
        response_type = "alipay_trade_close_response"
        return self.verified_sync_response(data, response_type)

    def api_alipay_trade_create(
            self, subject, out_trade_no, total_amount, notify_url=None, **kwargs
    ):
        biz_content = {
            "subject": subject,
            "out_trade_no": out_trade_no,
            "total_amount": total_amount
        }
        biz_content.update(kwargs)
        data = self.build_body("alipay.trade.create", biz_content, notify_url=notify_url)
        response_type = "alipay_trade_create"
        return self.verified_sync_response(data, response_type)

    def api_alipay_trade_precreate(
            self, subject, out_trade_no, total_amount, notify_url=None, **kwargs
    ):
        """
        success response  = {
          "alipay_trade_precreate_response": {
            "msg": "Success",
            "out_trade_no": "out_trade_no17",
            "code": "10000",
            "qr_code": "https://qr.alipay.com/bax03431ljhokirwl38f00a7"
          },
          "sign": ""
        }

        failed response = {
          "alipay_trade_precreate_response": {
            "msg": "Business Failed",
            "sub_code": "ACQ.TOTAL_FEE_EXCEED",
            "code": "40004",
            "sub_msg": "订单金额超过限额"
          },
          "sign": ""
        }

        """
        biz_content = {
            "out_trade_no": out_trade_no,
            "total_amount": total_amount,
            "subject": subject
        }
        biz_content.update(**kwargs)
        data = self.build_body("alipay.trade.precreate", biz_content, notify_url=notify_url)
        response_type = "alipay_trade_precreate_response"
        return self.verified_sync_response(data, response_type)

    def api_alipay_trade_fastpay_refund_query(
            self, out_request_no, trade_no=None, out_trade_no=None
    ):
        assert (out_trade_no is not None) or (trade_no is not None), \
            "Both trade_no and out_trade_no are None"

        biz_content = {"out_request_no": out_request_no}
        if trade_no:
            biz_content["trade_no"] = trade_no
        else:
            biz_content["out_trade_no"] = out_trade_no

        data = self.build_body("alipay.trade.fastpay.refund.query", biz_content)
        response_type = "alipay_trade_fastpay_refund_query_response"
        return self.verified_sync_response(data, response_type)

    def api_alipay_fund_trans_toaccount_transfer(
            self, out_biz_no, payee_type, payee_account, amount, **kwargs
    ):
        assert payee_type in ("ALIPAY_USERID", "ALIPAY_LOGONID"), "unknown payee type"
        biz_content = {
            "out_biz_no": out_biz_no,
            "payee_type": payee_type,
            "payee_account": payee_account,
            "amount": amount
        }
        biz_content.update(kwargs)
        data = self.build_body("alipay.fund.trans.toaccount.transfer", biz_content)
        response_type = "alipay_fund_trans_toaccount_transfer_response"
        return self.verified_sync_response(data, response_type)

    def api_alipay_fund_trans_order_query(self, out_biz_no=None, order_id=None):
        if out_biz_no is None and order_id is None:
            raise Exception("Both out_biz_no and order_id are None!")

        biz_content = {}
        if out_biz_no:
            biz_content["out_biz_no"] = out_biz_no
        if order_id:
            biz_content["order_id"] = order_id

        data = self.build_body("alipay.fund.trans.order.query", biz_content)
        response_type = "alipay_fund_trans_order_query_response"
        return self.verified_sync_response(data, response_type)

    def api_alipay_trade_order_settle(
            self,
            out_request_no,
            trade_no,
            royalty_parameters,
            **kwargs
    ):
        biz_content = {
            "out_request_no": out_request_no,
            "trade_no": trade_no,
            "royalty_parameters": royalty_parameters,
        }
        biz_content.update(kwargs)
        data = self.build_body("alipay.trade.order.settle", biz_content)
        response_type = "alipay_trade_order_settle_response"
        return self.verified_sync_response(data, response_type)

    def api_alipay_ebpp_invoice_token_batchquery(self, invoice_token=None, scene=None):
        if scene is None:
            scene = "INVOICE_EXPENSE"
        if invoice_token is None:
            raise Exception("invoice_token is None!")

        biz_content = {
            "invoice_token": invoice_token,
            "scene": scene
        }
        data = self.build_body("alipay.ebpp.invoice.token.batchquery", biz_content)
        response_type = "alipay_ebpp_invoice_token_batchquery_response"
        return self.verified_sync_response(data, response_type)

    def _verify_and_return_sync_response(self, raw_string, response_type):
        """
        return response if verification succeeded, raise exception if not

        As to issue #69, json.loads(raw_string)[response_type] should not be returned directly,
        use json.loads(plain_content) instead

        failed response is like this
        {
          "alipay_trade_query_response": {
            "sub_code": "isv.invalid-app-id",
            "code": "40002",
            "sub_msg": "无效的AppID参数",
            "msg": "Invalid Arguments"
          }
        }
        """
        response = json.loads(raw_string)
        # raise exceptions
        if "sign" not in response.keys():
            result = response[response_type]
            raise AliPayException(
                code=result.get("code", "0"),
                message=raw_string
            )

        sign = response["sign"]

        # locate string to be signed
        plain_content = self._get_string_to_be_signed(raw_string, response_type)

        if not self._verify(plain_content, sign):
            raise AliPayValidationError
        return json.loads(plain_content)

    def verified_sync_response(self, data, response_type):
        url = self._gateway + "?" + self.sign_data(data)
        raw_string = urlopen(url, timeout=self._config.timeout).read().decode()
        return self._verify_and_return_sync_response(raw_string, response_type)

    def _get_string_to_be_signed(self, raw_string, response_type):
        """
        https://docs.open.alipay.com/200/106120
        从同步返回的接口里面找到待签名的字符串
        """
        balance = 0
        start = end = raw_string.find("{", raw_string.find(response_type))
        # 从response_type之后的第一个｛的下一位开始匹配，
        # 如果是｛则balance加1; 如果是｝而且balance=0，就是待验签字符串的终点
        for i, c in enumerate(raw_string[start + 1:], start + 1):
            if c == "{":
                balance += 1
            elif c == "}":
                if balance == 0:
                    end = i + 1
                    break
                balance -= 1
        return raw_string[start:end]


class AliPay(BaseAliPay):
    pass


class DCAliPay(BaseAliPay):
    """
    数字证书 (digital certificate) 版本
    """

    def __init__(
            self,
            appid,
            app_notify_url,
            app_private_key_string,
            app_public_key_cert_string,
            alipay_public_key_cert_string,
            alipay_root_cert_string,
            sign_type="RSA2",
            debug=False
    ):
        """
        初始化
        DCAlipay(
            appid='',
            app_notify_url='http://example.com',
            app_private_key_string='',
            app_public_key_cert_string='',
            alipay_public_key_cert_sring='',
            aplipay_root_cert_string='',
        )
        """
        self._app_public_key_cert_string = app_public_key_cert_string
        self._alipay_public_key_cert_string = alipay_public_key_cert_string
        self._alipay_root_cert_string = alipay_root_cert_string
        alipay_public_key_string = self.load_alipay_public_key_string()
        super().__init__(
            appid=appid,
            app_notify_url=app_notify_url,
            app_private_key_string=app_private_key_string,
            alipay_public_key_string=alipay_public_key_string,
            sign_type=sign_type,
            debug=debug
        )

    def api_alipay_open_app_alipaycert_download(self, alipay_cert_sn):
        """
        下载支付宝证书
        验签使用，支付宝公钥证书无感知升级机制
        """
        biz_content = {
            "alipay_cert_sn": alipay_cert_sn
        }
        data = self.build_body("alipay.open.app.alipaycert.download", biz_content)
        return self.sign_data(data)

    def build_body(self, *args, **kwargs):
        data = super().build_body(*args, **kwargs)
        data["app_cert_sn"] = self.app_cert_sn
        data["alipay_root_cert_sn"] = self.alipay_root_cert_sn
        if self._verbose:
            logger.debug("data to be signed")
            logger.debug(data)
        return data

    def load_alipay_public_key_string(self):
        cert = OpenSSL.crypto.load_certificate(
            OpenSSL.crypto.FILETYPE_PEM, self._alipay_public_key_cert_string
        )
        return OpenSSL.crypto.dump_publickey(
            OpenSSL.crypto.FILETYPE_PEM, cert.get_pubkey()
        ).decode("utf-8")

    @staticmethod
    def get_cert_sn(cert):
        """
        获取证书 SN 算法
        """
        cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)
        certIssue = cert.get_issuer()
        name = 'CN={},OU={},O={},C={}'.format(certIssue.CN, certIssue.OU, certIssue.O, certIssue.C)
        string = name + str(cert.get_serial_number())
        return hashlib.md5(string.encode()).hexdigest()

    @staticmethod
    def read_pem_cert_chain(certContent):
        """解析根证书"""
        # 根证书中，每个 cert 中间有两个回车间隔
        items = [i for i in certContent.split('\n\n') if i]
        load_cert = partial(OpenSSL.crypto.load_certificate, OpenSSL.crypto.FILETYPE_PEM)
        return [load_cert(c) for c in items]

    @staticmethod
    def get_root_cert_sn(rootCert):
        """ 根证书 SN 算法"""
        certs = DCAliPay.read_pem_cert_chain(rootCert)
        rootCertSN = None
        for cert in certs:
            try:
                sigAlg = cert.get_signature_algorithm()
            except ValueError:
                continue
            if sigAlg in CryptoAlgSet:
                certIssue = cert.get_issuer()
                name = 'CN={},OU={},O={},C={}'.format(
                    certIssue.CN, certIssue.OU, certIssue.O, certIssue.C
                )
                string = name + str(cert.get_serial_number())
                certSN = hashlib.md5(string.encode()).hexdigest()
                if not rootCertSN:
                    rootCertSN = certSN
                else:
                    rootCertSN = rootCertSN + '_' + certSN
        return rootCertSN

    @property
    def app_cert_sn(self):
        if not hasattr(self, "_app_cert_sn"):
            self._app_cert_sn = self.get_cert_sn(self._app_public_key_cert_string)
        return getattr(self, "_app_cert_sn")

    @property
    def alipay_root_cert_sn(self):
        if not hasattr(self, "_alipay_root_cert_sn"):
            self._alipay_root_cert_sn = self.get_root_cert_sn(self._alipay_root_cert_string)
        return getattr(self, "_alipay_root_cert_sn")

    def api_alipay_fund_trans_uni_transfer(
            self, out_biz_no, identity_type, identity, trans_amount, name=None, **kwargs
    ):
        """
        单笔转账接口, 只支持公钥证书模式
        文档地址: https://opendocs.alipay.com/apis/api_28/alipay.fund.trans.uni.transfer
        """
        assert identity_type in ("ALIPAY_USER_ID", "ALIPAY_LOGON_ID"), "unknown identity type"

        biz_content = {
            "payee_info": {
                "identity": identity,
                "identity_type": identity_type,
            },
            "out_biz_no": out_biz_no,
            "trans_amount": trans_amount,
            "product_code": "TRANS_ACCOUNT_NO_PWD",
            "biz_scene": "DIRECT_TRANSFER",
        }
        biz_content["payee_info"]["name"] = name if name else None
        biz_content.update(kwargs)

        response_type = "alipay_fund_trans_uni_transfer_response"
        data = self.build_body("alipay.fund.trans.uni.transfer", biz_content)
        return self.verified_sync_response(data, response_type)


class ISVAliPay(BaseAliPay):

    def __init__(
            self,
            appid,
            app_notify_url,
            app_private_key_string=None,
            alipay_public_key_string=None,
            sign_type="RSA2",
            debug=False,
            app_auth_token=None,
            app_auth_code=None
    ):
        if not app_auth_token and not app_auth_code:
            raise Exception("Both app_auth_code and app_auth_token are None !!!")

        self._app_auth_token = app_auth_token
        self._app_auth_code = app_auth_code
        super().__init__(
            appid,
            app_notify_url,
            app_private_key_string=app_private_key_string,
            alipay_public_key_string=alipay_public_key_string,
            sign_type=sign_type,
            debug=debug
        )

    @property
    def app_auth_token(self):
        # 没有则换取token
        if not self._app_auth_token:
            result = self.api_alipay_open_auth_token_app(self._app_auth_code)
            self._app_auth_token = result.get("app_auth_token")
            if not self._app_auth_token:
                msg = "Get auth token by auth code failed: {}"
                raise Exception(msg.format(self._app_auth_code))
        return self._app_auth_token

    def build_body(self, *args, **kwargs):
        data = super().build_body(*args, **kwargs)
        if self._app_auth_token:
            data["app_auth_token"] = self._app_auth_token
        if self._verbose:
            logger.debug("data to be signed")
            logger.debug(data)

        return data

    def api_alipay_open_auth_token_app(self, refresh_token=None):
        """
        response = {
          "code": "10000",
          "msg": "Success",
          "app_auth_token": "201708BB28623ce3d10f4f62875e9ef5cbeebX07",
          "app_refresh_token": "201708BB108a270d8bb6409890d16175a04a7X07",
          "auth_app_id": "appid",
          "expires_in": 31536000,
          "re_expires_in": 32140800,
          "user_id": "2088xxxxx
        }
        """

        if refresh_token:
            biz_content = {
                "grant_type": "refresh_token",
                "refresh_token": refresh_token
            }
        else:
            biz_content = {
                "grant_type": "authorization_code",
                "code": self._app_auth_code
            }
        data = self.build_body(
            "alipay.open.auth.token.app",
            biz_content,
        )
        response_type = "alipay_open_auth_token_app_response"
        return self.verified_sync_response(data, response_type)

    def api_alipay_open_auth_token_app_query(self):
        biz_content = {"app_auth_token": self.app_auth_token}
        data = self.build_body(
            "alipay.open.auth.token.app.query",
            biz_content,
        )
        response_type = "alipay_open_auth_token_app_query_response"
        return self.verified_sync_response(data, response_type)
