package com.ruoyi.app.utils.ezPayCrypto;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.HexFormat;
import java.util.Map;
import java.util.TreeMap;

/**
 * ezPay 电子发票加值服务平台 加密 / 签名工具。
 *
 * <p>对应官方手册「附件一 PostData_ 加密方法」「附件二 CheckCode 产生规则」的 PHP / C# 示例。
 *
 * <p><b>关键点</b>：ezPay 要求 PKCS7 填充块为 <b>32 字节</b>（非 AES 标准的 16），再用
 * AES-256-CBC(NoPadding) 加密。因为 32 是 16 的倍数，填充后的数据对标准 AES-256-CBC 合法，
 * 无需 BouncyCastle，标准 javax.crypto 即可。
 *
 * <ul>
 *   <li>HashKey 固定 32 字节（→ AES-256），HashIV 固定 16 字节。</li>
 *   <li>加密结果输出小写 hex，即请求里的 PostData_。</li>
 * </ul>
 *
 * <p>自测：运行 {@link #main} 会用官方手册里的示例数据验证 CheckValue / CheckCode /
 * 加解密 round-trip，全部 PASS 才算实现正确。
 *
 * @author ruoyi
 */
public class EzPayEncryptUtil {

    /** ezPay 特殊的 PKCS7 填充块大小（字节），官方手册固定为 32。 */
    private static final int BLOCK_SIZE = 32;

    private static final String TRANSFORMATION = "AES/CBC/NoPadding";

    private EzPayEncryptUtil() {
    }

    /**
     * AES-256-CBC 加密 → 小写 hex（用于 PostData_）。
     *
     * @param data    明文，参数已按 http_build_query 形式拼成 {@code a=1&b=2}
     * @param hashKey 商店 HashKey（固定 32 字节）
     * @param hashIV  商店 HashIV（固定 16 字节）
     * @return 小写 hex 密文，放入请求的 PostData_
     */
    public static String encrypt(String data, String hashKey, String hashIV) {
        try {
            byte[] padded = pkcs7Pad(data.getBytes(StandardCharsets.UTF_8), BLOCK_SIZE);
            Cipher cipher = Cipher.getInstance(TRANSFORMATION);
            cipher.init(Cipher.ENCRYPT_MODE,
                    new SecretKeySpec(hashKey.getBytes(StandardCharsets.UTF_8), "AES"),
                    new IvParameterSpec(hashIV.getBytes(StandardCharsets.UTF_8)));
            byte[] encrypted = cipher.doFinal(padded);
            return HexFormat.of().formatHex(encrypted);
        } catch (Exception e) {
            throw new RuntimeException("ezPay PostData_ 加密失败", e);
        }
    }

    /**
     * AES-256-CBC 解密（用于解密回应里的 Result 字段）。
     *
     * @param hexData 小写 hex 密文
     * @return 解密后的明文（通常是 a=1&b=2 形式或 JSON 字符串）
     */
    public static String decrypt(String hexData, String hashKey, String hashIV) {
        try {
            byte[] bytes = HexFormat.of().parseHex(hexData);
            Cipher cipher = Cipher.getInstance(TRANSFORMATION);
            cipher.init(Cipher.DECRYPT_MODE,
                    new SecretKeySpec(hashKey.getBytes(StandardCharsets.UTF_8), "AES"),
                    new IvParameterSpec(hashIV.getBytes(StandardCharsets.UTF_8)));
            byte[] decrypted = cipher.doFinal(bytes);
            return new String(pkcs7Unpad(decrypted), StandardCharsets.UTF_8);
        } catch (Exception e) {
            throw new RuntimeException("ezPay Result 解密失败", e);
        }
    }

    /**
     * 生成请求 CheckValue：SHA256("HashKey={key}&{postDataHex}&HashIV={iv}") 转大写。
     *
     * <p>仅 BDV 验证类接口（checkBarCode / checkLoveCode）请求时需要带 CheckValue；
     * 发票类接口（issue / invalid / search ...）请求不带 CheckValue，只发 MerchantID_ + PostData_。
     *
     * <p><b>文档勘误提醒</b>：BDV 手册附件三把一个「回应 CheckCode」的示例值误标在 CheckValue 章节，
     * 故无法用文档示例验证本方法。本方法严格按附件三<b>文字描述</b>实现（HashKey 在前、HashIV 在后）。
     * 实际接入 checkBarCode 若平台返回 CheckValue 校验失败，可改为 HashIV 在前重试
     * （即与 {@link #genCheckCodeRaw} 同序，二者只差首尾 Key/IV 顺序）。
     */
    public static String genCheckValue(String postDataHex, String hashKey, String hashIV) {
        String raw = "HashKey=" + hashKey + "&" + postDataHex + "&HashIV=" + hashIV;
        return sha256Upper(raw);
    }

    /**
     * 生成回应 CheckCode：将指定字段按 A~Z 排序，
     * SHA256("HashIV={iv}&{k1=v1&k2=v2...}&HashKey={key}") 转大写。
     *
     * <p>用于校验回应确实来自 ezPay 平台。各接口参与校验的字段不同：
     * <ul>
     *   <li>发票开立 / 触发 / 查询：InvoiceTransNo, MerchantID, MerchantOrderNo, RandomNum, TotalAmt</li>
     *   <li>作废发票：MerchantID, InvoiceNumber, CreateTime（参考手册各章节「附件二」）</li>
     *   <li>折让：MerchantID, AllowanceNo, MerchantOrderNo, AllowanceAmt, RemainAmt</li>
     * </ul>
     * 调用方按接口把对应字段塞进 {@code fields} 即可，键名须与官方一致。
     *
     * @param fields 回应中参与校验的字段
     */
    public static String genCheckCode(Map<String, String> fields, String hashKey, String hashIV) {
        TreeMap<String, String> sorted = new TreeMap<>(fields);
        StringBuilder sb = new StringBuilder("HashIV=").append(hashIV).append('&');
        for (Map.Entry<String, String> e : sorted.entrySet()) {
            sb.append(e.getKey()).append('=').append(e.getValue()).append('&');
        }
        sb.append("HashKey=").append(hashKey);
        return sha256Upper(sb.toString());
    }

    /**
     * 生成回应 CheckCode（BDV 验证接口风格）：直接用一段原始串（如回应里的 Result 密文 hex）
     * 拼接，SHA256("HashIV={iv}&{raw}&HashKey={key}") 转大写。
     *
     * <p>BDV 接口（checkBarCode / checkLoveCode）的回应 CheckCode 以解密前的 Result 密文 hex
     * 作为唯一参与串；与发票接口（INVI）的多字段排序 CheckCode 不同，故单独提供。
     */
    public static String genCheckCodeRaw(String raw, String hashKey, String hashIV) {
        return sha256Upper("HashIV=" + hashIV + "&" + raw + "&HashKey=" + hashKey);
    }

    private static String sha256Upper(String raw) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            byte[] digest = md.digest(raw.getBytes(StandardCharsets.UTF_8));
            return HexFormat.of().formatHex(digest).toUpperCase();
        } catch (Exception e) {
            throw new RuntimeException("SHA256 计算失败", e);
        }
    }

    /** PKCS7 填充到 blockSize 的整数倍。 */
    private static byte[] pkcs7Pad(byte[] data, int blockSize) {
        int pad = blockSize - (data.length % blockSize);
        byte[] result = new byte[data.length + pad];
        System.arraycopy(data, 0, result, 0, data.length);
        for (int i = data.length; i < result.length; i++) {
            result[i] = (byte) pad;
        }
        return result;
    }

    /** 去除 PKCS7 填充。 */
    private static byte[] pkcs7Unpad(byte[] data) {
        int pad = data[data.length - 1] & 0xFF;
        return Arrays.copyOf(data, data.length - pad);
    }

    // ===================== 自测：用官方手册示例数据验证 =====================

    public static void main(String[] args) {
        int pass = 0, fail = 0;

        // 1) CheckCode：INVI 手册附件二示例
        //    字段 InvoiceTransNo/MerchantID/MerchantOrderNo/RandomNum/TotalAmt
        //    HashKey=abcdefghijklmnopqrstuvwxyzabcdef, HashIV=1234567891234567
        //    期望 303AB800650B724733B5D91CBCE075D9EA09E4CDE9CD33461D45F07D5EC7EECB
        Map<String, String> checkCodeFields = new java.util.HashMap<>();
        checkCodeFields.put("InvoiceTransNo", "14061313541640927");
        checkCodeFields.put("MerchantID", "3622183");
        checkCodeFields.put("MerchantOrderNo", "201409170000001");
        checkCodeFields.put("RandomNum", "0142");
        checkCodeFields.put("TotalAmt", "500");
        String cc = genCheckCode(checkCodeFields, "abcdefghijklmnopqrstuvwxyzabcdef", "1234567891234567");
        if ("303AB800650B724733B5D91CBCE075D9EA09E4CDE9CD33461D45F07D5EC7EECB".equals(cc)) {
            System.out.println("[PASS] CheckCode"); pass++;
        } else {
            System.out.println("[FAIL] CheckCode  got=" + cc); fail++;
        }

        // 2) BDV 回应 CheckCode（Result 密文单字段）：手册 PAGE 10 示例
        //    HashIV=SdsatSdXm1vH7N3T & Result hex & HashKey=rsBjnIvMG3VMMWsEynTK3PWiGccHuYiV
        //    期望 052E06C3720642DE306C1C33F921713EA4BA6DB9970C78F6C98952D4E69E85F7
        //    注：BDV 附件三把这个 CheckCode 值误标为 CheckValue，实际是回应 CheckCode（HashIV 在前）。
        String resultHex = "0628256c389824ce257c6e8796707aa3bc803d2245f4894261dbae64ff9a51f34f519b79c3fd8892157a8703779ab7639c62c3d078e7c8a09006a27f4f8aefbb";
        String cc2 = genCheckCodeRaw(resultHex, "rsBjnIvMG3VMMWsEynTK3PWiGccHuYiV", "SdsatSdXm1vH7N3T");
        if ("052E06C3720642DE306C1C33F921713EA4BA6DB9970C78F6C98952D4E69E85F7".equals(cc2)) {
            System.out.println("[PASS] CheckCode(BDV/Result)"); pass++;
        } else {
            System.out.println("[FAIL] CheckCode(BDV) got=" + cc2); fail++;
        }

        // 3) 加解密 round-trip：用 32 字节 key + 16 字节 iv
        String key = "abcdefghijklmnopqrstuvwxyzabcdef";
        String iv = "1234567891234567";
        String plain = "RespondType=JSON&Version=1.0&TimeStamp=1444963784";
        String enc = encrypt(plain, key, iv);
        String dec = decrypt(enc, key, iv);
        if (plain.equals(dec)) {
            System.out.println("[PASS] encrypt/decrypt round-trip"); pass++;
        } else {
            System.out.println("[FAIL] round-trip  dec=" + dec); fail++;
        }

        // 4) 边界：明文长度正好是 32 倍数时，应补一整块（32）
        String plain2 = "0123456789ABCDEF0123456789ABCDEF"; // 32 字节
        String dec2 = decrypt(encrypt(plain2, key, iv), key, iv);
        if (plain2.equals(dec2)) {
            System.out.println("[PASS] PKCS7 满块补整块"); pass++;
        } else {
            System.out.println("[FAIL] 满块补整块 dec=" + dec2); fail++;
        }

        // 5) BDV 附件一完整测试向量（PAGE 18-19）：加密/CheckValue/Result解密 三项全比对官方值
        {
            String bk = "rsBjnIvMG3VMMWsEynTK3PWiGccHuYiV";
            String bv = "SdsatSdXm1vH7N3T";
            String bPlain = "TimeStamp=1589261733&CellphoneBarcode=%2FAAA.CCC";
            String expPost = "ed2d2219af6cc92b9504be630658c70bd5eea80358a675cfa2e2f110002b92af244814d24a937c339e7ce938b4a32653daf85ba9a871b8003b0a5aed4db5e9bf";
            String gotPost = encrypt(bPlain, bk, bv);
            if (expPost.equals(gotPost)) { System.out.println("[PASS] BDV PostData_ 加密"); pass++; } else { System.out.println("[FAIL] BDV PostData_ 加密 got=" + gotPost); fail++; }
            String expCV = "5D7A25D069D176C187A5161B77064DB1E49C4A7BEBC56D2A1CEBF743D019820B";
            String gotCV = genCheckValue(expPost, bk, bv);
            if (expCV.equals(gotCV)) { System.out.println("[PASS] BDV CheckValue(请求)"); pass++; } else { System.out.println("[FAIL] BDV CheckValue(请求) got=" + gotCV); fail++; }
            String bResult = "0628256c389824ce257c6e8796707aa3bc803d2245f4894261dbae64ff9a51f34f519b79c3fd8892157a8703779ab7639c62c3d078e7c8a09006a27f4f8aefbb";
            String expDec = "CellphoneBarcode=%2FAAA.CCC&IsExist=N";
            String gotDec = decrypt(bResult, bk, bv);
            if (expDec.equals(gotDec)) { System.out.println("[PASS] BDV Result 解密 -> " + gotDec); pass++; } else { System.out.println("[FAIL] BDV Result 解密 got=" + gotDec); fail++; }
        }

        System.out.println("\n结果: " + pass + " passed, " + fail + " failed");
    }
}
