package com.eactive.eai.common.hsm;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.util.concurrent.ConcurrentHashMap;

import javax.annotation.PostConstruct;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.eactive.eai.common.property.PropManager;
import com.eactive.eai.common.util.Logger;

/**
 * SafeNet ProtectServer HSM 암복호화 서비스 (JDK 8)
 *
 * [키 조회]
 *   getPublicKey  - 공개키 반환 (외부 노출). 최초 1회만 HSM 통신, 이후 캐시 반환.
 *   getSecretKey  - AES 대칭키 반환 (CKA_EXTRACTABLE=true 필요). 최초 1회만 HSM 통신, 이후 캐시 반환.
 *
 * [암복호화]
 *   encryptRsa  - 공개키로 JVM 소프트웨어 암호화 (HSM 불필요)
 *   decryptRsa  - alias 기반, HSM 내부 복호화 (매 호출마다 HSM 통신 발생 - 불가피)
 *   encryptAes  - SecretKey 객체로 AES/CBC 암호화
 *   decryptAes  - SecretKey 객체로 AES/CBC 복호화
 */
@Service
public class HsmCryptoService implements PropertyChangeListener {

    static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);

    private static final String RSA_ALGORITHM = "RSA/ECB/PKCS1Padding";
    private static final String AES_ALGORITHM = "AES/CBC/PKCS5Padding";

    private static final String PROP_GROUP = "HSM";
    private static final String PROP_CACHE_RELOAD_YN = "CACHE_RELOAD_YN";

    private static final String PROP_CACHE_TTL_SEC = "CACHE_TTL_SEC";
    private static long CACHE_TTL_MS = 10 * 60 * 1000; // 10분, 필요시 PropManager로 외부화

    private static class CachedKey<T> {
        final T key;
        final long cachedAt;

        CachedKey(T key) {
            this.key = key;
            this.cachedAt = System.currentTimeMillis();
        }

        boolean isExpired() {
            return System.currentTimeMillis() - cachedAt > CACHE_TTL_MS;
        }
    }
    
    // HSM 통신 최소화: 최초 1회 조회 후 JVM 메모리에 캐싱
    private final ConcurrentHashMap<String, CachedKey<SecretKey>> secretKeyCache = new ConcurrentHashMap<>();
    private final ConcurrentHashMap<String, CachedKey<PublicKey>> publicKeyCache = new ConcurrentHashMap<>();

    @Autowired
    private PropManager propManager;

    @PostConstruct
    public void registerPropertyChangeListener() {
        propManager.addPropertyChangeListener(this);
        logger.warn("HsmCryptoService] PropManager PropertyChangeListener 등록 완료");
    }

    /**
     * 관리포털에서 PropManager.reload("HSM") 호출 시 이벤트를 수신한다.
     * HSM.CACHE_RELOAD_YN = Y 이면 키 캐시를 초기화한다.
     */
    @Override
    public void propertyChange(PropertyChangeEvent evt) {
        if (!PROP_GROUP.equals(evt.getPropertyName())) {
            return;
        }
        String reloadYn = propManager.getProperty(PROP_GROUP, PROP_CACHE_RELOAD_YN, "N");
        if ("Y".equalsIgnoreCase(reloadYn)) {
            clearKeyCache();
        }
        
        String propCacheTtlSec = propManager.getProperty(PROP_GROUP, PROP_CACHE_TTL_SEC, "600");
        CACHE_TTL_MS = (Integer.parseInt(propCacheTtlSec.trim())) * 1000; // 10분, 필요시 PropManager로 외부화
    }

    // -------------------------------------------------------------------------
    // 키 조회 (외부 노출)
    // -------------------------------------------------------------------------

    /**
     * HSM KeyStore 에서 공개키를 반환합니다. 최초 1회만 HSM 통신하고 이후 캐시를 반환합니다.
     * HSM 장애 시 만료된 캐시가 있으면 그것을 반환하여 서비스 연속성을 유지합니다.
     */
    public PublicKey getPublicKey(String keyAlias) throws HsmException {
        // 1. 유효한 캐시 즉시 반환 (HSM 상태 무관)
        CachedKey<PublicKey> cached = publicKeyCache.get(keyAlias);
        if (cached != null && !cached.isExpired()) {
            return cached.key;
        }

        // 2. 캐시 미스 또는 만료 → HSM 갱신 시도
        if (HsmManager.getInstance().isReady()) {
            try {
                Certificate cert = HsmManager.getInstance().getKeyStore().getCertificate(keyAlias);
                if (cert == null) {
                    throw new HsmException("인증서를 찾을 수 없습니다. alias=" + keyAlias);
                }
                PublicKey key = cert.getPublicKey();
                publicKeyCache.put(keyAlias, new CachedKey<>(key));
                logger.warn("HsmCryptoService] 공개키 캐시 등록: alias=" + keyAlias);
                return key;
            } catch (HsmException e) {
                throw e;
            } catch (Exception e) {
                logger.warn("HsmCryptoService] 공개키 HSM 조회 실패: " + e.getMessage());
            }
        }

        // 3. HSM 조회 불가 → 만료 캐시 fallback
        if (cached != null) {
            logger.warn("HsmCryptoService] HSM 장애, 만료 공개키 캐시 fallback: alias=" + keyAlias);
            return cached.key;
        }

        throw new HsmException("공개키 조회 불가: HSM 장애이며 캐시도 없습니다. alias=" + keyAlias);
    }

    /**
     * HSM KeyStore 에서 AES 대칭키를 반환합니다. 최초 1회만 HSM 통신하고 이후 캐시를 반환합니다.
     * HSM 키 생성 시 CKA_EXTRACTABLE=true (ctkmu -x 플래그) 로 생성된 키만 반환됩니다.
     *
     * [캐싱 전략]
     * HSM 에서 가져온 P11SecretKey(PKCS11 핸들 래퍼)를 그대로 캐싱하면, HSM Provider 가
     * 재초기화될 때 세션 무효화로 인해 캐시된 키를 사용한 Cipher 연산이 실패한다.
     * 따라서 getEncoded() 로 키 바이트를 추출하여 SecretKeySpec(JVM 메모리 키) 으로 변환 후
     * 캐싱한다. encryptAes/decryptAes 는 이미 JVM 소프트웨어 Cipher 를 사용하므로,
     * HSM Provider 상태와 완전히 독립적으로 동작한다.
     *
     * HSM 장애 시 만료된 캐시가 있으면 그것을 반환하여 서비스 연속성을 유지합니다.
     */
    public SecretKey getSecretKey(String keyAlias) throws HsmException {
        // 1. 유효한 캐시 즉시 반환 (HSM 상태 무관)
        CachedKey<SecretKey> cached = secretKeyCache.get(keyAlias);
        if (cached != null && !cached.isExpired()) {
            return cached.key;
        }

        // 2. 캐시 미스 또는 만료 → HSM 갱신 시도
        if (HsmManager.getInstance().isReady()) {
            try {
                KeyStore keyStore = HsmManager.getInstance().getKeyStore();
                java.security.Key key = keyStore.getKey(keyAlias, null);
                if (key == null) {
                    throw new HsmException("키를 찾을 수 없습니다. alias=" + keyAlias);
                }
                SecretKey secretKey = (SecretKey) key;

                // P11SecretKey → SecretKeySpec 변환: HSM Provider 의존성 제거
                // getEncoded() 가 null 이면 non-extractable 키이므로 원본 유지
                byte[] keyBytes = secretKey.getEncoded();
                if (keyBytes != null) {
                    secretKey = new SecretKeySpec(keyBytes, secretKey.getAlgorithm());
                }

                secretKeyCache.put(keyAlias, new CachedKey<>(secretKey));
                logger.warn("HsmCryptoService] 대칭키 캐시 등록(갱신): alias=" + keyAlias);
                return secretKey;

            } catch (HsmException e) {
                throw e;
            } catch (Exception e) {
                logger.warn("HsmCryptoService] 대칭키 HSM 조회 실패: " + e.getMessage());
            }
        }

        // 3. HSM 조회 불가 → 만료 캐시 fallback
        if (cached != null) {
            logger.warn("HsmCryptoService] HSM 장애, 만료 대칭키 캐시 fallback: alias=" + keyAlias);
            return cached.key;
        }

        throw new HsmException("키 조회 불가: HSM 장애이며 캐시도 없습니다. alias=" + keyAlias);
    }

    /** 캐시를 비웁니다. HSM 키 교체 후 재로드가 필요할 때 호출합니다. */
    public void clearKeyCache() {
        secretKeyCache.clear();
        publicKeyCache.clear();
        logger.warn("HsmCryptoService] 키 캐시 초기화 완료");
    }

    // -------------------------------------------------------------------------
    // RSA
    // -------------------------------------------------------------------------

    /**
     * RSA 암호화 - HSM 인증서의 공개키로 JVM 소프트웨어 암호화.
     * 공개키는 HSM 에서 추출하므로 HsmManager 가 초기화되어 있어야 합니다.
     */
    public byte[] encryptRsa(String keyAlias, byte[] plaintext) throws HsmException {
        return encryptRsa(getPublicKey(keyAlias), plaintext);
    }

    /**
     * RSA 암호화 - 전달받은 공개키로 JVM 소프트웨어 암호화.
     * HSM 없이도 호출 가능합니다.
     */
    public byte[] encryptRsa(PublicKey publicKey, byte[] plaintext) throws HsmException {
        try {
            // Provider 미명시 → JVM 소프트웨어 Provider(SunRsaSign) 자동 선택
            Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, publicKey);
            return cipher.doFinal(plaintext);
        } catch (Exception e) {
            throw new HsmException("RSA 암호화 실패: " + e.getMessage(), e);
        }
    }

    /**
     * RSA 복호화 - 개인키 핸들을 HSM 에서 가져와 HSM 내부에서 복호화.
     * HsmManager 가 초기화되지 않은 경우 JVM 소프트웨어로 fallback 합니다.
     *
     * @param keyAlias HSM KeyStore 의 개인키 별칭
     * @param pin      HSM 슬롯 PIN (null 이면 HsmManager 초기화 시 사용한 PIN 재사용)
     * @param ciphertext 암호문 바이트
     */
    public byte[] decryptRsa(String keyAlias, char[] pin, byte[] ciphertext) throws HsmException {
        try {
            HsmManager hsmManager = HsmManager.getInstance();

            if (hsmManager.isReady()) {
                // HSM 내부 복호화: pkcs11Provider 명시 필수
                Provider pkcs11Provider = hsmManager.getPkcs11Provider();
                java.security.Key key = hsmManager.getKeyStore().getKey(keyAlias, pin);
                if (!(key instanceof PrivateKey)) {
                    throw new HsmException("개인키를 찾을 수 없습니다. alias=" + keyAlias);
                }
                Cipher cipher = Cipher.getInstance(RSA_ALGORITHM, pkcs11Provider);
                cipher.init(Cipher.DECRYPT_MODE, (PrivateKey) key);
                return cipher.doFinal(ciphertext);

            } else {
                throw new HsmException("RSA 복호화 불가: HsmManager 가 초기화되지 않았습니다. alias=" + keyAlias);
            }

        } catch (HsmException e) {
            throw e;
        } catch (Exception e) {
            throw new HsmException("RSA 복호화 실패: " + e.getMessage(), e);
        }
    }

    // -------------------------------------------------------------------------
    // AES
    // -------------------------------------------------------------------------

    /**
     * AES/CBC/PKCS5Padding 암호화.
     * extractable 키: provider=null 로 JVM 소프트웨어 암호화.
     * non-extractable 키: provider 에 pkcs11Provider 를 전달해야 합니다.
     *
     * @param secretKey 대칭키 (HSM 추출 키 또는 소프트웨어 키)
     * @param iv        초기화 벡터 (16바이트)
     * @param plaintext 평문 바이트
     * @param provider  null 이면 JVM 기본 Provider 사용
     */
    public byte[] encryptAes(SecretKey secretKey, byte[] iv, byte[] plaintext, Provider provider) throws HsmException {
        try {
            Cipher cipher = (provider != null)
                    ? Cipher.getInstance(AES_ALGORITHM, provider)
                    : Cipher.getInstance(AES_ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));
            return cipher.doFinal(plaintext);
        } catch (Exception e) {
            throw new HsmException("AES 암호화 실패: " + e.getMessage(), e);
        }
    }

    /** AES 암호화 - extractable 키 전용 (JVM 소프트웨어 Provider 사용). */
    public byte[] encryptAes(SecretKey secretKey, byte[] iv, byte[] plaintext) throws HsmException {
        return encryptAes(secretKey, iv, plaintext, null);
    }

    /**
     * AES/CBC/PKCS5Padding 복호화.
     * extractable 키: provider=null 로 JVM 소프트웨어 복호화.
     * non-extractable 키: provider 에 pkcs11Provider 를 전달해야 합니다.
     *
     * @param secretKey  대칭키 (HSM 추출 키 또는 소프트웨어 키)
     * @param iv         초기화 벡터 (16바이트)
     * @param ciphertext 암호문 바이트
     * @param provider   null 이면 JVM 기본 Provider 사용
     */
    public byte[] decryptAes(SecretKey secretKey, byte[] iv, byte[] ciphertext, Provider provider) throws HsmException {
        try {
            Cipher cipher = (provider != null)
                    ? Cipher.getInstance(AES_ALGORITHM, provider)
                    : Cipher.getInstance(AES_ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));
            return cipher.doFinal(ciphertext);
        } catch (Exception e) {
            throw new HsmException("AES 복호화 실패: " + e.getMessage(), e);
        }
    }

    /** AES 복호화 - extractable 키 전용 (JVM 소프트웨어 Provider 사용). */
    public byte[] decryptAes(SecretKey secretKey, byte[] iv, byte[] ciphertext) throws HsmException {
        return decryptAes(secretKey, iv, ciphertext, null);
    }

}
