package com.erp.util;

import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Arrays;

/**
 * Node.js CredentialEncryption (utils/credentialEncryption.ts) 와 호환되는 Java 구현.
 *
 * 알고리즘: AES-256-GCM
 * 키 유도:  scrypt(password, "salt", N=16384, r=8, p=1, dkLen=32)
 * 저장 형식: iv_hex:authtag_hex:ciphertext_hex
 */
public class CredentialEncryption {

    private static final String ALGORITHM   = "AES/GCM/NoPadding";
    private static final int    GCM_TAG_LEN = 128; // bits
    private static final int    IV_LEN      = 16;  // bytes

    private final byte[] key;

    public CredentialEncryption(String secretKey) {
        // scrypt(secretKey.bytes, "salt".bytes, N=16384, r=8, p=1, dkLen=32) — Node 기본 파라미터
        this.key = SCrypt.generate(
                secretKey.getBytes(StandardCharsets.UTF_8),
                "salt".getBytes(StandardCharsets.UTF_8),
                16384, 8, 1, 32);
    }

    /** 평문 → IV:AuthTag:Ciphertext (all hex) */
    public String encrypt(String plaintext) {
        try {
            byte[] iv = new byte[IV_LEN];
            new SecureRandom().nextBytes(iv);

            SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
            GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LEN, iv);

            Cipher cipher = Cipher.getInstance(ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, keySpec, spec);

            byte[] ciphertextWithTag = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));

            // Java GCM appends auth-tag at the end of ciphertext
            int cipherLen = ciphertextWithTag.length - (GCM_TAG_LEN / 8);
            byte[] ciphertext = Arrays.copyOf(ciphertextWithTag, cipherLen);
            byte[] authTag    = Arrays.copyOfRange(ciphertextWithTag, cipherLen, ciphertextWithTag.length);

            return toHex(iv) + ":" + toHex(authTag) + ":" + toHex(ciphertext);
        } catch (Exception e) {
            throw new RuntimeException("암호화 실패", e);
        }
    }

    /** IV:AuthTag:Ciphertext → 평문 */
    public String decrypt(String encrypted) {
        try {
            String[] parts = encrypted.split(":");
            if (parts.length != 3) throw new IllegalArgumentException("잘못된 암호화 형식");

            byte[] iv         = fromHex(parts[0]);
            byte[] authTag    = fromHex(parts[1]);
            byte[] ciphertext = fromHex(parts[2]);

            // Java GCM needs ciphertext+tag combined
            byte[] combined = Arrays.copyOf(ciphertext, ciphertext.length + authTag.length);
            System.arraycopy(authTag, 0, combined, ciphertext.length, authTag.length);

            SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
            GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LEN, iv);

            Cipher cipher = Cipher.getInstance(ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, keySpec, spec);
            return new String(cipher.doFinal(combined), StandardCharsets.UTF_8);
        } catch (Exception e) {
            throw new RuntimeException("복호화 실패: " + e.getMessage(), e);
        }
    }

    // ── Hex utilities ──────────────────────────────────────────────

    private static String toHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder(bytes.length * 2);
        for (byte b : bytes) sb.append(String.format("%02x", b));
        return sb.toString();
    }

    private static byte[] fromHex(String hex) {
        int len = hex.length();
        byte[] data = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
                    + Character.digit(hex.charAt(i + 1), 16));
        }
        return data;
    }

    // ══════════════════════════════════════════════════════════════
    // Pure-Java SCrypt (RFC 7914) — no Bouncy Castle needed
    // Node.js crypto.scryptSync 와 동일한 결과 보장
    // ══════════════════════════════════════════════════════════════

    static final class SCrypt {

        static byte[] generate(byte[] password, byte[] salt, int N, int r, int p, int dkLen) {
            try {
                int blockSize = 128 * r;
                byte[] B = pbkdf2(password, salt, 1, p * blockSize);

                for (int i = 0; i < p; i++) {
                    romix(r, B, i * blockSize, N);
                }

                return pbkdf2(password, B, 1, dkLen);
            } catch (Exception e) {
                throw new RuntimeException("SCrypt 오류", e);
            }
        }

        private static void romix(int r, byte[] B, int bOff, int N) {
            int bsWords = 32 * r;
            int[] X = new int[bsWords];
            int[] V = new int[bsWords * N];

            // Load B → X (little-endian 32-bit words)
            for (int i = 0; i < bsWords; i++) {
                int o = bOff + i * 4;
                X[i] = (B[o] & 0xff) | ((B[o+1] & 0xff) << 8) | ((B[o+2] & 0xff) << 16) | ((B[o+3] & 0xff) << 24);
            }

            // Fill V with successive blockMix outputs
            for (int i = 0; i < N; i++) {
                System.arraycopy(X, 0, V, i * bsWords, bsWords);
                blockMix(r, X);
            }

            // XOR-mix with random V entries
            for (int i = 0; i < N; i++) {
                int j = X[(2 * r - 1) * 16] & (N - 1);
                for (int k = 0; k < bsWords; k++) X[k] ^= V[j * bsWords + k];
                blockMix(r, X);
            }

            // Write X → B (little-endian)
            for (int i = 0; i < bsWords; i++) {
                int o = bOff + i * 4;
                B[o]   = (byte) X[i];
                B[o+1] = (byte)(X[i] >>> 8);
                B[o+2] = (byte)(X[i] >>> 16);
                B[o+3] = (byte)(X[i] >>> 24);
            }
        }

        private static void blockMix(int r, int[] B) {
            int[] X = new int[16];
            System.arraycopy(B, (2 * r - 1) * 16, X, 0, 16);

            int[] Y = new int[B.length];
            for (int i = 0; i < 2 * r; i++) {
                for (int k = 0; k < 16; k++) X[k] ^= B[i * 16 + k];
                salsa20_8(X);
                int dest = (i % 2 == 0) ? i / 2 : r + i / 2;
                System.arraycopy(X, 0, Y, dest * 16, 16);
            }
            System.arraycopy(Y, 0, B, 0, B.length);
        }

        // Salsa20/8 core — 8 rounds (wg/scrypt reference)
        private static void salsa20_8(int[] B) {
            int[] x = B.clone();
            for (int i = 0; i < 4; i++) {
                x[ 4] ^= R(x[ 0]+x[12], 7);  x[ 8] ^= R(x[ 4]+x[ 0], 9);
                x[12] ^= R(x[ 8]+x[ 4],13);  x[ 0] ^= R(x[12]+x[ 8],18);
                x[ 9] ^= R(x[ 5]+x[ 1], 7);  x[13] ^= R(x[ 9]+x[ 5], 9);
                x[ 1] ^= R(x[13]+x[ 9],13);  x[ 5] ^= R(x[ 1]+x[13],18);
                x[14] ^= R(x[10]+x[ 6], 7);  x[ 2] ^= R(x[14]+x[10], 9);
                x[ 6] ^= R(x[ 2]+x[14],13);  x[10] ^= R(x[ 6]+x[ 2],18);
                x[ 3] ^= R(x[15]+x[11], 7);  x[ 7] ^= R(x[ 3]+x[15], 9);
                x[11] ^= R(x[ 7]+x[ 3],13);  x[15] ^= R(x[11]+x[ 7],18);
                x[ 1] ^= R(x[ 0]+x[ 3], 7);  x[ 2] ^= R(x[ 1]+x[ 0], 9);
                x[ 3] ^= R(x[ 2]+x[ 1],13);  x[ 0] ^= R(x[ 3]+x[ 2],18);
                x[ 6] ^= R(x[ 5]+x[ 4], 7);  x[ 7] ^= R(x[ 6]+x[ 5], 9);
                x[ 4] ^= R(x[ 7]+x[ 6],13);  x[ 5] ^= R(x[ 4]+x[ 7],18);
                x[11] ^= R(x[10]+x[ 9], 7);  x[ 8] ^= R(x[11]+x[10], 9);
                x[ 9] ^= R(x[ 8]+x[11],13);  x[10] ^= R(x[ 9]+x[ 8],18);
                x[12] ^= R(x[15]+x[14], 7);  x[13] ^= R(x[12]+x[15], 9);
                x[14] ^= R(x[13]+x[12],13);  x[15] ^= R(x[14]+x[13],18);
            }
            for (int i = 0; i < 16; i++) B[i] += x[i];
        }

        private static int R(int a, int b) { return (a << b) | (a >>> (32 - b)); }

        /** PBKDF2-HMAC-SHA256 (c=1 is the primary usage in scrypt) */
        private static byte[] pbkdf2(byte[] password, byte[] salt, int c, int dkLen) throws Exception {
            Mac mac = Mac.getInstance("HmacSHA256");
            SecretKeySpec spec = new SecretKeySpec(password, "HmacSHA256");
            int hLen = 32;

            byte[] dk = new byte[dkLen];
            byte[] U  = new byte[salt.length + 4];
            System.arraycopy(salt, 0, U, 0, salt.length);

            int blocks = (dkLen + hLen - 1) / hLen;
            for (int blk = 1; blk <= blocks; blk++) {
                U[salt.length]     = (byte)(blk >>> 24);
                U[salt.length + 1] = (byte)(blk >>> 16);
                U[salt.length + 2] = (byte)(blk >>>  8);
                U[salt.length + 3] = (byte) blk;

                mac.init(spec);
                byte[] T = mac.doFinal(U);
                byte[] prev = T.clone();

                for (int j = 1; j < c; j++) {
                    mac.init(spec);
                    prev = mac.doFinal(prev);
                    for (int k = 0; k < hLen; k++) T[k] ^= prev[k];
                }

                int off = (blk - 1) * hLen;
                System.arraycopy(T, 0, dk, off, Math.min(hLen, dkLen - off));
            }
            return dk;
        }
    }
}
