package cn.quant.baa.pay.acquirer.alipay;

import cn.quant.baa.pay.acquirer.AcquirerProperties;
import cn.quant.baa.pay.acquirer.MerchantAcquirer;
import cn.quant.baa.pay.jpa.entity.PayHistoryEntity;
import cn.quant.baa.pay.model.web.*;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.UriUtils;
import reactor.core.publisher.Mono;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.net.URI;
import java.security.*;
import java.security.cert.*;
import java.security.cert.Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Created by Administrator on 2021/8/31 0031.
 */
public class AlipayMerchantAcquirer extends MerchantAcquirer {

    private WebClient webClient;

    private String signType = "RSA2";

    private String charset = "utf-8";

    private static BouncyCastleProvider provider;

    static {
        provider = new BouncyCastleProvider();
        Security.addProvider(provider);
    }

    /**
     * 商户私钥
     */
    PrivateKey privateKey;

    /**
     * 支付宝公钥
     */
    X509Certificate payPublicKey;

    /**
     * 应用公钥
     */
    X509Certificate payCertKey;

    /**
     * 支付宝根证书
     */
    X509Certificate[] payRootCertKey;


    ObjectMapper objectMapper = new ObjectMapper();

    /**
     * 支付初始化
     *  1. 初始化私钥
     *  2. 初始化平台证书
     *  3. 初始化webClient
     *
     * @param properties
     * @return
     * @throws Exception
     */
    @Override
    public MerchantAcquirer init(AcquirerProperties properties) throws Exception {
//        super.init(properties);
        this.properties = properties;

        privateKey = getPrivateKey(properties.getPrivateKey());

        payPublicKey = loadCertificate(properties.getPayPublicKey());

        payCertKey = loadCertificate(properties.getPayCertKey());

        payRootCertKey = loadCertificates(properties.getPayRootCertKey());

        this.webClient = WebClient.builder()
                .baseUrl(properties.getDomain())
                .defaultHeader("Accept", "application/json")
                .defaultHeader("User-Agent", "QuantGroup PayCenter Java Api")
                .defaultHeader("Content-Type", "application/x-www-form-urlencoded;charset=" + charset)
                .build();
        return this;
    }

    @Override
    public Object code() {
        return this.properties.getMchChanId().toString();
    }

    @Override
    public JsonNode pay(PayRequestData payRequestData, PayHistoryEntity payHistoryEntity) {
        ObjectNode bodyNode = objectMapper.createObjectNode();
        bodyNode.put("out_trade_no", payRequestData.getOutTradeNo());
        bodyNode.put("total_amount", payRequestData.getAmount());
        bodyNode.put("subject", payRequestData.getSubject());
        bodyNode.put("body", payRequestData.getSubject());
        bodyNode.put("product_code", properties.getProductCode());
        switch (properties.getAccessCode()) {
            case APP:
                return doAppExecute(bodyNode);
            case H5:
            case WEB:
                return doWebExecute(bodyNode);
            default:
        }
        return doExecute(bodyNode);
    }

    @Override
    public JsonNode checkPay(CheckPayRequestData checkPayRequestData) {
//        String outTradeNo = "11111111218";
        ObjectNode bodyNode = objectMapper.createObjectNode();
        bodyNode.put("out_trade_no", checkPayRequestData.getOutTradeNo());
        return doExecute(bodyNode);
    }

    @Override
    public JsonNode checkRefund(CheckRefundRequestData checkRefundRequestData) {
        ObjectNode bodyNode = objectMapper.createObjectNode();
        bodyNode.put("out_trade_no", checkRefundRequestData.getOutTradeNo());
        bodyNode.put("out_request_no", checkRefundRequestData.getOutRefundNo());
        return doExecute(bodyNode);
    }

    @Override
    public JsonNode close(CloseRequestData closeRequestData) {
        ObjectNode bodyNode = objectMapper.createObjectNode();
        bodyNode.put("out_trade_no", closeRequestData.getOutTradeNo());
        return doExecute(bodyNode);
    }

    @Override
    public JsonNode refund(RefundRequestData refundRequestData) {
        ObjectNode bodyNode = objectMapper.createObjectNode();
        bodyNode.put("out_trade_no", refundRequestData.getOutTradeNo());
        bodyNode.put("refund_amount", refundRequestData.getRefundAmount());
        bodyNode.put("refund_reason", refundRequestData.getRefundReason());
        bodyNode.put("out_request_no", refundRequestData.getOutRefundNo());
        return doExecute(bodyNode);
    }

    /**
     * App支付发起请求
     *
     * @param bodyNode
     * @return
     */
    private JsonNode doAppExecute(ObjectNode bodyNode) {
        ObjectNode params = buildParams(bodyNode);
        TreeMap<String, String> sortedParams = getSortedMap(params);
        ObjectNode body = objectMapper.createObjectNode();
        body.put("data", buildQuery(sortedParams));
        return body;
    }

    /**
     * web支付发起请求
     * alipay.trade.wap.pay
     * alipay.trade.page.pay
     *
     * @param bodyNode
     * @return
     */
    private JsonNode doWebExecute(ObjectNode bodyNode) {
        ObjectNode params = buildParams(bodyNode);
        TreeMap<String, String> sortedParams = getSortedMap(params);
        String url = String.format("%s%s?%s", properties.getDomain(), properties.getPayAccess(), buildQuery(sortedParams));
        ObjectNode body = objectMapper.createObjectNode();
        body.put("data", url);
        return body;
    }

    /**
     * 发起请求
     *
     * @param bodyNode
     * @return
     */
    private JsonNode doExecute(ObjectNode bodyNode) {
        ObjectNode params = buildParams(bodyNode);
        params.remove("biz_content");
        Map<String, String> sortedParams = objectMapper.convertValue(params, new TypeReference<Map<String, String>>(){});
        URI uri = URI.create(String.format("%s%s?%s", properties.getDomain(), properties.getPayAccess(), buildQuery(sortedParams)));
        MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
        try {
            formData.add("biz_content", objectMapper.writeValueAsString(bodyNode));
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        Mono<ClientResponse> mono = webClient.post()
                .uri(uri)
                .body(BodyInserters.fromFormData(formData))
                .exchange();
        ClientResponse response = mono.block();
        JsonNode bodyJsonNode = objectMapper.createObjectNode();
        if (response != null) {
            Mono<String> resultMono = response.bodyToMono(String.class);
            String bodyRes = resultMono.block();
            Pattern pattern = Pattern.compile("[a-zA-Z_0-9]*_response"); //处理支付宝不同的返回格式，统一为response
            Matcher matcher = pattern.matcher(bodyRes);
            String body = matcher.replaceAll("response");
            try {
                bodyJsonNode = objectMapper.readTree(body);
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }

            if (response.statusCode().value() >= 200 && response.statusCode().value() < 300) {
                //支付宝公钥号
                String certSN = bodyJsonNode.get("alipay_cert_sn").asText();
                if (!getPublicKeyCertSN().equals(certSN)) {
                    // TODO: 证书可能过期了，需要处理支付宝公钥过期的情况
                    throw new RuntimeException("支付宝公钥过期");
                }
                //支付宝签名
                String signature = bodyJsonNode.get("sign").asText();

                //签名信息
                //这里的斜杠替换是因为json解析之后自动去掉了转义符/，签名的时候需要再给加上
                String signText = bodyJsonNode.get("response").toString().replace("/", "\\/");
                if (verify(signText, signature)) {
                    return bodyJsonNode;
                }
                // TODO: 签名校验失败，做异常处理
                throw new RuntimeException("签名校验失败");
            } else {
                // TODO: 返回异常，做异常处理
                throw new RuntimeException("返回异常，做异常处理");
            }
        }
        return bodyJsonNode;
    }


    public ObjectNode buildParams(ObjectNode bizContent) {
        ObjectNode paramNode = objectMapper.createObjectNode();
        paramNode.put("method", properties.getAccessMethod());
//        paramNode.put("method", "alipay.trade.query");
        paramNode.put("version", properties.getVersion());
        paramNode.put("app_id", properties.getPayAcctId());
        paramNode.put("sign_type", signType);
        paramNode.put("notify_url", bizContent.get("notify_url"));
        paramNode.put("return_url", bizContent.get("return_url"));
        paramNode.put("charset", charset);
        paramNode.put("app_cert_sn", properties.getPayCertNo());
        paramNode.put("alipay_root_cert_sn", properties.getPayRootCertNo());
        bizContent.remove("notify_url");
        bizContent.remove("return_url");
        Long timestamp = System.currentTimeMillis();
        DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        df.setTimeZone(TimeZone.getTimeZone("GMT+8"));
        paramNode.put("timestamp", df.format(new Date(timestamp)));
        paramNode.put("format", "json");
//        paramNode.put("auth_token", "");
        try {
            paramNode.put("biz_content", objectMapper.writeValueAsString(bizContent));
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        TreeMap<String, String> sortedParams = getSortedMap(paramNode);
        String signContent = getSignContent(sortedParams);
        paramNode.put("sign", sign(signContent));
        return paramNode;
    }

    public String buildQuery(Map<String, String> params) {
        if (params == null || params.isEmpty()) {
            return null;
        }

        StringBuilder query = new StringBuilder();
        Set<Map.Entry<String, String>> entries = params.entrySet();
        boolean hasParam = false;

        for (Map.Entry<String, String> entry : entries) {
            String name = entry.getKey();
            String value = entry.getValue();
            // 忽略参数名或参数值为空的参数
            if (areNotEmpty(name, value)) {
                if (hasParam) {
                    query.append("&");
                } else {
                    hasParam = true;
                }
                query.append(name).append("=").append(UriUtils.encode(value, charset));
            }
        }

        return query.toString();
    }


    public TreeMap<String, String> getSortedMap(ObjectNode paramNode) {
        return objectMapper.convertValue(paramNode, new TypeReference<TreeMap<String, String>>(){});
    }

    /**
     * @param sortedParams
     * @return
     */
    public String getSignContent(TreeMap<String, String> sortedParams) {
        StringBuilder content = new StringBuilder();
        List<String> keys = new ArrayList<String>(sortedParams.keySet());
        Collections.sort(keys);
        int index = 0;
        for (String key : keys) {
            String value = sortedParams.get(key);
            if (areNotEmpty(key, value)) {
                content.append(index == 0 ? "" : "&").append(key).append("=").append(value);
                index++;
            }
        }
        return content.toString();
    }

    /**
     * 检查指定的字符串是否为空。
     * <ul>
     * <li>SysUtils.isEmpty(null) = true</li>
     * <li>SysUtils.isEmpty("") = true</li>
     * <li>SysUtils.isEmpty("   ") = true</li>
     * <li>SysUtils.isEmpty("abc") = false</li>
     * </ul>
     *
     * @param value 待检查的字符串
     * @return true/false
     */
    public static boolean isEmpty(String value) {
        int strLen;
        if (value == null || (strLen = value.length()) == 0) {
            return true;
        }
        for (int i = 0; i < strLen; i++) {
            if ((Character.isWhitespace(value.charAt(i)) == false)) {
                return false;
            }
        }
        return true;
    }

    /**
     * 检查指定的字符串列表是否不为空。
     */
    public static boolean areNotEmpty(String... values) {
        boolean result = true;
        if (values == null || values.length == 0) {
            result = false;
        } else {
            for (String value : values) {
                result &= !isEmpty(value);
            }
        }
        return result;
    }

    /**
     * 签名
     *
     * @param signText 需要签名的字符串
     * @return
     */
    public String sign(String signText) {
        try {
            Signature sign = Signature.getInstance(properties.getSignType());
            sign.initSign(this.privateKey);
            sign.update(signText.getBytes());
            return Base64.getEncoder().encodeToString(sign.sign());
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("当前Java环境不支持SHA256withRSA", e);
        } catch (SignatureException e) {
            throw new RuntimeException("签名计算失败", e);
        } catch (InvalidKeyException e) {
            throw new RuntimeException("无效的私钥", e);
        }
    }

    /**
     * 获取证书。
     *
     * @param certString 证书内容
     * @return X509证书
     */
    public X509Certificate[] loadCertificates(String certString) {
        try {
            ByteArrayInputStream fis = new ByteArrayInputStream(certString.getBytes());
            CertificateFactory cf = CertificateFactory.getInstance("X509", provider);
            Collection<? extends Certificate> certs = cf.generateCertificates(fis);
            return (X509Certificate[]) certs.toArray(new X509Certificate[certs.size()]);
        } catch (CertificateExpiredException var3) {
            throw new RuntimeException("证书已过期", var3);
        } catch (CertificateNotYetValidException var4) {
            throw new RuntimeException("证书尚未生效", var4);
        } catch (CertificateException var5) {
            throw new RuntimeException("无效的证书", var5);
        }
    }


    /**
     * 获取证书。
     *
     * @param certString 证书内容
     * @return X509证书
     */
    public X509Certificate loadCertificate(String certString) {
        try {
            ByteArrayInputStream fis = new ByteArrayInputStream(certString.getBytes());
            CertificateFactory cf = CertificateFactory.getInstance("X509");
            X509Certificate cert = (X509Certificate) cf.generateCertificate(fis);
            cert.checkValidity();
            return cert;
        } catch (CertificateExpiredException var3) {
            throw new RuntimeException("证书已过期", var3);
        } catch (CertificateNotYetValidException var4) {
            throw new RuntimeException("证书尚未生效", var4);
        } catch (CertificateException var5) {
            throw new RuntimeException("无效的证书", var5);
        }
    }

    /**
     * RSA验签名检查
     *
     * @param content           待签名数据
     * @param signature              签名值
     * @return 布尔值
     */
    public boolean verify(String content, String signature) {
        try {
            Signature sign = Signature.getInstance(properties.getSignType());
            sign.initVerify(payPublicKey);
            sign.update(content.getBytes());
            return sign.verify(Base64.getDecoder().decode(signature));
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("当前Java环境不支持SHA256withRSA", e);
        } catch (SignatureException e) {
            throw new RuntimeException("签名验证过程发生了错误", e);
        } catch (InvalidKeyException e) {
            throw new RuntimeException("无效的证书", e);
        }
    }

    /**
     * 获取私钥。
     *
     * @param certString 私钥文件内容(required)
     * @return 私钥对象
     */
    public PrivateKey getPrivateKey(String certString) {
        try {
            String privateKey = certString
                    .replace("-----BEGIN PRIVATE KEY-----", "")
                    .replace("-----END PRIVATE KEY-----", "")
                    .replaceAll("\\s+", "");

            KeyFactory kf = KeyFactory.getInstance("RSA");
            return kf.generatePrivate(
                    new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)));
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("当前Java环境不支持RSA", e);
        } catch (InvalidKeySpecException e) {
            throw new RuntimeException("无效的密钥格式");
        }
    }

    /**
     * 获取公钥。
     *
     * @param certString 公钥文件内容(required)
     * @return 私钥对象
     */
    public static PublicKey getPublicKey(String certString) throws IOException {
        try {
            String publicKey = certString
                    .replace("-----BEGIN PUBLIC KEY-----", "")
                    .replace("-----END PUBLIC KEY-----", "")
                    .replaceAll("\\s+", "");

            KeyFactory kf = KeyFactory.getInstance("RSA");
            return kf.generatePublic(
                    new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey)));
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("当前Java环境不支持RSA", e);
        } catch (InvalidKeySpecException e) {
            throw new RuntimeException("无效的密钥格式");
        }
    }

    /**
     * 获取支付宝私钥证书号
     * @return
     */
    public String getPublicKeyCertSN() {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update((payPublicKey.getIssuerX500Principal().getName() + payPublicKey.getSerialNumber()).getBytes());
            String certSN = new BigInteger(1, md.digest()).toString(16);
            //BigInteger会把0省略掉，需补全至32位
            certSN = fillMD5(certSN);
            return certSN;
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }

    private String fillMD5(String md5) {
        return md5.length() == 32 ? md5 : fillMD5("0" + md5);
    }

}