package com.eactive.eai.common.hsm;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.security.AuthProvider;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.Provider;
import java.security.Security;
import java.security.UnrecoverableKeyException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import javax.crypto.SecretKey;

import org.springframework.stereotype.Component;

import com.eactive.eai.agent.encryption.EncryptionManager;
import com.eactive.eai.common.exception.ExceptionUtil;
import com.eactive.eai.common.lifecycle.Lifecycle;
import com.eactive.eai.common.lifecycle.LifecycleException;
import com.eactive.eai.common.lifecycle.LifecycleListener;
import com.eactive.eai.common.lifecycle.LifecycleSupport;
import com.eactive.eai.common.property.PropManager;
import com.eactive.eai.common.util.ApplicationContextProvider;
import com.eactive.eai.common.util.Logger;

/**
 * SafeNet ProtectServer HSM 연동 관리자 (JDK 8 / SunPKCS11)
 *
 * PropManager 그룹 "HSM" 에서 읽는 키:
 *   PKCS11_CONFIG  - pkcs11.cfg 파일 내용
 *   PIN            - HSM 슬롯 PIN
 *
 * PKCS11_CONFIG 값 예시 (개행은 \n 으로 입력):
 *   name = ProtectServer
 *   library = /opt/safenet/protecttoolkit5/ptk/lib/libcryptoki.so
 *   slot = 0
 *   
 *   name = SoftHSM
 *   library = C:/SoftHSM2/lib/softhsm2-x64.dll
 *   slotListIndex = 0
 *
 * ---------------------------------------------------------------------
 * [세션 누적 방지 / 회로차단 로직 추가]
 * 기존 구현은 재로드마다 KeyStore.getInstance(...).load(null, pin) 을 새로
 * 호출하여 PKCS11 세션(C_OpenSession)이 계속 누적되고, 결국 HSM 파티션의
 * 최대 세션 수를 초과하면서 reload 가 영구적으로 실패하는 문제가 있었다.
 * 이를 방지하기 위해:
 *   1) 정상 상황에서는 "기존 keyStore 인스턴스"에 다시 load() 하여 세션 재사용
 *   2) 실제 키 조회(probe)로 세션이 살아있는지 검증
 *   3) 연속 실패가 임계치를 넘으면 Provider 자체를 logout 후 완전히 재생성
 *   4) 재생성마저 실패하면 마지막으로 성공한 keyStore 를 유지 (서비스 연속성 우선)
 * ---------------------------------------------------------------------
 */
@Component
public class HsmManager implements Lifecycle {

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

    private static final String GROUP_NAME = "HSM";
    private static final String PROP_CONFIG = "PKCS11_CONFIG";
    private static final String PROP_PIN = "PIN";
    private static final String PROP_RELOAD_INTERVAL_MINUTES = "RELOAD_INTERVAL_MINUTES";

    private Provider pkcs11Provider;
    private volatile KeyStore keyStore;
    private boolean started;

    private final LifecycleSupport lifecycle = new LifecycleSupport(this);

    private volatile char[] pin;

    // 재로드 주기 (분 단위). 필요시 PropManager로 외부화 가능.
    private static long RELOAD_INTERVAL_MINUTES = 1;
    private ScheduledExecutorService scheduler;
    private ScheduledFuture<?> reloadFuture;

    private HsmManager() {
    }

    public static HsmManager getInstance() {
        return ApplicationContextProvider.getContext().getBean(HsmManager.class);
    }

    @Override
    public void start() throws LifecycleException {
        if (started) {
            throw new LifecycleException("RECEAIHSM001");
        }
        lifecycle.fireLifecycleEvent(STARTING_EVENT, this);

        try {
            init();
            startReloadScheduler();
        } catch (Exception e) {
            throw new LifecycleException(ExceptionUtil.getErrorCode(e, "RECEAIHSM002"));
        }

        started = true;
        lifecycle.fireLifecycleEvent(STARTED_EVENT, this);
    }

    private void init() throws Exception {

        String reloadIntervalStr = PropManager.getInstance().getProperty(GROUP_NAME, PROP_RELOAD_INTERVAL_MINUTES, "10");
        RELOAD_INTERVAL_MINUTES = Long.parseLong(reloadIntervalStr);
        EncryptionManager encManager = EncryptionManager.getInstance();

        String configContent = encManager.decryptDBData(PropManager.getInstance().getProperty(GROUP_NAME, PROP_CONFIG));
        String pinStr = encManager.decryptDBData(PropManager.getInstance().getProperty(GROUP_NAME, PROP_PIN));

        if (configContent == null || configContent.trim().isEmpty()) {
            logger.warn("HsmManager] PKCS11_CONFIG 가 설정되지 않았습니다. HSM 초기화를 건너뜁니다.");
            return;
        }

        pkcs11Provider = createProvider(configContent.trim().replace("\\n", "\n"));

        // 이미 등록된 Provider 가 있으면 제거 후 재등록
        Provider existing = Security.getProvider(pkcs11Provider.getName());
        if (existing != null) {
            Security.removeProvider(existing.getName());
        }
        Security.addProvider(pkcs11Provider);

        keyStore = KeyStore.getInstance("PKCS11", pkcs11Provider);
        this.pin = (pinStr != null) ? pinStr.toCharArray() : null;
        keyStore.load(null, pin);

        logger.warn("HsmManager] 초기화 완료. Provider=" + pkcs11Provider.getName());

        logKeyStore(this.keyStore);
    }

	private void logKeyStore(KeyStore keyStore) throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
		java.util.Enumeration<String> aliases = keyStore.aliases();
        StringBuilder aliasList = new StringBuilder();
        while (aliases.hasMoreElements()) {
            if (aliasList.length() > 0) aliasList.append(", ");

            String alias = aliases.nextElement();
            aliasList.append(alias);

            java.security.Key key = keyStore.getKey(alias, null);
            if (key instanceof SecretKey) {
                SecretKey secretKey = (SecretKey) key;
                if (secretKey.getEncoded() == null) {
                    logger.warn("HsmManager] HSM - secretKey null {} : [{}]", alias, secretKey);
                }
            }
        }
        logger.warn("HsmManager] HSM 키 목록: [" + aliasList + "]");
	}

    /**
     * 별도 스레드에서 주기적으로 KeyStore 를 다시 로드하여
     * HSM 에 새로 생성/추가된 키를 인식하도록 한다.
     */
    private void startReloadScheduler() {
        scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
            Thread t = new Thread(r, "hsm-keystore-reloader");
            t.setDaemon(true);
            return t;
        });

        reloadFuture = scheduler.scheduleWithFixedDelay(
                this::reloadKeyStoreSafely,
                RELOAD_INTERVAL_MINUTES,
                RELOAD_INTERVAL_MINUTES,
                TimeUnit.MINUTES
        );

        logger.warn("HsmManager] KeyStore 주기적 재로드 스케줄러 시작. interval=" + RELOAD_INTERVAL_MINUTES + "분");
    }

    /**
     * 스케줄러에서 호출되는 래퍼. 예외가 스케줄러 스레드를 죽이지 않도록 반드시 catch 한다.
     * (ScheduledExecutorService 는 task 에서 예외가 던져지면 이후 스케줄을 자동으로 중단시킨다)
     */
    private void reloadKeyStoreSafely() {
        try {
            reloadKeyStoreIfNeeded();
        } catch (Throwable t) {
            logger.warn("HsmManager] KeyStore 주기적 재로드 실패: " + t.getMessage(), t);
        }
    }

    /**
     * KeyStore 를 다시 로드한다. 외부(getSecretKey 등)에서 키 미스 발생 시
     * 즉시 재시도용으로 직접 호출할 수도 있다.
     *
     * 세션 누적 방지를 위해 새 KeyStore 인스턴스를 만들지 않고,
     * 기존 keyStore 객체에 다시 load() 하여 기존 PKCS11 세션을 재사용한다.
     * 연속 실패가 임계치를 넘으면 Provider 자체를 재생성한다.
     */
    public synchronized void reloadKeyStoreIfNeeded() throws Exception {
        if (pkcs11Provider == null) {
            return; // HSM 비활성화 상태
        }

        try {
            if (keyStore != null) {
                keyStore.load(null, pin);
                logger.warn("HsmManager] KeyStore 재로드 완료 (기존 세션 재사용).");
            } else {
                KeyStore ks = KeyStore.getInstance("PKCS11", pkcs11Provider);
                ks.load(null, pin);
                this.keyStore = ks;
                logger.warn("HsmManager] KeyStore 신규 생성 완료.");
            }

            logKeyStore(keyStore);

         } catch (Exception e) {
            logger.warn("HsmManager] KeyStore 재로드 실패:" + e.getMessage(), e);
            logger.warn("HsmManager] Provider 전체 재초기화를 시도합니다.");
            fullReinitialize();
        }
    }

    /**
     * 세션 누적, 네트워크 단절 등으로 일반 재로드가 더 이상 복구되지 않을 때
     * 기존 세션을 정리하고 Provider 를 완전히 새로 생성한다.
     *
     * 신규 Provider/KeyStore 준비가 완전히 성공한 후에만 기존 Provider 를 제거하고 교체한다.
     * 재초기화 중 예외가 발생하면 기존 Provider 와 keyStore 를 그대로 유지한다
     * (서비스 중단보다 마지막 정상 상태 보존을 우선).
     */
    private void fullReinitialize() throws Exception {
        Provider oldProvider = this.pkcs11Provider;
        try {
            // 1. 신규 Provider 인스턴스 생성 (Security 미등록 상태)
            String configContent = EncryptionManager.getInstance()
                    .decryptDBData(PropManager.getInstance().getProperty(GROUP_NAME, PROP_CONFIG));
            Provider newProvider = createProvider(configContent.trim().replace("\\n", "\n"));

            // 2. 신규 Provider 로 KeyStore 로드 테스트
            //    KeyStore.getInstance(type, providerInstance) 는 Security 등록 없이도 동작하므로
            //    여기서 실패해도 oldProvider/keyStore 는 변경되지 않은 상태를 유지함
            KeyStore ks = KeyStore.getInstance("PKCS11", newProvider);
            ks.load(null, pin);

            // 3. 신규 연결 성공 → 기존 Provider 정리 후 교체
            if (oldProvider instanceof AuthProvider) {
                try {
                    ((AuthProvider) oldProvider).logout();
                } catch (Exception logoutEx) {
                    logger.warn("HsmManager] 기존 세션 logout 실패(무시하고 진행): " + logoutEx.getMessage());
                }
            }
            Security.removeProvider(oldProvider.getName());
            Security.addProvider(newProvider);

            this.pkcs11Provider = newProvider;
            this.keyStore = ks;

            logger.warn("HsmManager] Provider 전체 재초기화 성공.");

        } catch (Exception e) {
            logger.warn("HsmManager] Provider 전체 재초기화 실패. 이전 keyStore 를 그대로 유지합니다: " + e.getMessage(), e);
            throw e;
        }
    }

    /**
     * JDK 버전에 따라 SunPKCS11 Provider 를 생성한다.
     *
     * JDK 8 : SunPKCS11(InputStream) 생성자를 리플렉션으로 호출
     * JDK 9+: Provider.configure(configFilePath) 를 리플렉션으로 호출
     *
     * 두 경로 모두 리플렉션을 사용하므로 컴파일 타임에 JDK 버전 의존성이 없다.
     */
    static Provider createProvider(String cfgContent) throws Exception {
        String javaVersion = System.getProperty("java.version");
        if (javaVersion.startsWith("1.")) {
            // JDK 8: new SunPKCS11(InputStream)
            InputStream is = new ByteArrayInputStream(cfgContent.getBytes("UTF-8"));
            return (Provider) Class.forName("sun.security.pkcs11.SunPKCS11")
                    .getConstructor(InputStream.class)
                    .newInstance(is);
        } else {
            // JDK 9+: Security.getProvider("SunPKCS11").configure(configFilePath)
            // configure() 는 JDK 9 에서 추가된 메서드이므로 리플렉션으로 호출
            File tmp = File.createTempFile("pkcs11-hsm-", ".cfg");
            tmp.deleteOnExit();
            Files.write(tmp.toPath(), cfgContent.getBytes("UTF-8"));
            Provider base = Security.getProvider("SunPKCS11");
            Method configure = Provider.class.getMethod("configure", String.class);
            return (Provider) configure.invoke(base, tmp.getAbsolutePath());
        }
    }

    @Override
    public void stop() throws LifecycleException {
        if (!started) {
            throw new LifecycleException("RECEAIHSM003");
        }
        lifecycle.fireLifecycleEvent(STOPING_EVENT, this);

        if (reloadFuture != null) {
            reloadFuture.cancel(false);
        }
        if (scheduler != null) {
            scheduler.shutdown();
        }

        if (pkcs11Provider != null) {
            if (pkcs11Provider instanceof AuthProvider) {
                try {
                    ((AuthProvider) pkcs11Provider).logout();
                } catch (Exception e) {
                    logger.warn("HsmManager] 종료 시 logout 실패(무시): " + e.getMessage());
                }
            }
            Security.removeProvider(pkcs11Provider.getName());
        }
        pkcs11Provider = null;
        keyStore = null;
        started = false;

        lifecycle.fireLifecycleEvent(STOPPED_EVENT, this);
    }

    @Override
    public void addLifecycleListener(LifecycleListener listener) {
        lifecycle.addLifecycleListener(listener);
    }

    @Override
    public LifecycleListener[] findLifecycleListeners() {
        return lifecycle.findLifecycleListeners();
    }

    @Override
    public void removeLifecycleListener(LifecycleListener listener) {
        lifecycle.removeLifecycleListener(listener);
    }

    @Override
    public boolean isStarted() {
        return started;
    }

    public Provider getPkcs11Provider() {
        return pkcs11Provider;
    }

    public KeyStore getKeyStore() {
        return keyStore;
    }

    public boolean isReady() {
        return started && pkcs11Provider != null && keyStore != null;
    }

    /**
     * isReady() 와 달리 실제 HSM 호출로 세션 생존 여부까지 확인하는 헬스체크.
     * 모니터링/헬스체크 엔드포인트에서 사용을 권장한다.
     */
    public boolean isHealthy() {
        if (!isReady()) {
            return false;
        }
        try {
            logKeyStore(keyStore);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}