package org.jeecg.module.custom.ocr.utils;

import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.jeecg.module.custom.ocr.common.entity.RequestData;
import org.jeecg.module.custom.ocr.common.entity.ResultData;
import org.springframework.lang.Nullable;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.*;

/**
 * openapi工具类，包括签名、加密算法等。<br>
 * 签名：
 * <ol>
 *     <li>字符串编码: {@link StandardCharsets#UTF_8}</li>
 *     <li>签名算法：HmacSHA256</li>
 *     <li>密钥和盐合并格式： &lt;密钥&gt;#&lt;/密钥&gt;</li>
 *     <li>密钥长度：128位</li>
 *     <li>密钥生成算法：SHA1PRNG</li>
 *     <li>签名参数按ASCII码升序排序，组成签名字符串：key1=value1&key2=&key3=value3…</li>
 *     <li>签名后均采用base64编码：{@link Base64#encodeBase64URLSafeString(byte[])}</li>
 * </ol>
 *
 * 加密：
 * <ol>
 *     <li>字符串编码: {@link StandardCharsets#UTF_8}</li>
 *     <li>加密算法：AES；具体算法：AES/ECB/PKCS5Padding</li>
 *     <li>密钥和盐合并格式： &lt;密钥&gt;#&lt;/密钥&gt;</li>
 *     <li>密钥长度：128位</li>
 *     <li>密钥生成算法：SHA1PRNG</li>
 *     <li>加密后均采用base64编码：{@link Base64#encodeBase64URLSafeString(byte[])}</li>
 * </ol>
 *
 * @author hdzhong
 * @date 2023/9/10
 */
@Slf4j
public class ApiHelper {

    /**
     * AES key algorithm: AES
     */
    private static final String KEY_ALGORITHM = "AES";
    /**
     * AES key length: 128
     */
    private static final int KEY_LENGTH = 128;
    /**
     * cipher algorithm: AES/ECB/PKCS5Padding
     */
    private static final String CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";

    /**
     * combine key and salt by: String.format("%s#%s", secretKey, salt)
     *
     * @param accessCode encrypt/decrypt key
     * @param salt salt append to key
     * @return combined key
     */
    public static String combineSalt(String accessCode, String salt) {
        return String.format("%s#%s", accessCode, salt);
    }

    /**
     * verify whether the signature is correct
     *
     * @param accessCode encrypt/decrypt key
     * @param salt salt append to key
     * @param parameters parameter which to be signed
     * @param signature signature which will be verified
     * @return whether the signature is correct
     * @see #combineSalt(String, String)
     */
    public static boolean verifySignature(String accessCode, String salt,
                                      Map<String, String> parameters, String signature) {
        String sign = getSignature(combineSalt(accessCode, salt), parameters);

        return sign.equals(signature);
    }

    /**
     * verify whether the signature is correct
     *
     * @param accessCode encrypt/decrypt key
     * @param salt salt append to key
     * @param data encrypted data which to be signed
     * @param signature signature which will be verified
     * @return whether the signature is correct
     * @see #combineSalt(String, String)
     */
    public static boolean verifySignature(String accessCode, String salt,
                                          String data, String signature) {
        String sign = getSignature(accessCode, salt, data);

        return sign.equals(signature);
    }

    /**
     * timestamp({@link RequestData#getTimestamp()}) & signature({@link RequestData#getSignature()})
     * will be added to {@code parameters}. {@code data} will be encrypted.
     *
     * @param tenantCode tenant code
     * @param accessKey access key
     * @param accessCode access secret
     * @param parameters parameter which to be signed
     * @return encrypted data and signature
     * @throws Exception
     */
    public static RequestData buildQueryRequest(String tenantCode, String accessKey, String accessCode,
                                                Map<String, String> parameters) throws Exception {
        RequestData requestData = new RequestData();

        // tenant code
        requestData.setTenantCode(tenantCode);
        // access key
        requestData.setAccessKey(accessKey);
        // parameter
        requestData.setParameters(parameters);

        // 1. do sign
        long timestamp = System.currentTimeMillis();
        // timestamp
        requestData.setTimestamp(timestamp);

        String sign = getSignature(accessCode, String.valueOf(timestamp), parameters);
        requestData.setSignature(sign);

        return requestData;
    }

    /**
     * timestamp({@link RequestData#getTimestamp()}) & signature({@link RequestData#getSignature()})
     * will be added to {@code parameters}. {@code data} will be encrypted.
     *
     * @param tenantCode tenant code
     * @param accessKey access key
     * @param accessCode access secret
     * @param accessCode encrypt/decrypt key
     * @param data request data which to be encrypted
     * @return encrypted data and signature
     * @throws Exception
     */
    public static <T> RequestData buildRequest(String tenantCode, String accessKey, String accessCode,
                                           T data) throws Exception {
        RequestData requestData = new RequestData();

        // tenant code
        requestData.setTenantCode(tenantCode);
        // access key
        requestData.setAccessKey(accessKey);

        // timestamp
        long timestamp = System.currentTimeMillis();
        requestData.setTimestamp(timestamp);

        // 1. convert data to json string
        String dataJson = null;
        if (data == null) {
            dataJson = "";
        } else if (data instanceof String) {
            dataJson = (String) data;
        } else {
            ObjectMapper mapper = new ObjectMapper();
            dataJson = mapper.writeValueAsString(data);
        }

        // 2. do encrypt
        String encryptedData = encrypt(accessCode, String.valueOf(timestamp), dataJson);
        requestData.setData(encryptedData);

        // 2. do sign
        // build query string for tenantCode & accessKey & encrypted data
        final StringBuilder builder = new StringBuilder();
        Map<String, String> map = new LinkedHashMap<>();
        // append tenant code
        map.put("tenantCode", requestData.getTenantCode());
        // append access key
        map.put("accessKey", requestData.getAccessKey());

        String query = RequestData.buildQuery(map);

        // combine data
        String text =  String.format("%s%s", query, requestData.getData());

        String sign = getSignature(accessCode, String.valueOf(timestamp), text);
        requestData.setSignature(sign);

        return requestData;
    }

    /**
     * verify signature then decrypt {@link RequestData#getData()}
     *
     * @param accessCode access code
     * @param requestData request data which {@link RequestData#getData()} is encrypted
     * @return decrypted String
     * @throws Exception
     * @see #decryptRequest(String, RequestData, Class)
     */
    public @Nullable static String decryptRequest(String accessCode, RequestData requestData) throws Exception {
        return decryptRequest(accessCode, requestData, String.class);
    }

    /**
     * verify signature then decrypt {@link RequestData#getData()}
     *
     * @param accessCode access code
     * @param requestData request data which {@link RequestData#getData()} is encrypted
     * @param clazz class use for {@link ObjectMapper#readValue(String, Class)} to convert JSON to object
     * @return decrypted Object
     * @throws Exception
     */
    public @Nullable static <T> T decryptRequest(String accessCode, RequestData requestData,
                                                  Class<T> clazz) throws Exception {
        String decrypedData = null;
        // 1. verify signature
        String timestamp = String.valueOf(requestData.getTimestamp());

        // build query string for tenantCode & accessKey & encrypted data
        final StringBuilder builder = new StringBuilder();
        Map<String, String> map = new LinkedHashMap<>();
        // append tenant code
        map.put("tenantCode", requestData.getTenantCode());
        // append access key
        map.put("accessKey", requestData.getAccessKey());

        String query = RequestData.buildQuery(map);

        // combine data
        String text =  String.format("%s%s", query, requestData.getData());

        if (!verifySignature(accessCode, timestamp, text, requestData.getSignature())) {
            throw new Exception(String.format("illegal signature: %s", requestData.getSignature()));
        }

        // 2. do decrypt
        byte[] bytes = decrypt(accessCode, timestamp, requestData.getData());
        if (bytes != null) {
            decrypedData = new String(bytes, StandardCharsets.UTF_8);
        }

        // 3. convert to object
        if (clazz.equals(String.class)) {
            return (T) decrypedData;
        }
        ObjectMapper mapper = new ObjectMapper();

        return mapper.readValue(decrypedData, clazz);
    }

    /**
     * do sign with salt
     * <ol>
     *     <li>combine key and salt by {@link #combineSalt(String, String)}</li>
     *     <li>invoke {@link #getSignature(String, Map)}</li>
     * </ol>
     *
     * @param accessCode encrypt/decrypt key
     * @param salt salt append to key
     * @param parameters parameter which to be signed
     * @return {@link #getSignature(String, Map)}
     */
    public static String getSignature(String accessCode, String salt, Map<String, String> parameters) {
        return getSignature(combineSalt(accessCode, salt), parameters);
    }

    /**
     * do sign without salt
     * <ol>
     *     <li>build message by {@link #buildMessage(Map)}</li>
     *     <li>do sign by "HmacSHA256" algorithm: {@link Mac#getInstance(String)}</li>
     *     <li>encode signature by base64 with URL safe character: {@link Base64#encodeBase64URLSafeString(byte[])}</li>
     * </ol>
     *
     * @param secretKey encrypt/decrypt key
     * @param parameters parameter which to be signed
     * @return signature
     */
    public static String getSignature(String secretKey, Map<String, String> parameters) {
        if (parameters.isEmpty()) {
            throw new RuntimeException("parameters are empty");
        }
        // 1. sort parameters to string
        String message = buildMessage(parameters);
        if (log.isInfoEnabled()) {
            log.debug("signature for message: {}", message);
        }

        // 2. do sign
        return getSignature(secretKey, message);
    }

    /**
     * do sign with salt
     * <ol>
     *     <li>combine key and salt by {@link #combineSalt(String, String)}</li>
     *     <li>invoke {@link #getSignature(String, String)}</li>
     * </ol>
     *
     * @param accessCode encrypt/decrypt key
     * @param salt salt append to key
     * @param data encrypted data which to be signed
     * @return signature
     * @see #getSignature(String, String)
     */
    public static String getSignature(String accessCode, String salt, String data) {
        return getSignature(combineSalt(accessCode, salt), data);
    }

    /**
     * sign {@code data}
     *
     * @param secretKey encrypt/decrypt key
     * @param data encrypted data which to be signed
     * @return signature
     * @see #getSignature(String, String, Map)
     * @see #buildMessage(Map)
     */
    public static String getSignature(String secretKey, String data) {
        if (data == null ) {
            return null;
        }

        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(), "HmacSHA256");
            mac.init(keySpec);
            byte[] result = mac.doFinal(data.getBytes());

            // 3. to base64 string
            return Base64.encodeBase64URLSafeString(result);
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        }
    }

    /**
     * sort parameters then combine to query string
     * <ol>
     *     <li>sort parameters' keys ASCII ASC by {@link Collections#sort(List)}</li>
     *     <li>combine to query string: key1=value1&key2=value2</li>
     * </ol>
     *
     * @param parameters parameter which to be signed
     * @return query string for parameters
     */
    public static String buildMessage(Map<String,String> parameters) {
        Collection<String> keys = parameters.keySet();
        List<String> keyList = new ArrayList<>(keys);
        // sort parameters by ASCII ASC
        Collections.sort(keyList);
        StringBuilder builder = new StringBuilder();
        for (String k : keyList) {
            String v = parameters.get(k);
            if (v == null) {
                v = "";
            }
            if (builder.length() > 0) {
                builder.append("&");
            }
            builder.append(k).append("=").append(v);
        }
        return builder.toString();
    }

    /**
     * encrypt data then do sign
     *
     * @param accessCode access code
     * @param data data which to be encrypted
     * @return ResultData with encrypted data & signature
     * @throws Exception
     */
    public static <T> ResultData<String> buildResponse(String accessCode, T data) throws Exception {
        ResultData<String> result = ResultData.success();

        // 1. convert data to json string
        String dataJson = null;
        if (data == null) {
            dataJson = "";
        } else if (data instanceof String) {
            dataJson = (String) data;
        } else {
            ObjectMapper mapper = new ObjectMapper();
            dataJson = mapper.writeValueAsString(data);
        }

        // 2. do encrypt
        String timestamp = String.valueOf(result.getTimestamp());
        String encryptedData = encrypt(accessCode, timestamp, dataJson);
        result.setData(encryptedData);

        // 3. do signature
        String signature = getSignature(accessCode, timestamp, encryptedData);
        result.setSignature(signature);

        // message
        return result;
    }

    /**
     * do verify signature then decrypt
     *
     * @param accessCode access code
     * @param resultData {@link ResultData} which to be decrypted
     * @return decrypted string
     * @throws Exception
     * @see #decryptResponse(String, ResultData, Class)
     */
    public @Nullable static String decryptResponse(String accessCode, ResultData<String> resultData) throws Exception {
        return decryptResponse(accessCode, resultData, String.class);
    }

    /**
     * do verify signature then decrypt
     *
     * @param accessCode access code
     * @param resultData {@link ResultData} which to be decrypted
     * @param clazz class use for {@link ObjectMapper#readValue(String, Class)} to convert JSON to object
     * @return decrypted string
     * @throws Exception
     */
    public @Nullable static <T> T decryptResponse(String accessCode, ResultData<String> resultData,
                                                   Class<T> clazz) throws Exception {
        String decrypedData = null;
        // 1. verify signature
        String timestamp = String.valueOf(resultData.getTimestamp());
        if (!verifySignature(accessCode, timestamp, resultData.getData(), resultData.getSignature())) {
            throw new Exception(String.format("illegal signature: %s", resultData.getSignature()));
        }

        // 2. do decrypt
        byte[] bytes = decrypt(accessCode, timestamp, resultData.getData());
        if (bytes != null) {
            decrypedData = new String(bytes, StandardCharsets.UTF_8);
        }

        // 3. convert to object
        if (clazz.equals(String.class)) {
            return (T) decrypedData;
        }
        ObjectMapper mapper = new ObjectMapper();

        return mapper.readValue(decrypedData, clazz);
    }

    /**
     * do verify signature then decrypt
     *
     * @param accessCode access code
     * @param resultData {@link ResultData} which to be decrypted
     * @param clazz class use for {@link ObjectMapper#readValue(String, Class)} to convert JSON to object
     * @return decrypted string
     * @throws Exception
     */
    public @Nullable static <T> T decryptResponse(String accessCode, Long time, String data, String signature,
                                                  Class<T> clazz) throws Exception {
        String decrypedData = null;
        // 1. verify signature
        String timestamp = String.valueOf(time);
        if (!verifySignature(accessCode, timestamp, data, signature)) {
            throw new Exception(String.format("illegal signature: %s", signature));
        }

        // 2. do decrypt
        byte[] bytes = decrypt(accessCode, timestamp, data);
        if (bytes != null) {
            decrypedData = new String(bytes, StandardCharsets.UTF_8);
        }

        // 3. convert to object
        if (clazz.equals(String.class)) {
            return (T) decrypedData;
        }
        ObjectMapper mapper = new ObjectMapper();

        return mapper.readValue(decrypedData, clazz);
    }

    public @Nullable static <T> List<T> decryptListResponse(String accessCode, Long time, String data, String signature,
                                                        Class<T> clazz) throws Exception {
        String decrypedData = null;
        // 1. 验证签名
        String timestamp = String.valueOf(time);
        if (!verifySignature(accessCode, timestamp, data, signature)) {
            throw new Exception(String.format("非法签名: %s", signature));
        }

        // 2. 解密
        byte[] bytes = decrypt(accessCode, timestamp, data);
        if (bytes != null) {
            decrypedData = new String(bytes, StandardCharsets.UTF_8);
        }

        // 3. 转换为对象
        if (clazz.equals(String.class)) {
            return Collections.singletonList((T) decrypedData);
        }

        ObjectMapper mapper = new ObjectMapper();
        // 反序列化为指定类型的列表
        JavaType listType = mapper.getTypeFactory().constructCollectionType(List.class, clazz);
        return mapper.readValue(decrypedData, listType);
    }


    /**
     * do AES encrypt
     *
     * <ol>
     *     <li>combine key and salt by {@link #combineSalt(String, String)}</li>
     *     <li>invoke {@link #encrypt(String, String)}</li>
     * </ol>
     *
     * @param accessCode encrypt/decrypt key
     * @param salt salt append to key
     * @param data data which will be encrypted
     * @return base64 string of AES encrypted data
     * @throws Exception
     * @see #combineSalt(String, String)
     * @see #encrypt(String, String)
     */
    public @Nullable static String encrypt(String accessCode, String salt, String data) throws Exception {
        return encrypt(combineSalt(accessCode, salt), data);
    }

    /**
     * encrypt data without salt
     *
     * @param secretKey encrypt/decrypt key
     * @param data data which will be encrypted
     * @return base64 string of AES encrypted data
     * @throws Exception
     * @see #encrypt(String, byte[])
     */
    public @Nullable static String encrypt(String secretKey, String data) throws Exception {
        if (data == null) {
            return null;
        }

        return encrypt(secretKey, data.getBytes(StandardCharsets.UTF_8));
    }

    /**
     * do AES encrypt
     *
     * <ol>
     *     <li>combine key and salt by {@link #combineSalt(String, String)}</li>
     *     <li>invoke {@link #encrypt(String, byte[])}</li>
     * </ol>
     *
     * @param accessCode encrypt/decrypt key
     * @param salt salt append to key
     * @param data data which will be encrypted
     * @return base64 string of AES encrypted data
     * @throws Exception
     * @see #combineSalt(String, String)
     * @see #encrypt(String, byte[])
     */
    public @Nullable static String encrypt(String accessCode, String salt, byte[] data) throws Exception {
        return encrypt(combineSalt(accessCode, salt), data);
    }

    /**
     * <ol>
     *     <li>generate secret key by {@link #getSecretKey(String)}</li>
     *     <li>do AES encrypt</li>
     *     <li>convert ot base64 string {@link Base64#encodeBase64URLSafeString(byte[])}</li>
     * </ol>
     *
     * @param secretKey encrypt/decrypt key
     * @param data data which will be encrypted
     * @return base64 string of AES encrypted data
     * @throws Exception
     */
    public @Nullable static String encrypt(String secretKey, byte[] data) throws Exception {
        if (data == null) {
            return null;
        }
        if (log.isInfoEnabled()) {
            log.debug("encrypt for data: {}", new String(data, StandardCharsets.UTF_8));
        }
        // 创建密码器
        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);

        // 初始化为加密模式的密码器
        cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(secretKey));

        byte[] result = cipher.doFinal(data);

        // base64 encode
        String base64 = Base64.encodeBase64URLSafeString(result);

        if (log.isInfoEnabled()) {
            log.debug("encrypted: {}", base64);
        }
        return base64;
    }


    /**
     * do AES decrypt
     *
     * <ol>
     *     <li>combine key and salt by {@link #combineSalt(String, String)}</li>
     *     <li>invoke {@link #decrypt(String, String)}</li>
     * </ol>
     *
     * @param accessCode encrypt/decrypt key
     * @param salt salt append to key
     * @param data data which will be decrypted
     * @return decrypted data
     * @throws Exception
     * @see #combineSalt(String, String)
     * @see #decrypt(String, String)
     */
    public @Nullable static byte[] decrypt(String accessCode, String salt, String data) throws Exception {
        return decrypt(combineSalt(accessCode, salt), data);
    }

    /**
     * <ol>
     *     <li>generate secret key by {@link #getSecretKey(String)}</li>
     *     <li>decode base64 string of {@code data} by: {@link Base64#decodeBase64(byte[])}</li>
     *     <li>do decrypt</li>
     * </ol>
     *
     * @param secretKey encrypt/decrypt key
     * @param data data which will be decrypted
     * @return decrypted data
     * @throws Exception
     * @see #encrypt(String, byte[])
     */
    public @Nullable static byte[] decrypt(String secretKey, String data) throws Exception {
        if (data == null) {
            return null;
        }

        if (log.isInfoEnabled()) {
            log.debug("decrypt for data: {}", data);
        }

        // 创建密码器
        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);

        // 初始化为解密模式的密码器
        cipher.init(Cipher.DECRYPT_MODE, getSecretKey(secretKey));

        // base64 decode
        byte[] bytes = Base64.decodeBase64(data.getBytes(StandardCharsets.UTF_8));

        byte[] result = cipher.doFinal(bytes);

        return result;
    }

    /**
     * generate key spec.
     * <ol>
     *     <li>create {@link SecureRandom} of "SHA1PRNG": {@link SecureRandom#getInstance(String)}</li>
     *     <li>create {@link #KEY_LENGTH} bits key generator of {@link #KEY_ALGORITHM}: {@link KeyGenerator#getInstance(String)}</li>
     *     <li>generate key spec of {@link #CIPHER_ALGORITHM}: {@link SecretKeySpec}</li>
     * </ol>
     *
     * @return {@link SecretKeySpec}
     */
    private static SecretKeySpec getSecretKey(final String secretKey) throws NoSuchAlgorithmException {
        // 返回生成指定算法密钥生成器的 KeyGenerator 对象
        KeyGenerator kg = null;

        kg = KeyGenerator.getInstance(KEY_ALGORITHM);

        // AES 要求密钥长度为 128
        SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
        secureRandom.setSeed(secretKey.getBytes());
        kg.init(KEY_LENGTH, secureRandom);

        // 生成一个密钥
        SecretKey key = kg.generateKey();

        // 转换为AES专用密钥
        return new SecretKeySpec(key.getEncoded(), KEY_ALGORITHM);
    }

    public static void main(String arg[]){

        BigDecimal b1 = new BigDecimal("42.29");
        BigDecimal b2 = new BigDecimal("100");
        int val = b1.compareTo(b2);
        //租户编码(租户no：tenantNo)
        String tenantCode = "123";
        //访问key
        String accessKey = "key";
        //访问密钥
        String accessCode = "";
        //请求参数
        Map<String, String> queryParam = new HashMap<>();
        queryParam.put("key1", "111");
        try {
            RequestData requestData = ApiHelper.buildQueryRequest("tenantCode", "accessKey", "accessCode", queryParam);

            System.out.println(requestData.getData());

        } catch (Exception e) {
            throw new RuntimeException(e);
        }


        RequestData requestData = null;
        try {
            requestData = ApiHelper.buildRequest("tenantCode", "accessKey", "accessCode", queryParam);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        System.out.println(requestData.getData());


        ResultData<String> resultData = new ResultData<>(); // 实际返回对象

        String data = null;
        try {
            data = ApiHelper.decryptResponse("accessCode", resultData);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        System.out.println(data);
    }
}
