package com.eitc.system.utils;

import lombok.Getter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.encoders.Hex;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.security.Security;
import java.util.Base64;

import static java.util.Objects.isNull;

@Slf4j
public class SM4Utils {

    private static final int DEFAULT_KEY_SIZE = 128; // 秘钥
    private static final String ALGORITHM = "SM4";
    private static final String SM4_ECB_ = "SM4/ECB/";
    private static final String SM4_CBC_ = "SM4/CBC/";
    private static final Base64.Encoder BASE64_ENCODER = Base64.getEncoder();
    private static final Base64.Decoder BASE64_DECODER = Base64.getDecoder();
    private static final BouncyCastleProvider PROVIDER = new BouncyCastleProvider();

    static {
        if (isNull(Security.getProvider(BouncyCastleProvider.PROVIDER_NAME))) {
            Security.addProvider(PROVIDER);
        }
    }

    @Getter
    public enum Padding {
        PKCS5("PKCS5Padding"),
        PKCS7("PKCS7Padding"),
        ISO10126("ISO10126Padding");

        private final String name;

        Padding(String name) {
            this.name = name;
        }
    }

    // region generateKey

    public static byte[] genKey() {
        return genKey(DEFAULT_KEY_SIZE);
    }

    @SneakyThrows
    public static byte[] genKey(int keySize) {
        KeyGenerator kg = KeyGenerator.getInstance(ALGORITHM, BouncyCastleProvider.PROVIDER_NAME);
        kg.init(keySize, new SecureRandom());
        return kg.generateKey().getEncoded();
    }

    public static String genKeyAsHex() {
        return genKeyAsHex(DEFAULT_KEY_SIZE);
    }

    public static String genKeyAsHex(int keySize) {
        return Hex.toHexString(genKey(keySize));
    }

    public static String genKeyAsBase64() {
        return genKeyAsBase64(DEFAULT_KEY_SIZE);
    }

    public static String genKeyAsBase64(int keySize) {
        return BASE64_ENCODER.encodeToString(genKey(keySize));
    }

    // endregion generateKey

    // region ECB mode

    @SneakyThrows
    public static Cipher getCipher_ECB(Padding padding) {
        return Cipher.getInstance(SM4_ECB_ + padding.name, BouncyCastleProvider.PROVIDER_NAME);
    }

    /**
     * 使用指定的加密算法和密钥对给定的字节数组进行加密
     *
     * @param data 要加密的字节数组
     * @param key  加密所需的密钥
     * @return byte[]   加密后的字节数组
     */
    @SneakyThrows
    public static byte[] encrypt_ECB(byte[] data, byte[] key, Padding padding) {
        Cipher cipher = getCipher_ECB(padding);
        SecretKeySpec secretKeySpec = new SecretKeySpec(key, ALGORITHM);
        cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
        return cipher.doFinal(data);
    }

    /**
     * 使用指定的加密算法和密钥对给定的字节数组进行解密
     *
     * @param data 要解密的字节数组
     * @param key  解密所需的密钥
     * @return byte[]   解密后的字节数组
     */
    @SneakyThrows
    public static byte[] decrypt_ECB(byte[] data, byte[] key, Padding padding) {
        Cipher cipher = getCipher_ECB(padding);
        SecretKeySpec secretKeySpec = new SecretKeySpec(key, ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
        return cipher.doFinal(data);
    }

    public static String encryptHex_ECB(String data, String key, Padding padding) {
        return Hex.toHexString(encrypt_ECB(
                data.getBytes(StandardCharsets.UTF_8),
                Hex.decode(key),
                padding
        ));
    }

    public static String decryptHex_ECB(String data, String key, Padding padding) {
        return new String(decrypt_ECB(
                Hex.decode(data),
                Hex.decode(key),
                padding
        ), StandardCharsets.UTF_8);
    }

    public static String encryptBase64_ECB(String data, String key, Padding padding) {
        return BASE64_ENCODER.encodeToString(encrypt_ECB(
                data.getBytes(StandardCharsets.UTF_8),
                BASE64_DECODER.decode(key),
                padding
        ));
    }

    public static String decryptBase64_ECB(String data, String key, Padding padding) {
        return new String(decrypt_ECB(
                BASE64_DECODER.decode(data),
                BASE64_DECODER.decode(key),
                padding
        ), StandardCharsets.UTF_8);
    }

    // endregion ECB mode

    // region CBC mode

    @SneakyThrows
    public static Cipher getCipher_CBC(Padding padding) {
        return Cipher.getInstance(SM4_CBC_ + padding.name, BouncyCastleProvider.PROVIDER_NAME);
    }

    /**
     * 使用指定的加密算法和密钥对给定的字节数组进行加密
     *
     * @param data 要加密的字节数组
     * @param key  加密所需的密钥
     * @param iv   解密所需的 IV
     * @return byte[]   加密后的字节数组
     */
    @SneakyThrows
    public static byte[] encrypt_CBC(byte[] data, byte[] key, byte[] iv, Padding padding) {
        Cipher cipher = getCipher_CBC(padding);
        SecretKeySpec secretKeySpec = new SecretKeySpec(key, ALGORITHM);
        IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
        cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
        return cipher.doFinal(data);
    }

    /**
     * 使用指定的加密算法和密钥对给定的字节数组进行解密
     *
     * @param data 要解密的字节数组
     * @param key  解密所需的密钥
     * @param iv   解密所需的 IV
     * @return byte[]   解密后的字节数组
     */
    @SneakyThrows
    public static byte[] decrypt_CBC(byte[] data, byte[] key, byte[] iv, Padding padding) {
        Cipher cipher = getCipher_CBC(padding);
        SecretKeySpec secretKeySpec = new SecretKeySpec(key, ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, new IvParameterSpec(iv));
        return cipher.doFinal(data);
    }

    public static String encryptHex_CBC(String data, String key, String iv, Padding padding) {
        return Hex.toHexString(encrypt_CBC(
                data.getBytes(StandardCharsets.UTF_8),
                Hex.decode(key),
                Hex.decode(iv),
                padding
        ));
    }

    public static String decryptHex_CBC(String data, String key, String iv, Padding padding) {
        try {
            return new String(decrypt_CBC(
                    Hex.decode(data),
                    Hex.decode(key),
                    Hex.decode(iv),
                    padding
            ), StandardCharsets.UTF_8);
        } catch (Exception e) {
            throw new RuntimeException("校验失败，请重试");
        }
    }

    public static String encryptBase64_CBC(String data, String key, String iv, Padding padding) {
        return BASE64_ENCODER.encodeToString(encrypt_CBC(
                data.getBytes(StandardCharsets.UTF_8),
                BASE64_DECODER.decode(key),
                BASE64_DECODER.decode(iv),
                padding
        ));
    }

    public static String decryptBase64_CBC(String data, String key, String iv, Padding padding) {
        try {
            return new String(decrypt_CBC(
                    BASE64_DECODER.decode(data),
                    BASE64_DECODER.decode(key),
                    BASE64_DECODER.decode(iv),
                    padding
            ), StandardCharsets.UTF_8);
        } catch (Exception e) {
            throw new RuntimeException("参数解析错误，核对后重试");
        }
    }

    // endregion CBC mode

    public static void main(String[] args) {
        final Padding padding = Padding.PKCS5;
        final String plainText = "{\"phone\":\"12345678901\",\"serverCode\":\"ED39BA70-393D-4B28-ADE6-68184C0D6BAC\",\"clinicIp\":\"192.168.3.17\"}";
        final byte[] iv = "guonenghuizhi666".getBytes(StandardCharsets.UTF_8);

        final String key0 = genKeyAsBase64();
        log.debug("\n密钥 : {}", key0);

        String encrypt0 = encryptBase64_ECB(plainText, key0, padding);
        String decrypt0 = decryptBase64_ECB(encrypt0, key0, padding);
        log.debug("\n加密 : {}\n解密 : {}", encrypt0, decrypt0);

        String encrypt1 = encryptBase64_CBC(plainText, key0, BASE64_ENCODER.encodeToString(iv), padding);
        String decrypt1 = decryptBase64_CBC(encrypt1, key0, BASE64_ENCODER.encodeToString(iv), padding);
        log.debug("\n加密 : {}\n解密 : {}", encrypt1, decrypt1);

        final String key1 = genKeyAsHex();
        log.debug("\n密钥 : {}", key1);

        String encrypt2 = encryptHex_ECB(plainText, key1, padding);
        String decrypt2 = decryptHex_ECB(encrypt2, key1, padding);
        log.debug("\n加密 : {}\n解密 : {}", encrypt2, decrypt2);

        String encrypt3 = encryptHex_CBC(plainText, key1, Hex.toHexString(iv), padding);
        String decrypt3 = decryptHex_CBC(encrypt3, key1, Hex.toHexString(iv), padding);
        log.debug("\n加密 : {}\n解密 : {}", encrypt3, decrypt3);
    }

}
