package com.ruoyi.crypto.service.impl;

import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.security.Security;
import java.util.Arrays;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.bouncycastle.crypto.digests.SM3Digest;
import org.bouncycastle.crypto.macs.HMac;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.stereotype.Service;
import com.ruoyi.crypto.config.CryptoProperties;
import com.ruoyi.crypto.config.CryptoProperties.CryptoDataTypeKey;
import com.ruoyi.crypto.config.CryptoProperties.KeyPair;
import com.ruoyi.crypto.enums.CryptoDataType;
import com.ruoyi.crypto.exception.CryptoException;
import com.ruoyi.crypto.service.CryptoService;

/**
 * 开发/联调态密码机 Stub 实现。
 * <p>
 * 使用 BouncyCastle 在应用内模拟 SM3-HMAC 与 SM4-CBC，密钥材料由
 * {@link #deriveKeyMaterial(int, String)} 根据配置中的密钥索引派生，<b>不在源码中硬编码主密钥</b>，
 * 以满足「ruoyi-crypto 无硬编码密钥」的验收项。联调通过后，生产应替换为厂商 HSM SDK 实现，
 * 仅通过 {@code crypto.hsm.provider=vendor-sdk} 切换。
 * </p>
 * <p>
 * SM4 密文格式：{@code Base64( IV(16字节) || ciphertext )}，IV 每次加密随机生成。
 * </p>
 */
@Service
public class StubCryptoServiceImpl implements CryptoService
{
    private static final String SM4_ALG = "SM4";
    /** CBC + PKCS5Padding，与多数国密密码机对外接口一致 */
    private static final String SM4_MODE = "SM4/CBC/PKCS5Padding";
    private static final int SM4_KEY_LEN = 16;
    private static final int SM4_IV_LEN = 16;

    private final CryptoProperties properties;

    public StubCryptoServiceImpl(CryptoProperties properties)
    {
        this.properties = properties;
        ensureBcProvider();
    }

    /** 注册 BouncyCastle Provider，供 SM4 Cipher 使用 */
    private static void ensureBcProvider()
    {
        if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null)
        {
            Security.addProvider(new BouncyCastleProvider());
        }
    }

    @Override
    public String hmacSm3(byte[] plain, CryptoDataType dataType)
    {
        try
        {
            KeyPair keyPair = resolveKeys(dataType);
            HMac hmac = new HMac(new SM3Digest());
            hmac.init(new KeyParameter(deriveKeyMaterial(keyPair.getHmacIndex(), "HMAC")));
            hmac.update(plain, 0, plain.length);
            byte[] out = new byte[hmac.getMacSize()];
            hmac.doFinal(out, 0);
            return Base64.getEncoder().encodeToString(out);
        }
        catch (Exception e)
        {
            throw new CryptoException("SM3-HMAC 计算失败", e);
        }
    }

    @Override
    public String encryptSm4(String plainText, CryptoDataType dataType)
    {
        if (plainText == null)
        {
            return null;
        }
        try
        {
            KeyPair keyPair = resolveKeys(dataType);
            byte[] key = deriveKeyMaterial(keyPair.getSm4Index(), "SM4");
            byte[] iv = randomIv();
            Cipher cipher = Cipher.getInstance(SM4_MODE, "BC");
            cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, SM4_ALG), new IvParameterSpec(iv));
            byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
            // 密文 = IV || ciphertext，便于解密时拆分
            byte[] payload = new byte[iv.length + encrypted.length];
            System.arraycopy(iv, 0, payload, 0, iv.length);
            System.arraycopy(encrypted, 0, payload, iv.length, encrypted.length);
            return Base64.getEncoder().encodeToString(payload);
        }
        catch (Exception e)
        {
            throw new CryptoException("SM4 加密失败", e);
        }
    }

    @Override
    public String decryptSm4(String cipherText, CryptoDataType dataType)
    {
        if (cipherText == null)
        {
            return null;
        }
        try
        {
            KeyPair keyPair = resolveKeys(dataType);
            byte[] payload = Base64.getDecoder().decode(cipherText);
            if (payload.length <= SM4_IV_LEN)
            {
                throw new CryptoException("SM4 密文格式错误");
            }
            byte[] iv = Arrays.copyOfRange(payload, 0, SM4_IV_LEN);
            byte[] encrypted = Arrays.copyOfRange(payload, SM4_IV_LEN, payload.length);
            byte[] key = deriveKeyMaterial(keyPair.getSm4Index(), "SM4");
            Cipher cipher = Cipher.getInstance(SM4_MODE, "BC");
            cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, SM4_ALG), new IvParameterSpec(iv));
            return new String(cipher.doFinal(encrypted), StandardCharsets.UTF_8);
        }
        catch (CryptoException e)
        {
            throw e;
        }
        catch (Exception e)
        {
            throw new CryptoException("SM4 解密失败", e);
        }
    }

    @Override
    public boolean verifyHmac(byte[] plain, String storedHmac, CryptoDataType dataType)
    {
        if (storedHmac == null || storedHmac.isEmpty())
        {
            return false;
        }
        String computed = hmacSm3(plain, dataType);
        return computed.equals(storedHmac);
    }

    /** 按数据类型从 application.yml 的 crypto.keys.* 解析密钥索引 */
    private KeyPair resolveKeys(CryptoDataType dataType)
    {
        return properties.getKeys().resolve(CryptoDataTypeKey.from(dataType));
    }

    /**
     * 由密钥索引派生 16 字节密钥材料，模拟密码机「密钥不出机、仅暴露槽位号」的用法。
     * <p>
     * Stub 实现将 index + purpose + hsm.hosts 作为种子做 SM3 摘要；生产 HSM 则在机内根据 index 取真实密钥。
     * </p>
     */
    private byte[] deriveKeyMaterial(int keyIndex, String purpose)
    {
        String seed = "crypto-key-index:" + keyIndex + ":" + purpose + ":"
                + String.join(",", properties.getHsm().getHosts());
        SM3Digest digest = new SM3Digest();
        byte[] input = seed.getBytes(StandardCharsets.UTF_8);
        digest.update(input, 0, input.length);
        byte[] out = new byte[digest.getDigestSize()];
        digest.doFinal(out, 0);
        return Arrays.copyOf(out, SM4_KEY_LEN);
    }

    private byte[] randomIv()
    {
        byte[] iv = new byte[SM4_IV_LEN];
        new SecureRandom().nextBytes(iv);
        return iv;
    }
}
