/**
 * T2 — synchronous AEAD primitives over @noble/ciphers (AES-256-GCM + XChaCha20-Poly1305).
 *
 * SYNCHRONOUS so it satisfies the frozen §4.4 `sealFrame`/`openFrame` synchronous signatures.
 * @noble/ciphers is audited, isomorphic, and provides XChaCha (which WebCrypto lacks). Noble's
 * one-shot AEAD returns `ciphertext‖tag`; §4.4 `E2EEnvelope` keeps them split, so we slice the
 * fixed 16-byte tag off the tail on seal and re-join on open.
 */
import { gcm } from '@noble/ciphers/aes'
import { xchacha20poly1305 } from '@noble/ciphers/chacha'
import type { AeadAlg, AeadKey } from 'relay-contracts'
import { NONCE_BYTES } from 'relay-contracts'
import { AeadOpenError, E2EError } from './errors.js'
import { unwrapAeadKey, wrapAeadKey } from './aead-key.js'

const TAG_BYTES = 16 as const

/** Nonce width for an AEAD alg (12 GCM · 24 XChaCha, §4.4). */
export function nonceLength(alg: AeadAlg): 12 | 24 {
  const width = NONCE_BYTES[alg]
  return width as 12 | 24
}

/** Fixed AEAD tag length (16 bytes for both algs). */
export function tagLength(_alg: AeadAlg): 16 {
  return TAG_BYTES
}

/** Construct the frozen opaque `AeadKey` from 32 raw bytes (P4-local constructor, INDEX §2.1). */
export function importAeadKey(raw: Uint8Array, alg: AeadAlg, aadLabel = ''): AeadKey {
  return wrapAeadKey(raw, alg, aadLabel)
}

function cipherFor(alg: AeadAlg, raw: Uint8Array, nonce: Uint8Array, aad: Uint8Array) {
  if (nonce.length !== nonceLength(alg)) {
    throw new E2EError(
      'E2E_NONCE_LENGTH',
      `nonce length ${nonce.length} != required ${nonceLength(alg)} for ${alg}`,
    )
  }
  return alg === 'aes-256-gcm' ? gcm(raw, nonce, aad) : xchacha20poly1305(raw, nonce, aad)
}

/** Seal plaintext; returns split ciphertext + 16-byte tag (§4.4 E2EEnvelope). */
export function aeadSeal(
  key: AeadKey,
  nonce: Uint8Array,
  plaintext: Uint8Array,
  aad: Uint8Array,
): { ciphertext: Uint8Array; tag: Uint8Array } {
  const { raw, alg } = unwrapAeadKey(key)
  const sealed = cipherFor(alg, raw, nonce, aad).encrypt(plaintext)
  const cut = sealed.length - TAG_BYTES
  return { ciphertext: sealed.slice(0, cut), tag: sealed.slice(cut) }
}

/** Open ciphertext+tag; throws {@link AeadOpenError} on any tag/aad/key/nonce mismatch. */
export function aeadOpen(
  key: AeadKey,
  nonce: Uint8Array,
  ciphertext: Uint8Array,
  tag: Uint8Array,
  aad: Uint8Array,
): Uint8Array {
  if (tag.length !== TAG_BYTES) {
    throw new AeadOpenError(`tag length ${tag.length} != ${TAG_BYTES}`)
  }
  const { raw, alg } = unwrapAeadKey(key)
  const joined = new Uint8Array(ciphertext.length + tag.length)
  joined.set(ciphertext, 0)
  joined.set(tag, ciphertext.length)
  try {
    return cipherFor(alg, raw, nonce, aad).decrypt(joined)
  } catch {
    throw new AeadOpenError()
  }
}
