/**
 * AEAD wrapper around AES-256-GCM via SubtleCrypto.
 *
 * Wire format for one ciphertext blob:
 *
 *   nonce(12) || ciphertext-with-tag(N+16)
 *
 * On disk we store this as a single BLOB column; AAD is reconstructed at
 * read-time from (table, column, pk) and is NOT stored.
 */

const NONCE_LEN = 12;

// Local mirror of the WebCrypto KeyUsage union — avoids depending on
// `lib.dom` (we run on Bun + plain ES2022) while keeping API parity.
type WebCryptoKeyUsage =
  | 'encrypt'
  | 'decrypt'
  | 'sign'
  | 'verify'
  | 'deriveKey'
  | 'deriveBits'
  | 'wrapKey'
  | 'unwrapKey';

function bs(u: Uint8Array): ArrayBuffer {
  return u as unknown as ArrayBuffer;
}

async function importKey(key: Uint8Array, usages: WebCryptoKeyUsage[]): Promise<CryptoKey> {
  if (key.length !== 32) throw new Error(`AES-256-GCM key must be 32 bytes, got ${key.length}`);
  return globalThis.crypto.subtle.importKey('raw', bs(key), 'AES-GCM', false, usages);
}

/**
 * Encrypt a plaintext blob with the given key and a deterministic nonce.
 * Returns `nonce || ct||tag` as a single Uint8Array suitable for direct
 * BLOB storage.
 */
export async function aeadSeal(
  key: Uint8Array,
  nonce: Uint8Array,
  plaintext: Uint8Array,
  aad: Uint8Array,
): Promise<Uint8Array> {
  if (nonce.length !== NONCE_LEN) throw new Error(`nonce must be ${NONCE_LEN} bytes`);
  const aesKey = await importKey(key, ['encrypt']);
  const ct = await globalThis.crypto.subtle.encrypt(
    { name: 'AES-GCM', iv: bs(nonce), additionalData: bs(aad) },
    aesKey,
    bs(plaintext),
  );
  const ctU8 = new Uint8Array(ct);
  const out = new Uint8Array(NONCE_LEN + ctU8.length);
  out.set(nonce, 0);
  out.set(ctU8, NONCE_LEN);
  return out;
}

/**
 * Decrypt a `nonce || ct||tag` blob. The expected nonce is verified against
 * the prefix to detect tampering before we even reach the AEAD; if the
 * caller passes a `expectedNonce`, mismatch throws before SubtleCrypto runs.
 */
export async function aeadOpen(
  key: Uint8Array,
  blob: Uint8Array,
  aad: Uint8Array,
  expectedNonce?: Uint8Array,
): Promise<Uint8Array> {
  if (blob.length < NONCE_LEN + 16) throw new Error('ciphertext blob too short');
  const nonce = blob.subarray(0, NONCE_LEN);
  const ct = blob.subarray(NONCE_LEN);
  if (expectedNonce !== undefined && !ctEqual(nonce, expectedNonce)) {
    throw new Error('nonce mismatch — ciphertext blob has been tampered or row identity changed');
  }
  const aesKey = await importKey(key, ['decrypt']);
  const pt = await globalThis.crypto.subtle.decrypt(
    { name: 'AES-GCM', iv: bs(nonce), additionalData: bs(aad) },
    aesKey,
    bs(ct),
  );
  return new Uint8Array(pt);
}

function ctEqual(a: Uint8Array, b: Uint8Array): boolean {
  if (a.length !== b.length) return false;
  let diff = 0;
  for (let i = 0; i < a.length; i++) diff |= a[i]! ^ b[i]!;
  return diff === 0;
}

export const AEAD_NONCE_LEN = NONCE_LEN;
export const AEAD_TAG_LEN = 16;
