package com.yida.data.common.core.utils;

import cn.hutool.core.codec.Base64Encoder;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.crypto.digest.HMac;
import cn.hutool.crypto.digest.HmacAlgorithm;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.yida.data.common.core.common.ResultMsgType;
import com.yida.data.common.core.entity.constant.StringConstant;
import com.yida.data.common.core.entity.constant.UnionPayConstant;
import com.yida.data.common.core.entity.pay.UnionPayOrder;
import com.yida.data.common.core.entity.pay.UnionRefund;
import com.yida.data.common.core.entity.system.UnionPayConfig;
import com.yida.data.common.core.enums.UnionSignTypeEnum;
import com.yida.data.common.core.exception.FebsException;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

import java.math.BigDecimal;
import java.net.URLEncoder;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * 银联云闪付支付工具类
 * 当前暂时只针对一个子商户的概念
 * 平台方和主商户入账金额为0，子商户入全金额
 *
 * @author ZYJ
 * @date 2022/6/14
 */
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class UnionPayUtil {

    /**
     * 生成银联云闪付支付地址
     *
     * @param payConfig 银联云闪付配置信息
     * @param order     银联云闪付订单信息
     * @return java.lang.String
     * @author ZYJ
     * @date 2022/6/16 15:19
     */
    public static String placeOrder(UnionPayConfig payConfig, UnionPayOrder order) {

        // 设置主商户配置信息
        UnionPayConfig config = Objects.nonNull(payConfig.getParentConfig()) ? payConfig.getParentConfig() : payConfig;
        // 生成业务参数
        Map<String, Object> map = new HashMap<>(16);
        // 消息ID. 对应我方流水号
        map.put("msgId", order.getTransactionNumber());
        // 时间
        Date date = new Date();
        map.put("requestTimestamp", DateUtil.formatDateTime(date));
        // 订单号
        map.put("merOrderId", order.getOrderNo());
        // 商户号
        map.put("mid", config.getMchId());
        // 终端号
        map.put("tid", config.getTerminalId());
        // 业务类型
        map.put("instMid", "H5DEFAULT");
        // 商户附加数据. 小于255字符
        map.put("attachedData", order.getAttachedData());
        map.put("orderDesc", StringUtils.isEmpty(order.getOrderDesc()) ? "缴费账单" : order.getOrderDesc());
        // 支付总金额. 单位分
        map.put("totalAmount", NumberUtil.mul(order.getPayMoney(), new BigDecimal(100)));
        // 订单过期时间. 15分钟
        map.put("expireTime", DateUtil.formatDateTime(DateUtil.offsetMinute(date, 15)));
        // 支付结果通知地址. 小于255字符
        map.put("notifyUrl", order.getNotifyUrl());
        // 网页跳转地址. 小于255字符
        map.put("returnUrl", order.getReturnUrl());
        // 判断是否需要设置子订单信息
        if (Objects.nonNull(payConfig.getParentConfig())) {
            // 分账标记
            map.put("divisionFlag", true);
            // 平台商户分账金额
            map.put("platformAmount", 0);
            // 子订单
            map.put("subOrders", JSONUtil.createArray().set(
                    JSONUtil.createObj()
                            .set("mid", payConfig.getMchId())
                            .set("totalAmount", NumberUtil.mul(order.getPayMoney(), new BigDecimal(100)))
            ));
        }
        log.info("银联云闪付下单请求参数: {}", map);
        // 根据支付类型获取请求地址
        String requestUrl = order.getPayTypeEnum().getUrl();
        // 生成支付地址
        String url = getPayUrl(requestUrl, config.getAppId(), config.getAppKey(),
                DateUtil.format(date, DatePattern.PURE_DATETIME_PATTERN),
                RandomUtil.randomString(64), JSONUtil.toJsonStr(map));
        log.info("银联云闪付支付地址: {}", url);
        return url;
    }

    /**
     * 订单查询
     * 请求方式: HTTP(S) + JSON
     *
     * @param payConfig   银联云闪付配置信息
     * @param accessToken token值
     * @param orderNo     商户订单号
     * @return cn.hutool.json.JSONObject
     * @author ZYJ
     * @date 2022/6/27 17:53
     */
    public static JSONObject queryOrder(UnionPayConfig payConfig, String accessToken, String orderNo) {
        // 设置主商户配置信息
        UnionPayConfig config = Objects.nonNull(payConfig.getParentConfig()) ? payConfig.getParentConfig() : payConfig;
        // 封装参数
        JSONObject map = JSONUtil.createObj()
                .set("requestTimestamp", DateUtil.formatDateTime(new Date()))
                // 商户号
                .set("mid", config.getMchId())
                // 终端号
                .set("tid", config.getTerminalId())
                // 业务类型
                .set("instMid", "H5DEFAULT")
                // 订单号
                .set("merOrderId", orderNo);

        HttpResponse httpResponse = HttpRequest.post(UnionPayConstant.QUERY_ORDER)
                .header("Authorization", setAccessTokenHeader(accessToken))
                .body(map.toString())
                .execute();
        // 返回数据
        log.info("订单查询接口返回数据: {}", httpResponse.body());
        JSONObject jsonObject = JSONUtil.parseObj(httpResponse.body());
        if (jsonObject.containsKey(UnionPayConstant.CODE_NAME)
                && !UnionPayConstant.SUCCESS_CODE.equals(jsonObject.getStr(UnionPayConstant.CODE_NAME))
                && !UnionPayConstant.NO_ORDER_CODE.equals(jsonObject.getStr(UnionPayConstant.CODE_NAME))
                && !UnionPayConstant.TARGET_FAIL_CODE.equals(jsonObject.getStr(UnionPayConstant.CODE_NAME))
        ) {
            log.error("订单查询失败, param : {}, code: {}, msg: {}", map, jsonObject.getStr(UnionPayConstant.CODE_NAME),
                    jsonObject.getStr(UnionPayConstant.MSG_NAME));
            throw new FebsException(ResultMsgType.ORDER_QUERY_FAIL.getValue());
        }
        return jsonObject;
    }

    /**
     * 关闭订单
     * 请求方式: HTTP(S) + JSON
     *
     * @param payConfig   银联云闪付配置信息
     * @param accessToken token值
     * @param orderNo     商户订单号
     * @author ZYJ
     * @date 2022/6/16 16:07
     */
    public static void closeOrder(UnionPayConfig payConfig, String accessToken, String orderNo) {
        // 设置主商户配置信息
        UnionPayConfig config = Objects.nonNull(payConfig.getParentConfig()) ? payConfig.getParentConfig() : payConfig;
        // 封装参数
        JSONObject map = JSONUtil.createObj()
                .set("requestTimestamp", DateUtil.formatDateTime(new Date()))
                // 商户号
                .set("mid", config.getMchId())
                // 终端号
                .set("tid", config.getTerminalId())
                // 业务类型
                .set("instMid", "H5DEFAULT")
                // 订单号
                .set("merOrderId", orderNo);

        HttpResponse httpResponse = HttpRequest.post(UnionPayConstant.CLOSE_ORDER)
                .header("Authorization", setAccessTokenHeader(accessToken))
                .body(map.toString())
                .execute();
        // 返回数据
        log.info("关闭订单接口返回数据: {}", httpResponse.body());
        JSONObject jsonObject = JSONUtil.parseObj(httpResponse.body());
        if (jsonObject.containsKey(UnionPayConstant.CODE_NAME)
                && !UnionPayConstant.SUCCESS_CODE.equals(jsonObject.getStr(UnionPayConstant.CODE_NAME))
                && !UnionPayConstant.NO_ORDER_CODE.equals(jsonObject.getStr(UnionPayConstant.CODE_NAME))
                && !UnionPayConstant.TARGET_FAIL_CODE.equals(jsonObject.getStr(UnionPayConstant.CODE_NAME))) {
            log.error("关闭订单失败, param: {}, code: {}, msg: {}", map, jsonObject.getStr(UnionPayConstant.CODE_NAME),
                    jsonObject.getStr(UnionPayConstant.MSG_NAME));
            throw new FebsException(ResultMsgType.ORDER_CLOSE_FAIL.getValue());
        }
    }

    /**
     * 退款
     * 当前暂时只针对一个子商户的概念
     * 请求方式: HTTP(S) + JSON
     *
     * @param payConfig   银联云闪付配置信息
     * @param refund      银联云闪付退款信息
     * @param accessToken token值
     * @return cn.hutool.json.JSONObject
     * @author ZYJ
     * @date 2022/6/27 18:04
     */
    public static JSONObject refund(UnionPayConfig payConfig, UnionRefund refund, String accessToken) {
        // 设置主商户配置信息
        UnionPayConfig config = Objects.nonNull(payConfig.getParentConfig()) ? payConfig.getParentConfig() : payConfig;
        // 封装参数
        JSONObject map = JSONUtil.createObj()
                .set("msgId", refund.getTransactionNumber())
                // 时间戳
                .set("requestTimestamp", DateUtil.formatDateTime(new Date()))
                // 商户号
                .set("mid", config.getMchId())
                // 终端号
                .set("tid", config.getTerminalId())
                // 业务类型
                .set("instMid", "H5DEFAULT")
                // 订单号
                .set("merOrderId", refund.getOrderNo())
                // 退款订单号
                .set("refundOrderId", refund.getRefundOrderNo())
                // 退款金额. 若下单接口中上送了分账标记字段divisionFlag，则该字段refundAmount=subOrders中totalAmount之和+platformAmount
                .set("refundAmount", NumberUtil.mul(refund.getRefundAmount(), new BigDecimal(100)))
                // 退款说明
                .set("refundDesc", StringUtils.isNotBlank(refund.getRefundDesc()) ? refund.getRefundDesc() : "退款");
        // 判断是否需要设置子订单信息
        if (Objects.nonNull(payConfig.getParentConfig())) {
            // 平台商户退款金额
            map.set("platformAmount", 0);
            // 子订单
            map.set("subOrders", JSONUtil.createArray().set(
                    JSONUtil.createObj()
                            .set("mid", payConfig.getMchId())
                            .set("totalAmount", NumberUtil.mul(refund.getRefundAmount(), new BigDecimal(100)))
            ));
        }

        HttpResponse httpResponse = HttpRequest.post(UnionPayConstant.REFUND_ORDER)
                .header("Authorization", setAccessTokenHeader(accessToken))
                .body(map.toString())
                .execute();
        // 返回数据
        log.info("订单退款接口返回数据: {}", httpResponse.body());
        JSONObject jsonObject = JSONUtil.parseObj(httpResponse.body());
        if (jsonObject.containsKey(UnionPayConstant.CODE_NAME)
                && !UnionPayConstant.SUCCESS_CODE.equals(jsonObject.getStr(UnionPayConstant.CODE_NAME))) {
            log.error("订单退款失败, param: {}, code: {}, msg: {}", map, jsonObject.getStr(UnionPayConstant.CODE_NAME),
                    jsonObject.getStr(UnionPayConstant.MSG_NAME));
            throw new FebsException(ResultMsgType.ORDER_REFUND_FAIL.getValue());
        }
        return jsonObject;
    }

    /**
     * 退款查询
     * 请求方式: HTTP(S) + JSON
     *
     * @param payConfig   银联云闪付配置信息
     * @param orderNo     退货订单号
     * @param accessToken token值
     * @return cn.hutool.json.JSONObject
     * @author ZYJ
     * @date 2022/6/27 19:49
     */
    public static JSONObject refundQuery(UnionPayConfig payConfig, String orderNo, String accessToken) {
        // 设置主商户配置信息
        UnionPayConfig config = Objects.nonNull(payConfig.getParentConfig()) ? payConfig.getParentConfig() : payConfig;
        // 封装参数
        JSONObject map = JSONUtil.createObj()
                // 时间戳
                .set("requestTimestamp", DateUtil.formatDateTime(new Date()))
                // 商户号
                .set("mid", config.getMchId())
                // 终端号
                .set("tid", config.getTerminalId())
                // 业务类型
                .set("instMid", "H5DEFAULT")
                // 订单号
                .set("merOrderId", orderNo);

        HttpResponse httpResponse = HttpRequest.post(UnionPayConstant.REFUND_QUERY)
                .header("Authorization", setAccessTokenHeader(accessToken))
                .body(map.toString())
                .execute();
        // 返回数据
        log.info("退款查询接口返回数据: {}", httpResponse.body());
        JSONObject jsonObject = JSONUtil.parseObj(httpResponse.body());
        if (jsonObject.containsKey(UnionPayConstant.CODE_NAME)
                && !UnionPayConstant.SUCCESS_CODE.equals(jsonObject.getStr(UnionPayConstant.CODE_NAME))) {
            log.error("退款查询失败, param: {}, code: {}, msg: {}", map, jsonObject.getStr(UnionPayConstant.CODE_NAME),
                    jsonObject.getStr(UnionPayConstant.MSG_NAME));
            throw new FebsException(ResultMsgType.REFUND_QUERY_FAIL.getValue());
        }
        return jsonObject;
    }

    /**
     * 生成订单号
     * 下单订单号和退款订单号生成规则一致
     * 规则: {来源编号(4位)}{时间(yyyyMMddmmHHssSSS)(17位)}{7位随机数}
     *
     * @param sourceNumber 来源编号
     * @return java.lang.String
     * @author ZYJ
     * @date 2022/6/16 14:32
     */
    public static String getOrderNumber(String sourceNumber) {
        return sourceNumber + DateUtil.format(new Date(), DatePattern.PURE_DATETIME_MS_PATTERN) + RandomUtil.randomNumbers(7);
    }

    /**
     * 封装请求头
     *
     * @param accessToken token值
     * @return java.lang.String
     * @author ZYJ
     * @date 2022/6/16 16:56
     */
    private static String setAccessTokenHeader(String accessToken) {
        return "OPEN-ACCESS-TOKEN AccessToken=\"" + accessToken + "\"";
    }

    /**
     * 生成签名
     * 格式: Base64_Encode(HmacSHA256(appId+ timestamp + nonce + SHA256_HEX(content), AppKey))
     *
     * @param appId     开发者ID
     * @param appKey    开发者key
     * @param timestamp 时间戳. 格式: yyyyMMddHHmmss
     * @param nonce     随机数. 最大128位
     * @param content   业务内容. 请求字符串
     * @return java.lang.String
     * @author ZYJ
     * @date 2022/6/15 16:37
     */
    private static String getSignature(String appId, String appKey, String timestamp,
                                       String nonce, String content) {
        HMac hmac = DigestUtil.hmac(HmacAlgorithm.HmacSHA256, appKey.getBytes());
        byte[] digest = hmac.digest(appId + timestamp + nonce + DigestUtil.sha256Hex(content));
        return Base64Encoder.encode(digest);
    }

    /**
     * 生成支付地址
     *
     * @param requestUrl 请求地址
     * @param appId      开发者ID
     * @param appKey     开发者key
     * @param timestamp  时间戳. 格式: yyyyMMddHHmmss
     * @param nonce      随机数. 最大128位
     * @param content    业务内容. 请求字符串
     * @return java.lang.String
     * @author ZYJ
     * @date 2022/6/16 11:02
     */
    private static String getPayUrl(String requestUrl, String appId, String appKey, String timestamp, String nonce, String content) {
        try {
            // 生成签名
            String signature = getSignature(appId, appKey, timestamp, nonce, content);
            // 返回支付地址
            return (requestUrl + "?authorization=OPEN-FORM-PARAM" + "&appId=" + appId + "&timestamp=" + timestamp +
                    "&nonce=" + nonce + "&content=" + URLEncoder.encode(content, CharsetUtil.UTF_8)
                    + "&signature=" + URLEncoder.encode(signature, CharsetUtil.UTF_8));
        } catch (Exception e) {
            throw new FebsException(ResultMsgType.ORDER_FAIL.getValue());
        }
    }

    /**
     * 银联云闪付验签
     * 签名支持MD5方式与SHA256方式（不上送signType字段时，默认md5方式）
     * {@link com.yida.data.common.core.enums.UnionSignTypeEnum}
     *
     * @param payConfig 银联云闪付配置信息
     * @param map       参数
     * @return boolean
     * @author ZYJ
     * @date 2022/6/27 14:37
     */
    public static boolean checkSign(UnionPayConfig payConfig, Map<String, Object> map) {
        // 设置主商户配置信息
        UnionPayConfig config = Objects.nonNull(payConfig.getParentConfig()) ? payConfig.getParentConfig() : payConfig;

        // 排除参数中的签名信息
        String sign = (String) map.get("sign");
        map.remove("sign");
        // 设置待签名串
        String params = MapUtil.sortJoin(map, StringConstant.AMPERSAND,
                StringConstant.EQUALS, false, config.getPrivateKey());
        log.info("待签名串: params: {}", params);
        // 签名
        String checkSign;
        // 判断签名类型
        String signType = (String) map.get("signType");
        if (UnionSignTypeEnum.SHA256_TYPE.getType().equals(signType)) {
            // SHA256验签
            checkSign = DigestUtil.sha256Hex(params.getBytes());
        } else {
            // MD5验签
            checkSign = DigestUtil.md5Hex(params.getBytes());
        }
        log.info("原签名, sign: {}", sign);
        log.info("生成的对比签名值, checkSign: {}", checkSign);
        return sign.equalsIgnoreCase(checkSign);
    }

}
