package com.zioinfo.hrm.config;

import com.zioinfo.hrm.auth.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Base64;

/**
 * admin 비밀번호 암호화 재시드(UIMS 방식 + 암호화 저장).
 *
 * <p>기동 시 멱등 재시드:
 * <pre>
 *   ADMIN_KEY_FILE(hex 32바이트 키 파일) 로드 → ADMIN_PASSWORD_ENC(base64(nonce12 + ct + tag), AES-256-GCM) 복호
 *   → admin 계정 BCrypt 해시 갱신(하드코딩 admin123 시드를 env 값으로 덮어씀).
 * </pre>
 *
 * <p>안전 규칙:
 * <ul>
 *   <li>env 미설정 / 키 로드 실패 / 복호 실패 시 <b>재시드 스킵</b>(기동 계속). WARN 로그에 <b>값(평문/키/시크릿) 미기록</b>.</li>
 *   <li>새 admin 비밀번호 값은 서버 env 에만 존재 — 코드/DB/로그/응답에 평문 미기재(보안 불변규칙).</li>
 *   <li>키 값은 별도 파일(root 600)에만 — env/코드/git 에 키 미기재. 여기서는 파일 경로만 읽는다.</li>
 * </ul>
 *
 * <p>스키마 init(schema.sql: admin/admin123 시드) 이후 실행되어 기존 해시를 덮어쓴다(멱등: 매 기동 동일 결과).
 */
@Slf4j
@Component
@Order(Integer.MIN_VALUE + 10)
@RequiredArgsConstructor
public class AdminPasswordSeeder implements ApplicationRunner {

    private static final String ADMIN_USERNAME = "admin";
    private static final int GCM_TAG_BITS = 128;
    private static final int NONCE_LEN = 12;

    @Value("${ADMIN_PASSWORD_ENC:}")
    private String adminPasswordEnc;

    @Value("${ADMIN_KEY_FILE:}")
    private String adminKeyFile;

    private final UserMapper userMapper;
    private final PasswordEncoder passwordEncoder;

    @Override
    public void run(ApplicationArguments args) {
        if (isBlank(adminPasswordEnc) || isBlank(adminKeyFile)) {
            log.info("[admin-reseed] ADMIN_PASSWORD_ENC/ADMIN_KEY_FILE 미설정 — 재시드 스킵(기동 계속)");
            return;
        }
        char[] plain = null;
        byte[] key = null;
        try {
            key = loadHexKey(adminKeyFile);          // hex 32바이트 → 32B (AES-256)
            plain = decrypt(adminPasswordEnc, key);  // base64(nonce12+ct+tag) → 평문
            String hash = passwordEncoder.encode(new String(plain));
            int updated = userMapper.updatePasswordByUsername(ADMIN_USERNAME, hash);
            if (updated > 0) {
                log.info("[admin-reseed] admin 비밀번호 env 값으로 재시드 완료(멱등)");
            } else {
                log.warn("[admin-reseed] admin 계정 미존재 — 재시드 스킵");
            }
        } catch (Exception e) {
            // 값 미기록: 예외 클래스명만(메시지에 평문/키가 섞일 여지 차단)
            log.warn("[admin-reseed] 복호/재시드 실패 — 스킵(기동 계속). cause={}", e.getClass().getSimpleName());
        } finally {
            if (plain != null) Arrays.fill(plain, '\0');
            if (key != null) Arrays.fill(key, (byte) 0);
        }
    }

    /** 키 파일(hex 문자열, 공백/개행 무시)을 32바이트 키로 로드. 32바이트가 아니면 예외. */
    private static byte[] loadHexKey(String path) throws Exception {
        String hex = Files.readString(Path.of(path), StandardCharsets.UTF_8)
                .replaceAll("\\s", "");
        byte[] key = hexToBytes(hex);
        if (key.length != 32) {
            throw new IllegalStateException("key length != 32 bytes");
        }
        return key;
    }

    /** base64(nonce12 + ciphertext + tag) → AES-256-GCM 복호 평문(char[]). */
    private static char[] decrypt(String encBase64, byte[] key) throws Exception {
        byte[] blob = Base64.getDecoder().decode(encBase64.trim());
        if (blob.length <= NONCE_LEN) {
            throw new IllegalStateException("cipher blob too short");
        }
        byte[] nonce = Arrays.copyOfRange(blob, 0, NONCE_LEN);
        byte[] ct = Arrays.copyOfRange(blob, NONCE_LEN, blob.length);
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"),
                new GCMParameterSpec(GCM_TAG_BITS, nonce));
        byte[] out = cipher.doFinal(ct);
        char[] chars = new String(out, StandardCharsets.UTF_8).toCharArray();
        Arrays.fill(out, (byte) 0);
        return chars;
    }

    private static byte[] hexToBytes(String hex) {
        int len = hex.length();
        if (len % 2 != 0) {
            throw new IllegalStateException("odd hex length");
        }
        byte[] out = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            out[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
                    + Character.digit(hex.charAt(i + 1), 16));
        }
        return out;
    }

    private static boolean isBlank(String s) {
        return s == null || s.isBlank();
    }
}
