package org.dromara.settlement.common.bank.minsheng;

import cmbc.cfca.sm2.signature.SM2PublicKey;
import cmbc.cfca.sm2rsa.common.Mechanism;
import cmbc.cfca.util.CertUtil;
import cmbc.cfca.util.EnvelopeUtil;
import cmbc.cfca.util.KeyUtil;
import cmbc.cfca.util.SignatureUtil2;
import cmbc.cfca.util.cipher.lib.JCrypto;
import cmbc.cfca.x509.certificate.X509Cert;
import cmbc.cfca.x509.certificate.X509CertHelper;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.security.PrivateKey;
import java.security.PublicKey;

/* *
 * 加解密工具类
 * 使用民生定制cfca安全处理包，解决商户方多个jar包版本的冲突问题
 * @author adt-huangyh
 *
 */
public class EcpSecurityUtil {
    private static final Logger logger = LoggerFactory.getLogger(EcpSecurityUtil.class);
    private static final String charset = "utf-8";
    private static final String signAlg = cmbc.cfca.sm2rsa.common.Mechanism.SM3_SM2;
    private static cmbc.cfca.util.cipher.lib.Session session = null;

    static {
        try {
            JCrypto.getInstance().initialize(JCrypto.JSOFT_LIB, null);
            session = JCrypto.getInstance().openSession(JCrypto.JSOFT_LIB);
        } catch (cmbc.cfca.sm2rsa.common.PKIException e) {
            logger.error("获取session失败:" + e.getMessage());
        }
    }

    /**
     * 加密加签
     *
     * @param plainText  明文
     * @param priKeyPath 私钥路径
     * @param priKeyPwd  私钥密码
     * @param certPath   公钥路径
     * @return
     * @throws Exception
     */
    public static EcpSecurityResult encryptAndSign(String plainText, String priKeyPath, String priKeyPwd,
                                                   String certPath) throws Exception {
        EcpSecurityResult sr = new EcpSecurityResult();
        String cipherText = encrypt(plainText, certPath);
        sr.setCipherText(cipherText);
        String signature = sign(plainText, priKeyPath, priKeyPwd);
        sr.setSignature(signature);
        return sr;
    }

    /**
     * 使用公钥进行Sm2加密
     *
     * @param plainText 明文
     * @param certPath  公钥路径
     * @return
     * @throws Exception
     * @function
     */
    public static String encrypt(String plainText, String certPath) throws Exception {
        X509Cert cert = null;
        logger.debug("明文:" + plainText);
        try {
            cert = X509CertHelper.parse(certPath);
        } catch (Throwable t) {
            String errMsg = "公钥文件加载失败。sm2Path=" + certPath;
            logger.error(errMsg);
            throw new Exception(errMsg, t);
        }
        try {
            X509Cert[] certs = {cert};
            byte[] base64Bytes = EnvelopeUtil.envelopeMessage(plainText.getBytes(charset), Mechanism.SM4_CBC, certs, session);
            String cipherText = new String(base64Bytes, charset);
            logger.debug("密文:" + cipherText);
            return cipherText;
        } catch (Throwable t) {
            String msg = "加密失败";
            logger.error(msg);
            throw new Exception(msg, t);
        }

    }

    /**
     * 签名
     *
     * @param plainText  待签名的明文
     * @param priKeyPath 己方私钥文件路径
     * @param priKeyPwd  己方私钥文件密码
     * @return
     * @throws Exception
     * @function
     */
    public static String sign(String plainText, String priKeyPath, String priKeyPwd) throws Exception {
        String signature = null;
        logger.debug("签名开始。。");
        String sm2FileData = readFileToString(priKeyPath, charset);
        try {
            PrivateKey priKey = KeyUtil.getPrivateKeyFromSM2(sm2FileData.getBytes(charset), priKeyPwd);
            SignatureUtil2 signatureUtil = new SignatureUtil2();
            signatureUtil.setSm2WithZFlag(true);
            // 签名
            byte[] base64P1SignedData = signatureUtil.p1SignMessage(signAlg,
                    plainText.getBytes(charset), priKey, session);
            signature = new String(base64P1SignedData, charset);
            logger.debug("签名内容：" + signature);
        } catch (Exception e) {
            logger.error("签名失败");
            throw new Exception("签名失败", e);
        }
        return signature;
    }

    /**
     * 解密
     *
     * @param cipherText 密文
     * @param signature  签名
     * @param priKeyPath 私钥路径
     * @param priKeyPwd  私钥密码
     * @param pubKeyPath 公钥路径
     * @return
     * @throws Exception
     */
    public static EcpSecurityResult decryptAndVerify(String cipherText, String signature, String priKeyPath, String priKeyPwd, String pubKeyPath) throws Exception {
        EcpSecurityResult sr = new EcpSecurityResult();
        sr.setCipherText(cipherText);
        sr.setSignature(signature);
        PrivateKey priKey;
        X509Cert cert;
        try {
            priKey = getPrivateKeyFromSM2Path(priKeyPath, priKeyPwd);
            cert = getX509CertFromSm2Path(priKeyPath);
            byte[] base64Bytes = cipherText.getBytes(charset);
            byte[] decryptBytes = EnvelopeUtil.openEvelopedMessage(base64Bytes, priKey, cert, session);
            String decryptText = new String(decryptBytes, charset);
            sr.setPlainText(decryptText);
        } catch (Throwable t) {
            String msg = "解密失败";
            logger.error(msg);
            throw new Exception(msg, t);
        }
        //验签
        try {
            PublicKey publicKey = getPublicKeyFromCertPath(pubKeyPath);
            boolean verifyResult = verifySignature(sr.getPlainText(),
                    signature, publicKey);
            sr.setVerifyPass(verifyResult);
        } catch (Exception e) {
            String msg = "验签失败";
            logger.error(msg);
            throw new Exception(msg, e);
        }
        return sr;
    }

    private static boolean verifySignature(String plainText, String signature, PublicKey publicKey) throws Exception {
        if (plainText == null || signature == null) {
            return true;
        }
        logger.debug("签名明文:" + plainText);
        logger.debug("签名:" + signature);
        //验签
        SignatureUtil2 signatureUtil = new SignatureUtil2();
        signatureUtil.setSm2WithZFlag(true);
        SM2PublicKey pubKey = (SM2PublicKey) publicKey;

        boolean verifyResult = signatureUtil.p1VerifyMessage(signAlg,
                plainText.getBytes(charset), signature.getBytes(charset), pubKey,
                session);
        logger.debug("验签结果:" + String.valueOf(verifyResult));
        return verifyResult;
    }


    /**
     * 从文件路径读取sm2文件。首先从缓存中查找是否有该文件路径的文件，若有则直接取出。若无则从文件系统中读取，并写入缓存。
     *
     * @param sm2FilePath
     * @param sm2FilePwd
     * @return
     * @throws Exception
     * @function
     */
    private static PrivateKey getPrivateKeyFromSM2Path(String sm2FilePath, String sm2FilePwd) throws Exception {

        String sm2FileData = readFileToString(sm2FilePath, charset);
        PrivateKey priKey;
        try {
            priKey = KeyUtil.getPrivateKeyFromSM2(sm2FileData.getBytes(charset), sm2FilePwd);
        } catch (Throwable t) {
            String errMsg = "私钥文件加载失败。sm2FilePath=" + sm2FilePath;
            logger.error(errMsg);
            throw new Exception(errMsg, t);
        }
        return priKey;

    }

    private static X509Cert getX509CertFromSm2Path(String sm2Path) throws Exception {
        X509Cert cert;
        try {
            cert = CertUtil.getCertFromSM2(sm2Path);
        } catch (Throwable t) {
            String errMsg = "公钥文件加载失败。sm2Path=" + sm2Path;
            logger.error(errMsg);
            throw new Exception(errMsg, t);
        }


        return cert;

    }

    /**
     * 从文件路径读取公钥。首先从缓存中查找是否有该文件路径的文件，若有则直接取出。若无则从文件系统中读取，并写入缓存。
     *
     * @param certPath
     * @return
     * @throws Exception
     * @function
     */
    private static PublicKey getPublicKeyFromCertPath(String certPath) throws Exception {

        PublicKey publicKey;
        String certData = readFileToString(certPath, charset);
        try {
            X509Cert cert = X509CertHelper.parse(certData.getBytes());
            publicKey = cert.getPublicKey();

        } catch (Throwable t) {
            String errMsg = "公钥文件加载失败。certPath=" + certPath;
            logger.error(errMsg);
            throw new Exception(errMsg, t);
        }
        return publicKey;

    }

    private static String readFileToString(String fileName, String encoding) {
        File file = new File(fileName);
        if (!file.exists()) {
            throw new RuntimeException(fileName + "文件不存在");
        }
        try {
            return FileUtils.readFileToString(file, encoding);
        } catch (IOException e) {
            throw new RuntimeException(fileName + "读取文件失败", e);
        }
    }

}
