package com.slodon.b2b2c.starter.client;

import cn.hutool.core.util.XmlUtil;
import com.github.binarywang.wxpay.bean.order.WxPayAppOrderResult;
import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult;
import com.github.binarywang.wxpay.bean.order.WxPayMwebOrderResult;
import com.github.binarywang.wxpay.bean.order.WxPayNativeOrderResult;
import com.github.binarywang.wxpay.bean.request.WxPayRefundRequest;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
import com.github.binarywang.wxpay.bean.result.WxPayOrderQueryResult;
import com.github.binarywang.wxpay.bean.result.WxPayRefundResult;
import com.github.binarywang.wxpay.config.WxPayConfig;
import com.github.binarywang.wxpay.service.WxPayService;
import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.slodon.b2b2c.core.config.DomainUrlUtil;
import com.slodon.b2b2c.core.exception.MallException;
import com.slodon.b2b2c.core.util.AssertUtil;
import com.slodon.b2b2c.starter.entity.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.DefaultHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContexts;

import javax.net.ssl.SSLContext;
import java.io.*;
import java.math.BigDecimal;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.text.SimpleDateFormat;
import java.util.*;

import static com.slodon.b2b2c.core.constant.PayConst.*;

/**
 * 微信支付客户端
 */
@Slf4j
public class SlodonWxPayClient extends SlodonPayClient {

    /**
     * 支付服务类
     */
    private WxPayService wxPayService = new WxPayServiceImpl();

    private static final String TRANS_URL = "https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers";

    /**
     * 支付构造函数
     *
     * @param request
     */
    public SlodonWxPayClient(SlodonPayRequest request) {
        super(request);
        WxPayProperties wxPayProperties = request.getWxPayProperties();
        AssertUtil.notNull(wxPayProperties, "微信支付参数配置无效");

        WxPayConfig payConfig = new WxPayConfig();
        payConfig.setAppId(StringUtils.trimToNull(wxPayProperties.getAppId()));
        payConfig.setMchId(StringUtils.trimToNull(wxPayProperties.getMchId()));
        payConfig.setMchKey(StringUtils.trimToNull(wxPayProperties.getMchKey()));
        payConfig.setKeyPath(StringUtils.trimToNull(wxPayProperties.getKeyPath()));
        // 可以指定是否使用沙箱环境
        payConfig.setUseSandboxEnv(false);

        this.wxPayService.setConfig(payConfig);
    }

    /**
     * 退款构造函数
     *
     * @param refundRequest
     */
    public SlodonWxPayClient(SlodonRefundRequest refundRequest) {
        super(refundRequest);
        WxPayProperties wxPayProperties = refundRequest.getWxPayProperties();
        AssertUtil.notNull(wxPayProperties, "微信支付参数配置无效");

        WxPayConfig payConfig = new WxPayConfig();
        payConfig.setAppId(StringUtils.trimToNull(wxPayProperties.getAppId()));
        payConfig.setMchId(StringUtils.trimToNull(wxPayProperties.getMchId()));
        payConfig.setMchKey(StringUtils.trimToNull(wxPayProperties.getMchKey()));
        payConfig.setKeyPath(StringUtils.trimToNull(wxPayProperties.getKeyPath()));
        // 可以指定是否使用沙箱环境
        payConfig.setUseSandboxEnv(false);

        this.wxPayService.setConfig(payConfig);
    }

    /**
     * 提现构造函数
     *
     * @param transfersRequest
     */
    public SlodonWxPayClient(SlodonTransfersRequest transfersRequest) {
        super(transfersRequest);
        WxPayProperties wxPayProperties = transfersRequest.getWxPayProperties();
        AssertUtil.notNull(wxPayProperties, "微信支付参数配置无效");

        WxPayConfig payConfig = new WxPayConfig();
        payConfig.setAppId(StringUtils.trimToNull(wxPayProperties.getAppId()));
        payConfig.setMchId(StringUtils.trimToNull(wxPayProperties.getMchId()));
        payConfig.setMchKey(StringUtils.trimToNull(wxPayProperties.getMchKey()));
        payConfig.setKeyPath(StringUtils.trimToNull(wxPayProperties.getKeyPath()));
        // 可以指定是否使用沙箱环境
        payConfig.setUseSandboxEnv(false);

        this.wxPayService.setConfig(payConfig);
    }

    /**
     * 微信支付
     *
     * @return
     * @throws Exception
     */
    @Override
    public SlodonPayResponse doPay() throws Exception {
        //支付单号拼接支付类型，作为微信支付的商户订单号，回调时要截取支付单号
        String outTradeNo = this.payRequest.getPaySn() + "_" + this.payRequest.getPayType();

        //查询支付状态
        this.checkPayState(null, outTradeNo, payRequest.getPayType());

        SlodonPayResponse response = new SlodonPayResponse();

        //将支付金额转为分
        Integer totalFee = payRequest.getPayAmount().multiply(new BigDecimal(100)).intValue();

        WxPayUnifiedOrderRequest unifiedOrderRequest = new WxPayUnifiedOrderRequest();
        unifiedOrderRequest.setBody(this.payRequest.getBody());
        unifiedOrderRequest.setOutTradeNo(outTradeNo);
        unifiedOrderRequest.setTotalFee(totalFee);
        unifiedOrderRequest.setSpbillCreateIp(this.payRequest.getIp());
        unifiedOrderRequest.setNotifyUrl(this.payRequest.getNotifyUrl());
        if (WX_TYPE_MINIAPP.equals(this.payRequest.getPayType())) {
            //小程序支付，在微信侧方式为jsapi
            unifiedOrderRequest.setTradeType(WX_TYPE_JSAPI);
        } else {
            unifiedOrderRequest.setTradeType(this.payRequest.getPayType());
        }
        //订单起止时间设置
        Date timeStart = new Date();
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(timeStart);
        //设置订单过期时间为5分钟
        calendar.add(Calendar.HOUR, 24);
        Date timeExpire = calendar.getTime();
        SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmss");
        unifiedOrderRequest.setTimeStart(format.format(timeStart));
        unifiedOrderRequest.setTimeExpire(format.format(timeExpire));
        //处理几种支付类型所需的特殊参数
        switch (this.payRequest.getPayType()) {
            case WX_TYPE_JSAPI:
            case WX_TYPE_MINIAPP:
                //公众号支付/小程序支付，获取openid
                unifiedOrderRequest.setOpenid(this.payRequest.getOpenId());
                break;
            case WX_TYPE_MWEB:
                //h5支付，拼接场景信息
                String scene_info = "{\"h5_info\":{\"type\": \"Wap\",\"wap_url\":" + DomainUrlUtil.SLD_API_URL + ",\"wap_name\": \"linePay\"}}";
                unifiedOrderRequest.setSceneInfo(scene_info);
                break;
            case WX_TYPE_NATIVE:
                //原生扫码支付，配置product_id
                unifiedOrderRequest.setProductId(this.payRequest.getPaySn());
                break;
            default:
        }

        //订单支付
        Object o = this.wxPayService.createOrder(unifiedOrderRequest);

        //跳转处理
        switch (this.payRequest.getPayType()) {
            case WX_TYPE_MWEB:
                //h5支付
                WxPayMwebOrderResult mwebResult = (WxPayMwebOrderResult) o;
                String mwebUrl = mwebResult.getMwebUrl();
                //返回地址
                String encode = URLEncoder.encode(this.payRequest.getReturnUrl(), "GBK");
                response.setActionType("redirect");
                response.setPayData(mwebUrl + "&redirect_url=" + encode);
                break;
            case WX_TYPE_APP:
                //app
                WxPayAppOrderResult appResult = (WxPayAppOrderResult) o;
                response.setActionType(null);
                response.setPayData(appResult);
                break;
            case WX_TYPE_JSAPI:
            case WX_TYPE_MINIAPP:
                //公众号支付/小程序支付,生成签名
                WxPayMpOrderResult jsResult = (WxPayMpOrderResult) o;
                response.setActionType(null);
                response.setPayData(jsResult);
                break;
            case WX_TYPE_NATIVE:
                //原生扫码支付，生成二维码所需的参数
                WxPayNativeOrderResult nativeOrderResult = (WxPayNativeOrderResult) o;
                response.setActionType("native");
                response.setPayData(this.paySuccess(nativeOrderResult.getCodeUrl()));
                break;
            default:
        }

        return response;
    }

    /**
     * 检测支付状态
     *
     * @param tradeNo    三方支付流水号
     * @param outTradeNo 商城的支付单号
     * @return
     */
    @Override
    public void checkPayState(String tradeNo, String outTradeNo, String payType) throws Exception {
        WxPayOrderQueryResult result = null;
        try {
            result = this.wxPayService.queryOrder(tradeNo, outTradeNo);
        } catch (Exception e) {
            //微信侧未创建订单，可以继续支付
        }

        if (result != null) {
            log.error("查询微信订单结果：" + result.getTradeState());
            //说明在微信侧已经创建了订单
            switch (result.getTradeState()) {
                case "SUCCESS":
                    //已支付，等待wx通知
                    throw new MallException("订单已支付，无需重复支付");
                case "USERPAYING":
                    //用户支付中
                    throw new MallException("正在支付中，请稍后重试");
                case "REFUND":
                    //转入退款，此订单为重复支付的订单
                    throw new MallException("订单无法支付,请联系管理员");
                case "NOTPAY":
                case "PAYERROR":
                    break;
                default:
                    //如果需要，执行关闭订单操作
            }
        }
    }

    /**
     * 执行退款
     */
    @Override
    public SlodonRefundResponse doRefund() throws Exception {
        //将支付金额转为分
        Integer refundFee = this.refundRequest.getRefundAmount().multiply(new BigDecimal(100)).intValue();
        Integer totalFee = this.refundRequest.getTotalAmount().multiply(new BigDecimal(100)).intValue();

        //构造业务参数
        WxPayRefundRequest refundRequest = new WxPayRefundRequest();
        refundRequest.setTransactionId(this.refundRequest.getTradeSn());
        refundRequest.setOutRefundNo(this.refundRequest.getRefundSn());
        refundRequest.setRefundFee(refundFee);
        refundRequest.setTotalFee(totalFee);
        //执行退款
        WxPayRefundResult wxPayRefundResult = this.wxPayService.refund(refundRequest);
        SlodonRefundResponse refundResponse = new SlodonRefundResponse();
        refundResponse.setSuccess("SUCCESS".equalsIgnoreCase(wxPayRefundResult.getResultCode()));
        refundResponse.setMsg(wxPayRefundResult.getErrCodeDes());
        return refundResponse;
    }

    /**
     * 执行转账
     */
    @Override
    public SlodonTransfersResponse doTransfers() throws Exception {
        WxPayProperties wxPayProperties = this.transfersRequest.getWxPayProperties();
        AssertUtil.notNull(wxPayProperties, "微信支付参数配置无效");
        //随机字符串，不长于32位
        String nonceStr = generateNonceStr();
        // 1.计算参数签名
        String paramStr = createLinkString(this.transfersRequest, nonceStr);
        String signStr = paramStr + "&key=" + wxPayProperties.getMchKey();
        String sign = DigestUtils.md5Hex(signStr).toUpperCase();
        System.out.println(sign);
        // 2.封装请求参数
        StringBuilder reqXmlStr = new StringBuilder();
        reqXmlStr.append("<xml>");
        reqXmlStr.append("<mchid>").append(wxPayProperties.getMchId()).append("</mchid>");
        reqXmlStr.append("<mch_appid>").append(wxPayProperties.getAppId()).append("</mch_appid>");
        reqXmlStr.append("<nonce_str>").append(nonceStr).append("</nonce_str>");
        reqXmlStr.append("<check_name>").append(this.transfersRequest.getCheckName()).append("</check_name>");
        reqXmlStr.append("<re_user_name>").append(this.transfersRequest.getRealName()).append("</re_user_name>");
        reqXmlStr.append("<openid>").append(this.transfersRequest.getOpenId()).append("</openid>");
        //将支付金额转为分
        Integer amount = this.transfersRequest.getAmount().multiply(new BigDecimal(100)).intValue();
        reqXmlStr.append("<amount>").append(amount).append("</amount>");
        reqXmlStr.append("<desc>").append(this.transfersRequest.getRemark()).append("</desc>");
        reqXmlStr.append("<sign>").append(sign).append("</sign>");
        reqXmlStr.append("<partner_trade_no>").append(this.transfersRequest.getTransferSn()).append("</partner_trade_no>");
        reqXmlStr.append("<spbill_create_ip>").append(this.transfersRequest.getSpBillCreateIp()).append("</spbill_create_ip>");
        reqXmlStr.append("</xml>");

        log.info("调用微信接口 request xml = " + reqXmlStr);
        // 3.加载证书请求接口
        String result = httpsRequest(TRANS_URL, reqXmlStr.toString(), wxPayProperties.getMchId());
        log.info("调用微信接口 response xml = " + result);
        if (result.contains("CDATA[FAIL]")) {
            log.error(("调用微信接口失败, 具体信息请查看访问日志"));
        }
        Map<String, Object> map = XmlUtil.xmlToMap(result);
        SlodonTransfersResponse response = new SlodonTransfersResponse();
        response.setSuccess("SUCCESS".equals(map.get("result_code")));
        response.setMsg(map.get("return_msg").toString());
        response.setTradeSn(map.getOrDefault("payment_no", "").toString());
        return response;
    }

    /**
     * 生成二维码并转为base64格式输出
     *
     * @param codeUrl 二维码链接
     * @return
     * @throws IOException
     * @throws WriterException
     */
    private String paySuccess(String codeUrl) throws IOException, WriterException {
        //将图片转化成base64进行输出
        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        Map<EncodeHintType, String> hints = new HashMap<>();
        // 内容所使用编码
        hints.put(EncodeHintType.CHARACTER_SET, "utf-8");
        BitMatrix bitMatrix = new MultiFormatWriter().encode(codeUrl, BarcodeFormat.QR_CODE, 300,
                300, hints);
        // 生成二维码
        String codeImg = "";
        MatrixToImageWriter.writeToStream(bitMatrix, "jpg", baos);
        codeImg = new String(Base64.encodeBase64(baos.toByteArray()));
        return codeImg;
    }

    /**
     * 构造签名参数
     *
     * @param request
     * @param nonceStr
     * @return
     */
    private static String createLinkString(SlodonTransfersRequest request, String nonceStr) {
        Map<String, Object> paramMap = new HashMap<>();

        WxPayProperties wxPayProperties = request.getWxPayProperties();

        //将支付金额转为分
        Integer amount = request.getAmount().multiply(new BigDecimal(100)).intValue();

        paramMap.put("mch_appid", wxPayProperties.getAppId());
        paramMap.put("mchid", wxPayProperties.getMchId());
        paramMap.put("openid", request.getOpenId());
        paramMap.put("amount", amount);
        paramMap.put("check_name", request.getCheckName());
        paramMap.put("re_user_name", request.getRealName());
        paramMap.put("desc", request.getRemark());
        paramMap.put("partner_trade_no", request.getTransferSn());
        paramMap.put("nonce_str", nonceStr);
        paramMap.put("spbill_create_ip", request.getSpBillCreateIp());

        List<String> keys = new ArrayList(paramMap.keySet());
        Collections.sort(keys);
        String prestr = "";
        for (int i = 0; i < keys.size(); i++) {
            String key = keys.get(i);
            Object value = paramMap.get(key);
            // 拼接时，不包括最后一个&字符
            if (i == keys.size() - 1) {
                prestr = prestr + key + "=" + value;
            } else {
                prestr = prestr + key + "=" + value + "&";
            }
        }
        return prestr;
    }

    /**
     * 获取随机字符串 Nonce Str
     *
     * @return String 随机字符串
     */
    public static String generateNonceStr() {
        String base = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
        Random random = new SecureRandom();
        char[] nonceChars = new char[32];
        for (int index = 0; index < nonceChars.length; ++index) {
            nonceChars[index] = base.charAt(random.nextInt(base.length()));
        }
        return new String(nonceChars);
    }

    /**
     * 带证书httpPost请求
     *
     * @param url   接口地址
     * @param param 参数
     * @param mchId 商户号
     * @return
     * @throws Exception
     */
    public static String httpsRequest(String url, String param, String mchId) throws Exception {
        //PKCS12的密码
        String PKCS12 = mchId;
        //指定读取证书格式为PKCS12
        KeyStore keyStore = KeyStore.getInstance("PKCS12");
        //读取本机存放的PKCS12证书
        InputStream instream = SlodonWxPayClient.class.getClassLoader().getResourceAsStream("wx_cert/apiclient_cert.p12");
        //指定PKCS12的密码
        keyStore.load(instream, PKCS12.toCharArray());
        //指定TLS版本
        SSLContext sslcontext = SSLContexts.custom()
                .loadKeyMaterial(keyStore, PKCS12.toCharArray())
                .build();
        //设置httpclient的SSLSocketFactory
        SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
                sslcontext,
                new String[]{"TLSv1"},
                null,
                new DefaultHostnameVerifier());
        CloseableHttpClient httpclient = HttpClients.custom()
                .setSSLSocketFactory(sslsf)
                .build();
        StringBuilder stringBuffer = new StringBuilder();
        try {
            HttpPost httpPost = new HttpPost(url);
            InputStream is = new ByteArrayInputStream(param.getBytes(StandardCharsets.UTF_8));
            //InputStreamEntity严格是对内容和长度相匹配的。用法和BasicHttpEntity类似
            InputStreamEntity inputStreamEntity = new InputStreamEntity(is, is.available());
            httpPost.setEntity(inputStreamEntity);
            CloseableHttpResponse response = httpclient.execute(httpPost);
            try {
                HttpEntity entity = response.getEntity();
                BufferedReader reader = new BufferedReader(new InputStreamReader(
                        entity.getContent(), StandardCharsets.UTF_8));
                String inputLine;
                while ((inputLine = reader.readLine()) != null) {
                    stringBuffer.append(inputLine);
                }
            } finally {
                response.close();
            }
        } finally {
            httpclient.close();
        }
        return stringBuffer.toString();
    }
}
