package no.zyon.shade.storage

import org.bouncycastle.crypto.generators.Argon2BytesGenerator
import org.bouncycastle.crypto.generators.SCrypt
import org.bouncycastle.crypto.params.Argon2Parameters
import java.text.Normalizer

/**
 * Password / PIN key-derivation primitives. Mirror
 * `@shade/storage-encrypted/crypto/kdf` (`deriveMasterKey` /
 * `deriveMasterKeyArgon2id`) byte-for-byte — Tink doesn't ship password
 * KDFs so we wrap Bouncy Castle.
 *
 * Both functions normalize string passphrases to NFKC before hashing,
 * matching the TS implementation's `passphrase.normalize('NFKC')`.
 * This ensures the same password typed on different OSes/keyboards
 * produces the same master key regardless of which compatibility-form
 * the input arrived in.
 *
 * The reference test-vector lives in `test-vectors/storage-encryption.json`
 * and `test-vectors/blob-storage.json`. Cross-platform parity is gated
 * by `CrossPlatformVectorTest`.
 */

/** scrypt parameters. Defaults match `DEFAULT_SCRYPT` in TS. */
data class ScryptParams(
    /** CPU/memory cost. Must be a power of 2. */
    val n: Int = 1 shl 17,
    /** Block size. */
    val r: Int = 8,
    /** Parallelization. */
    val p: Int = 1,
    /** Output length in bytes. */
    val dkLen: Int = 32,
)

/** Argon2id parameters. Defaults match `DEFAULT_ARGON2ID` in TS. */
data class Argon2idParams(
    /** Memory cost in KiB. Default 64 MiB. */
    val m: Int = 64 * 1024,
    /** Time cost (iterations). Default 3. */
    val t: Int = 3,
    /** Parallelism. Default 1. */
    val p: Int = 1,
    /** Output length in bytes. Default 32. */
    val dkLen: Int = 32,
)

/**
 * Derive a 32-byte master key from a passphrase + salt via scrypt.
 * Salt MUST be at least 16 bytes and persisted alongside the
 * encrypted database. Throws on empty passphrase.
 */
fun deriveMasterKey(
    passphrase: String,
    salt: ByteArray,
    params: ScryptParams = ScryptParams(),
): ByteArray {
    require(passphrase.isNotEmpty()) { "passphrase must be non-empty" }
    require(salt.size >= 16) { "salt must be at least 16 bytes" }
    val nfkc = Normalizer.normalize(passphrase, Normalizer.Form.NFKC)
    val pwBytes = nfkc.toByteArray(Charsets.UTF_8)
    return SCrypt.generate(pwBytes, salt, params.n, params.r, params.p, params.dkLen)
}

/**
 * Derive a 32-byte master key from a low-entropy secret (PIN) + salt
 * via argon2id. Salt MUST be at least 16 bytes. The Bouncy Castle
 * `Argon2BytesGenerator` parameters mirror RFC 9106's argon2id mode
 * with version 1.3 (`Argon2Parameters.ARGON2_VERSION_13`), which is
 * what `@noble/hashes/argon2` produces — keeping cross-platform parity.
 */
fun deriveMasterKeyArgon2id(
    secret: String,
    salt: ByteArray,
    params: Argon2idParams = Argon2idParams(),
): ByteArray {
    require(secret.isNotEmpty()) { "argon2id secret must be non-empty" }
    require(salt.size >= 16) { "salt must be at least 16 bytes" }
    val nfkc = Normalizer.normalize(secret, Normalizer.Form.NFKC)
    return deriveMasterKeyArgon2id(nfkc.toByteArray(Charsets.UTF_8), salt, params)
}

/**
 * Byte-array overload — useful when the secret is already binary
 * (e.g. derived from a hardware token rather than typed) and
 * shouldn't be NFKC-normalized as text.
 */
fun deriveMasterKeyArgon2id(
    secret: ByteArray,
    salt: ByteArray,
    params: Argon2idParams = Argon2idParams(),
): ByteArray {
    require(secret.isNotEmpty()) { "argon2id secret must be non-empty" }
    require(salt.size >= 16) { "salt must be at least 16 bytes" }
    val builder = Argon2Parameters.Builder(Argon2Parameters.ARGON2_id)
        .withVersion(Argon2Parameters.ARGON2_VERSION_13)
        .withIterations(params.t)
        .withMemoryAsKB(params.m)
        .withParallelism(params.p)
        .withSalt(salt)
    val gen = Argon2BytesGenerator()
    gen.init(builder.build())
    val out = ByteArray(params.dkLen)
    gen.generateBytes(secret, out)
    return out
}
