package org.openautonomousconnection.webserver.utils;

import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Objects;

/**
 * Provides hashing utilities for the registrar frontend.
 *
 * <p>Username: SHA-256 hex
 * <p>Password: PBKDF2WithHmacSHA256 storage format:
 * PBKDF2$sha256$<iterations>$<saltB64>$<hashB64>
 */
public final class WebHasher {

    private final SecureRandom rng = new SecureRandom();
    private final int pbkdf2Iterations;
    private final int pbkdf2SaltBytes;
    private final int pbkdf2KeyBytes;

    /**
     * Creates a hasher.
     *
     * @param pbkdf2Iterations iterations (recommended >= 100k)
     * @param pbkdf2SaltBytes salt bytes
     * @param pbkdf2KeyBytes derived key bytes
     */
    public WebHasher(int pbkdf2Iterations, int pbkdf2SaltBytes, int pbkdf2KeyBytes) {
        if (pbkdf2Iterations < 10_000) throw new IllegalArgumentException("pbkdf2Iterations too low");
        if (pbkdf2SaltBytes < 8) throw new IllegalArgumentException("pbkdf2SaltBytes too low");
        if (pbkdf2KeyBytes < 16) throw new IllegalArgumentException("pbkdf2KeyBytes too low");
        this.pbkdf2Iterations = pbkdf2Iterations;
        this.pbkdf2SaltBytes = pbkdf2SaltBytes;
        this.pbkdf2KeyBytes = pbkdf2KeyBytes;
    }

    /**
     * SHA-256 hashes text (lowercase hex).
     *
     * @param text input
     * @return sha256 hex
     */
    public String sha256Hex(String text) {
        if (text == null) text = "";
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            byte[] digest = md.digest(text.getBytes(StandardCharsets.UTF_8));
            return toHexLower(digest);
        } catch (Exception e) {
            throw new IllegalStateException("SHA-256 not available", e);
        }
    }

    /**
     * PBKDF2-hashes a raw password into storage format.
     *
     * @param password raw password
     * @return encoded storage string
     */
    public String pbkdf2Hash(String password) {
        Objects.requireNonNull(password, "password");
        byte[] salt = new byte[pbkdf2SaltBytes];
        rng.nextBytes(salt);

        byte[] dk = derive(password.toCharArray(), salt, pbkdf2Iterations, pbkdf2KeyBytes);

        return "PBKDF2$sha256$" + pbkdf2Iterations + "$" +
                Base64.getEncoder().encodeToString(salt) + "$" +
                Base64.getEncoder().encodeToString(dk);
    }

    /**
     * Verifies a raw password against a stored PBKDF2 string.
     *
     * @param password raw password
     * @param stored stored format
     * @return true if valid
     */
    public boolean pbkdf2Verify(String password, String stored) {
        if (password == null || stored == null) return false;

        String[] parts = stored.split("\\$");
        if (parts.length != 5) return false;
        if (!"PBKDF2".equals(parts[0])) return false;
        if (!"sha256".equalsIgnoreCase(parts[1])) return false;

        int it;
        try {
            it = Integer.parseInt(parts[2]);
        } catch (NumberFormatException e) {
            return false;
        }

        byte[] salt;
        byte[] expected;
        try {
            salt = Base64.getDecoder().decode(parts[3]);
            expected = Base64.getDecoder().decode(parts[4]);
        } catch (Exception e) {
            return false;
        }

        byte[] actual = derive(password.toCharArray(), salt, it, expected.length);
        return constantTimeEquals(expected, actual);
    }

    private static byte[] derive(char[] password, byte[] salt, int iterations, int keyBytes) {
        try {
            PBEKeySpec spec = new PBEKeySpec(password, salt, iterations, keyBytes * 8);
            SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
            return skf.generateSecret(spec).getEncoded();
        } catch (Exception e) {
            throw new IllegalStateException("PBKDF2 not available", e);
        }
    }

    private static boolean constantTimeEquals(byte[] a, byte[] b) {
        if (a == null || b == null) return false;
        if (a.length != b.length) return false;
        int r = 0;
        for (int i = 0; i < a.length; i++) r |= (a[i] ^ b[i]);
        return r == 0;
    }

    private static String toHexLower(byte[] data) {
        final char[] hex = "0123456789abcdef".toCharArray();
        char[] out = new char[data.length * 2];
        int i = 0;
        for (byte b : data) {
            out[i++] = hex[(b >>> 4) & 0x0F];
            out[i++] = hex[b & 0x0F];
        }
        return new String(out);
    }
}
