package com.starry.admin.common.play.wx;

import com.starry.common.utils.IdUtils;
import cn.hutool.crypto.digest.MD5;
import com.github.wxpay.sdk.WXPayUtil;
import com.starry.admin.common.exception.CustomException;
import com.starry.common.utils.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.*;

/**
 * 微信支付辅助类
 *
 * @author admin
 */
@Slf4j
public class WxCustomPayUtils {

    private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";

    private static final Random RANDOM = new SecureRandom();


    /**
     * 获取微信支付请求参数（map格式）
     *
     * @param openId         微信公众号ID
     * @param appid          用户ID
     * @param mchId          商户ID
     * @param orderId        订单ID
     * @param nonceStr       订单ID
     * @param spBillCreateIp 终端设备ID
     * @param body           商品描述
     * @param totalFee       商品金额
     * @param attach         附加数据，在查询API和支付通知中原样返回，可作为自定义参数使用（传入租户ID）
     */
    public static Map<String, String> getPayRequestParameters(String openId, String appid, String mchId, String orderId, String nonceStr, String spBillCreateIp, String body, String attach, long totalFee) {
        Map<String, String> playRequestParameters = new HashMap<>(16);
        playRequestParameters.put("openId", openId);
        playRequestParameters.put("appid", appid);
        playRequestParameters.put("mch_id", mchId);
        playRequestParameters.put("nonce_str", nonceStr);
        playRequestParameters.put("body", body);
        playRequestParameters.put("attach", attach);
        playRequestParameters.put("out_trade_no", orderId);
        playRequestParameters.put("total_fee", String.valueOf(totalFee));
        playRequestParameters.put("spbill_create_ip", spBillCreateIp);
        playRequestParameters.put("notify_url", WeChatConstants.NOTIFY_URL);
        playRequestParameters.put("trade_type", "JSAPI");
        return playRequestParameters;
    }

    /**
     * @param playRequestParameters 微信支付请求参数
     * @param orderId               订单ID
     * @param sign                  签名信息
     */
    public static String unifiedOrderJsApi(Map<String, String> playRequestParameters, String orderId, String sign) throws Exception {
        playRequestParameters.put("sign", sign);
        // 将所有参数(map)转xml格式
        String xml = WXPayUtil.mapToXml(playRequestParameters);
        String newXml = new String(xml.getBytes(StandardCharsets.UTF_8));
        log.error("xml:{}", newXml);
        // 发送post请求"统一下单接口"返回预支付id:prepay_id
        String xmlStr = HttpUtils.sendPost(WeChatConstants.UNIFIEDORDER_URL, newXml);
        log.info("xmlStr:{}", xmlStr);
        if (StringUtils.isBlank(xmlStr)) {
            throw new CustomException("微信支付返回信息为空");
        }
        Map<String, String> map = WXPayUtil.xmlToMap(xmlStr);
        // 如果预支付返回状态码为不为SUCCESS,说明预支付接口调用失败(通讯业务失败),抛出异常,业务进行处理
        if (!map.get("return_code").equals("SUCCESS")) {
            throw new CustomException(map.get("return_msg"));
        }
        // 如果预支付返回状态码为不为SUCCESS,说明预支付接口调用失败(交易业务失败),抛出异常,业务进行处理
        if (!map.get("result_code ").equals("SUCCESS")) {
            throw new CustomException(map.get("err_code_des"));
        }
        return map.get("prepay_id");
    }

    /**
     * 订单支付状态查询
     *
     * @param wechat_appid
     * @param wechat_mchid
     * @param wechat_seckey
     * @param out_trade_no
     * @return
     * @throws Exception
     */
    public static Map<String, String> orderQuery(String wechat_appid, String wechat_mchid, String wechat_seckey, String out_trade_no) throws Exception {
        // 拼接 参数
        Map<String, String> paraMap = new HashMap<String, String>();
        paraMap.put("appid", wechat_appid);
        paraMap.put("mch_id", wechat_mchid);
        paraMap.put("nonce_str", IdUtils.getUuid());
        paraMap.put("out_trade_no", out_trade_no);// 订单号
        String sign = WXPayUtil.generateSignature(paraMap, wechat_seckey);
        paraMap.put("sign", sign);
        String xml = WXPayUtil.mapToXml(paraMap);// 将所有参数(map)转xml格式
        String xmlStr = HttpUtils.sendPost(WeChatConstants.ORDERQUERY_URL, xml);// 发送post请求"统一下单接口"返回预支付id:prepay_id
        return WXPayUtil.xmlToMap(xmlStr);
    }

    /**
     * XML格式字符串转换为Map
     *
     * @param strXML XML字符串
     * @return XML数据转换后的Map
     */
    public static Map<String, String> xmlToMap(String strXML) throws Exception {
        Map<String, String> data = new HashMap<>();
        DocumentBuilder documentBuilder = WXPayXmlUtil.newDocumentBuilder();
//            InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8"));
        InputStream stream = new ByteArrayInputStream(strXML.getBytes("GBK"));
        Document doc = documentBuilder.parse(stream);
        doc.getDocumentElement().normalize();
        NodeList nodeList = doc.getDocumentElement().getChildNodes();
        for (int idx = 0; idx < nodeList.getLength(); ++idx) {
            Node node = nodeList.item(idx);
            if (node.getNodeType() == Node.ELEMENT_NODE) {
                Element element = (Element) node;
                data.put(element.getNodeName(), element.getTextContent());
            }
        }
        try {
            stream.close();
        } catch (Exception ignored) {

        }
        return data;
    }


    /**
     * 将Map转换为XML格式的字符串
     *
     * @param data Map类型数据
     * @return XML格式的字符串
     * @throws Exception 系统异常
     */
    public static String mapToXml(Map<String, String> data) throws Exception {
        Document document = WXPayXmlUtil.newDocument();
        Element root = document.createElement("xml");
        document.appendChild(root);
        for (String key : data.keySet()) {
            String value = data.get(key);
            if (value == null) {
                value = "";
            }
            value = value.trim();
            Element filed = document.createElement(key);
            filed.appendChild(document.createTextNode(value));
            root.appendChild(filed);
        }
        TransformerFactory tf = TransformerFactory.newInstance();
        Transformer transformer = tf.newTransformer();
        DOMSource source = new DOMSource(document);
        transformer.setOutputProperty(OutputKeys.ENCODING, "GBK");
//        transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        StringWriter writer = new StringWriter();
        StreamResult result = new StreamResult(writer);
        transformer.transform(source, result);
        String output = writer.getBuffer().toString();
        try {
            writer.close();
        } catch (Exception ignored) {
        }
        return output;
    }


    /**
     * 生成带有 sign 的 XML 格式字符串
     *
     * @param data Map类型数据
     * @param key  API密钥
     * @return 含有sign字段的XML
     */
    public static String generateSignedXml(final Map<String, String> data, String key) throws Exception {
        return generateSignedXml(data, key, WeChatConstants.SignType.MD5);
    }

    /**
     * 生成带有 sign 的 XML 格式字符串
     *
     * @param data     Map类型数据
     * @param key      API密钥
     * @param signType 签名类型
     * @return 含有sign字段的XML
     */
    public static String generateSignedXml(final Map<String, String> data, String key, WeChatConstants.SignType signType) throws Exception {
        String sign = generateSignature(data, key, signType);
        data.put(WeChatConstants.FIELD_SIGN, sign);
        return mapToXml(data);
    }


    /**
     * 判断签名是否正确
     *
     * @param xmlStr XML格式数据
     * @param key    API密钥
     * @return 签名是否正确
     * @throws Exception 系统异常
     */
    public static boolean isSignatureValid(String xmlStr, String key) throws Exception {
        Map<String, String> data = xmlToMap(xmlStr);
        if (!data.containsKey(WeChatConstants.FIELD_SIGN)) {
            return false;
        }
        String sign = data.get(WeChatConstants.FIELD_SIGN);
        return generateSignature(data, key).equals(sign);
    }

    /**
     * 判断签名是否正确，必须包含sign字段，否则返回false。使用MD5签名。
     *
     * @param data Map类型数据
     * @param key  API密钥
     * @return 签名是否正确
     * @throws Exception 系统异常
     */
    public static boolean isSignatureValid(Map<String, String> data, String key) throws Exception {
        return isSignatureValid(data, key, WeChatConstants.SignType.MD5);
    }

    /**
     * 判断签名是否正确，必须包含sign字段，否则返回false。
     *
     * @param data     Map类型数据
     * @param key      API密钥
     * @param signType 签名方式
     * @return 签名是否正确
     * @throws Exception 系统异常
     */
    public static boolean isSignatureValid(Map<String, String> data, String key, WeChatConstants.SignType signType) throws Exception {
        if (!data.containsKey(WeChatConstants.FIELD_SIGN)) {
            return false;
        }
        String sign = data.get(WeChatConstants.FIELD_SIGN);
        return generateSignature(data, key, signType).equals(sign);
    }

    /**
     * 生成签名
     *
     * @param data 待签名数据
     * @param key  API密钥
     * @return 签名
     */
    public static String generateSignature(final Map<String, String> data, String key) throws Exception {
        return generateSignature(data, key, WeChatConstants.SignType.MD5);
    }

    /**
     * 生成签名. 注意，若含有sign_type字段，必须和signType参数保持一致。
     *
     * @param data     待签名数据
     * @param key      API密钥
     * @param signType 签名方式
     * @return 签名
     */
    public static String generateSignature(final Map<String, String> data, String key, WeChatConstants.SignType signType) throws Exception {
        Set<String> keySet = data.keySet();
        String[] keyArray = keySet.toArray(new String[0]);
        Arrays.sort(keyArray);
        StringBuilder sb = new StringBuilder();
        for (String k : keyArray) {
            if (k.equals(WeChatConstants.FIELD_SIGN)) {
                continue;
            }
            // 参数值为空，则不参与签名
            if (!data.get(k).trim().isEmpty()) {
                sb.append(k).append("=").append(data.get(k).trim()).append("&");
            }
        }
        sb.append("key=").append(key);
        if (WeChatConstants.SignType.MD5.equals(signType)) {
            return MD5.create().digestHex(sb.toString()).toUpperCase();
        } else if (WeChatConstants.SignType.HMACSHA256.equals(signType)) {
            return HmacSHA256(sb.toString(), key);
        } else {
            throw new Exception(String.format("Invalid sign_type: %s", signType));
        }
    }


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


//    /**
//     * 生成 MD5
//     *
//     * @param data 待处理数据
//     * @return MD5结果
//     */
//    public static String MD5(String data) throws Exception {
//        MessageDigest md = MessageDigest.getInstance("MD5");
//        byte[] array = md.digest(data.getBytes(StandardCharsets.UTF_8));
//        StringBuilder sb = new StringBuilder();
//        for (byte item : array) {
//            sb.append(Integer.toHexString((item & 0xFF) | 0x100), 1, 3);
//        }
//        return sb.toString().toUpperCase();
//    }

    /**
     * 生成 HmacSHA256
     *
     * @param data 待处理数据
     * @param key  密钥
     * @return 加密结果
     * @throws NoSuchAlgorithmException,InvalidKeyException 系统异常
     */
    public static String HmacSHA256(String data, String key) throws NoSuchAlgorithmException, InvalidKeyException {
        Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
        SecretKeySpec secret_key = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
        sha256_HMAC.init(secret_key);
        byte[] array = sha256_HMAC.doFinal(data.getBytes(StandardCharsets.UTF_8));
        StringBuilder sb = new StringBuilder();
        for (byte item : array) {
            sb.append(Integer.toHexString((item & 0xFF) | 0x100), 1, 3);
        }
        return sb.toString().toUpperCase();
    }

    /**
     * 获取当前时间戳，单位秒
     */
    public static long getCurrentTimestamp() {
        return System.currentTimeMillis() / 1000;
    }

}
