package no.zyon.shade.blob

import javax.crypto.Cipher
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec

/**
 * AEAD wrapper for the V4.9 profile blob.
 *
 * Wire format for one ciphertext blob:
 *   `nonce(12) || ciphertext(N) || tag(16)`
 *
 * Mirror `@shade/storage-encrypted/crypto/aead.ts` byte-for-byte. The
 * relay stores this as a single opaque BLOB column; AAD is reconstructed
 * at read-time as `"shade-profile-aad-v1:" + slotIdHex` and is NOT
 * stored on the relay.
 */

const val BLOB_AEAD_NONCE_LEN = 12
const val BLOB_AEAD_TAG_LEN = 16

private const val MIN_CIPHERTEXT_LEN = BLOB_AEAD_NONCE_LEN + BLOB_AEAD_TAG_LEN

/**
 * Seal a plaintext blob. Returns `nonce || ct||tag` ready for direct
 * blob storage. The caller supplies the nonce so this function is
 * deterministic — the high-level Profile namespace generates a fresh
 * 12-byte random nonce per write to keep (key, nonce, plaintext)
 * unique across re-uploads.
 */
fun aeadSeal(
    key: ByteArray,
    nonce: ByteArray,
    plaintext: ByteArray,
    aad: ByteArray,
): ByteArray {
    require(key.size == 32) { "AES-256-GCM key must be 32 bytes" }
    require(nonce.size == BLOB_AEAD_NONCE_LEN) {
        "nonce must be $BLOB_AEAD_NONCE_LEN bytes"
    }
    val cipher = Cipher.getInstance("AES/GCM/NoPadding")
    val spec = GCMParameterSpec(128, nonce)
    cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, "AES"), spec)
    cipher.updateAAD(aad)
    val ctTag = cipher.doFinal(plaintext)
    val out = ByteArray(BLOB_AEAD_NONCE_LEN + ctTag.size)
    System.arraycopy(nonce, 0, out, 0, BLOB_AEAD_NONCE_LEN)
    System.arraycopy(ctTag, 0, out, BLOB_AEAD_NONCE_LEN, ctTag.size)
    return out
}

/**
 * Open a `nonce || ct||tag` blob and return the plaintext. Throws on
 * tamper (AEAD tag mismatch) or short input. The caller may pass an
 * `expectedNonce` to enforce a deterministic nonce — mismatch throws
 * before the AEAD even runs (defense-in-depth against a relay returning
 * the wrong slot's blob).
 */
fun aeadOpen(
    key: ByteArray,
    blob: ByteArray,
    aad: ByteArray,
    expectedNonce: ByteArray? = null,
): ByteArray {
    require(key.size == 32) { "AES-256-GCM key must be 32 bytes" }
    require(blob.size >= MIN_CIPHERTEXT_LEN) { "ciphertext blob too short" }
    val nonce = blob.copyOfRange(0, BLOB_AEAD_NONCE_LEN)
    if (expectedNonce != null && !ctEqual(nonce, expectedNonce)) {
        throw IllegalArgumentException(
            "nonce mismatch — ciphertext blob has been tampered or row identity changed",
        )
    }
    val ctTag = blob.copyOfRange(BLOB_AEAD_NONCE_LEN, blob.size)
    val cipher = Cipher.getInstance("AES/GCM/NoPadding")
    val spec = GCMParameterSpec(128, nonce)
    cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), spec)
    cipher.updateAAD(aad)
    return cipher.doFinal(ctTag)
}

private fun ctEqual(a: ByteArray, b: ByteArray): Boolean {
    if (a.size != b.size) return false
    var diff = 0
    for (i in a.indices) {
        diff = diff or (a[i].toInt() xor b[i].toInt())
    }
    return diff == 0
}

/** Build the AAD for a given slotId hex string. */
fun blobAadForSlot(slotIdHex: String): ByteArray {
    require(slotIdHex.length == 64) { "slotIdHex must be 64 hex chars" }
    return "shade-profile-aad-v1:$slotIdHex".toByteArray(Charsets.UTF_8)
}
