/**
 * cljpay.com
 * Copyright (C) 2015-2023 All Rights Reserved.
 */package com.jeequan.jeepay.thirdparty.service.zw.uils;

import com.alibaba.fastjson.JSONObject;
import com.clj.leshuapay.sdk.exception.LeshuaException;
import com.clj.leshuapay.sdk.util.LeshuaSignature;
import com.clj.leshuapay.sdk.util.SignUtil;
import com.jeequan.jeepay.core.utils.JeepayKit;
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.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.xml.sax.SAXException;

import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author yangjun
 * @version SignatureUtil.java, v 0.1 2023-02-20 14:47 yangjun
 */
@Slf4j
public class SignatureUtil {

    private static final String SECRET_KEY = "key";

    private static Pattern UNDERLINE_PATTERN = Pattern.compile("[A-Z]");

    /**
     * 将对象转换成map，key为属性名，value为属性值，值为null的属性排除
     *
     * @param obj
     * @return
     */
    public static Map<String, String> toMap(Object obj) {
        Map<String, String> map = new ConcurrentSkipListMap<>();
        Field[] fields = obj.getClass().getDeclaredFields();
        for (Field field : fields) {
            try {
                field.setAccessible(true);
                if (Modifier.isStatic(field.getModifiers())) {
                    continue;
                }
                Object val = field.get(obj);
                if (val != null) {
                    String key = field.getName();
                    map.put(camelToUnderline(key), String.valueOf(val));
                }
            } catch (IllegalArgumentException | IllegalAccessException e) {
                log.error(e.getMessage(), e);
            }
        }
        return map;
    }


    /**
     * 驼峰转下划线
     */
    public static String camelToUnderline(String str) {
        Matcher matcher = UNDERLINE_PATTERN.matcher(str);
        StringBuffer sb = new StringBuffer();
        while (matcher.find()) {
            matcher.appendReplacement(sb, "_" + matcher.group(0).toLowerCase());
        }
        matcher.appendTail(sb);
        return sb.toString();
    }


    /**
     * 计算签名
     *
     * @param paramMap
     * @param key
     * @return
     */
    public static String getMD5Sign(Map<String, Object> paramMap, String key) {
        if (paramMap == null || paramMap.isEmpty()) {
            log.warn("待签名参数map为空!");
            return null;
        }
        paramMap.remove("md5");
        String result = JeepayKit.getSign(paramMap, key);
        return result;
    }

    /**
     * 计算签名
     *
     * @param paramMap
     * @param exludedSignParams 计算签名排除的参数
     * @param key
     * @return
     */
    public static String generateMd5Sign(Map<String, Object> paramMap, Set<String> exludedSignParams, String key) {
        if (paramMap == null || paramMap.isEmpty()) {
            log.warn("待签名参数map为空!");
            return null;
        }
        String str2Sign = generateMd5SignStr(paramMap, exludedSignParams, key);
        String result = DigestUtils.md5Hex(str2Sign).toUpperCase();
        log.info("MD5签名原始串：{}，签名结果：{}", new Object[]{str2Sign, result});
        return result;
    }

    /**
     * 从API返回的XML数据里面重新计算一次签名
     *
     * @param responseString
     * @param exludedSignParams
     * @param key
     * @return
     * @throws DocumentException
     */
    public static String getSignFromResponseString(String responseString, Set<String> exludedSignParams, String key) throws DocumentException {
        Map<String, String> map = getMapFromXML(responseString);
        //将API返回的数据根据用签名算法进行计算新的签名，用来跟API返回的签名进行比较
        return LeshuaSignature.getMD5Sign(map, exludedSignParams, key);
    }

    public static void main(String[] args) throws DocumentException {
        String jsonStr = "{\"bill_data\":\"      财来聚美食城收银小票\\n收银员：店长（1001)\\n消费流水：202302241642010040060\\n牌号：0060\\n打印时间：2023-02-24 16:42:01\\n--------------------------------\\n商品名称     单价  数量  小计\\n无码商品\\n                   1     0.1\\n--------------------------------\\n总计：0.1     支付方式：现金:0.1\\n应收：0.1\\n实收：0.1     找零：0.0\\n\",\"uuid\":\"10482023022416422904\",\"sn\":\"CP1502210080002\",\"paycode\":\"288193019487752692\",\"shopid\":\"M1663659251\",\"termid\":\"1048\",\"md5\":\"6C0395EDF01C5392E2E4B42C2992ED3B\",\"amount\":\"0.10\"}";

        String jsonStr2 = "{\"bill_data\":\"      财来聚美食城收银小票\\n收银员：店长（1001)\\n消费流水：202302271205537060002\\n牌号：0002\\n打印时间：2023-02-27 12:05:54\\n--------------------------------\\n商品名称     单价  数量  小计\\n无码商品\\n                   1     0.02\\n--------------------------------\\n总计：0.02    支付方式：现金:0.02\\n应收：0.02\\n实收：0.02    找零：0.00\\n\",\"uuid\":\"10482023022712060406\",\"sn\":\"CP1502210080002\",\"paycode\":\"130300082400702149\",\"shopid\":\"M1663659251\",\"termid\":\"1048\",\"md5\":\"E6B0249A3C95F4CCF6BE255C624FF7EB\",\"amount\":\"0.02\"}";

        boolean abc = checkIsSignValidFromResponseString(jsonStr2, "B5BB463ADA7B68B1005CC25D2E5BEDC7");
        log.info("========>" + abc);
    }


    /**
     * 检查api返回数据的签名
     *
     * @param responseString
     * @param key
     * @return
     * @throws DocumentException
     */
    public static boolean checkIsSignValidFromResponseString(String responseString, String key) {
        Map<String, Object> map = JSONObject.parseObject(responseString, Map.class);

        String sign = map.get("md5").toString();
        if (StringUtils.isEmpty(sign)) {
            log.error("API返回的数据签名数据不存在");
            return false;
        }
        log.debug("服务器回包里面的签名是：{}", sign);
        // 将API返回的数据根据用签名算法进行计算新的签名，用来跟API返回的签名进行比较
        String verSign = getMD5Sign(map, key);

        if (!sign.equalsIgnoreCase(verSign)) {
            // 签名验不过，表示这个API返回的数据有可能已经被篡改了
            log.error("API返回的数据签名验证不通过，verSign={}，sign={}", verSign, sign);
            return false;
        }
        return true;
    }

    /**
     * 拼接用于签名的参数
     *
     * @param paramMap
     * @param md5Key
     * @return
     */
    private static String buildParam4Sign(Map<String, String> paramMap, String md5Key) {
        Set<String> keySet = paramMap.keySet();
        StringBuilder param = new StringBuilder(20 * keySet.size());
        String[] keys = keySet.toArray(new String[keySet.size()]);
        Arrays.sort(keys);
        for (String key : keys) {
//            if (exludedSignParams != null && exludedSignParams.contains(key)) {
//                continue;
//            }
            Object value = paramMap.get(key);
            // 排除值为null的情况
            if (value != null) {
                param.append(key).append("=").append(value).append("&");
            }
        }
        param.append("key").append("=").append(md5Key);
        return param.toString();
    }

    /**
     * 拼接用于签名的参数
     *
     * @param paramMap
     * @param exludedSignParams
     * @param md5Key
     * @return
     */
    private static String generateMd5SignStr(Map<String, Object> paramMap, Set<String> exludedSignParams, String md5Key) {
        Set<String> keySet = paramMap.keySet();
        StringBuilder param = new StringBuilder(20 * keySet.size());
        String[] keys = keySet.toArray(new String[keySet.size()]);
        Arrays.sort(keys);
        for (String key : keys) {
            if (exludedSignParams != null && exludedSignParams.contains(key)) {
                continue;
            }
            Object value = paramMap.get(key);
            // 排除值为null的情况
            if (value != null) {
                param.append(key).append("=").append(value).append("&");
            }
        }
        param.append(SECRET_KEY).append("=").append(md5Key);
        return param.toString();
    }

    /**
     * 将XML字符串转换成map
     *
     * @param xmlString
     * @return
     * @throws DocumentException
     * @throws ParserConfigurationException
     * @throws IOException
     * @throws SAXException
     */
    public static Map<String, String> getMapFromXML(String xmlString) throws DocumentException {
        // 这里用dom的方式解析回包的最主要目的是防止API新增回包字段
        org.dom4j.Document document = DocumentHelper.parseText(xmlString);
        Element root = document.getRootElement();
        List<?> eleList = root.elements();
        if (eleList != null) {
            Map<String, String> map = new HashMap<String, String>(eleList.size());
            for (Object obj : eleList) {
                if (!(obj instanceof Element)) {
                    log.warn("节点[{}]不是Element，忽略！", obj);
                    continue;
                }
                Element ele = (Element) obj;
                map.put(ele.getName(), ele.getText());
            }
            return map;
        }
        return Collections.emptyMap();

    }


    /**
     * 计算base64签名，用于商户进件等
     *
     * @param agentKey
     * @param data
     * @return
     * @throws UnsupportedEncodingException
     */
    public static String getBase64Sign(String agentKey, JSONObject data) throws UnsupportedEncodingException {
        StringBuilder sb = new StringBuilder();
        sb.append(SignUtil.SING_PREFIX).append(agentKey).append(data);
        return Base64.encodeBase64String(DigestUtils.md5Hex(sb.toString()).getBytes(SignUtil.DEFAULT_CHARSET));
    }

    public static String getRiskBase64Sign(String agentKey, String data) throws LeshuaException {
        StringBuilder sb = new StringBuilder();
        sb.append(SignUtil.SING_PREFIX).append(agentKey).append(data);
        try {
            return Base64.encodeBase64String(DigestUtils.md5Hex(sb.toString()).toLowerCase().getBytes(SignUtil.DEFAULT_CHARSET));
        } catch (Exception e) {
            throw new LeshuaException("签名获取失败", e);
        }
    }

    public static Set<String> reqExcludedSignParams() {
        Set<String> excludedSignParams = new HashSet<String>(1);
        excludedSignParams.add("md5");
        return excludedSignParams;
    }

    /**
     * 响应结果不参与计算签名的参数
     *
     * @return
     */
    public static Set<String> resExcludedSignParams() {
        Set<String> excludedSignParams = new HashSet<>(2);
        excludedSignParams.add("sign");
        excludedSignParams.add("resp_code");
        return excludedSignParams;
    }


    /**
     * 响应结果不参与计算签名的参数
     *
     * @return
     */
    public static Set<String> notifyExcludedSignParams() {
        Set<String> excludedSignParams = new HashSet<>(2);
        excludedSignParams.add("sign");
        excludedSignParams.add("error_code");
        return excludedSignParams;
    }

}