/**
 * §4.4 E2EEnvelope binary codec + deterministic nonce helper.
 *
 * relay-contracts owns the WIRE SHAPE of the envelope (so P2 agent and P6 browser frame it
 * identically); the AEAD that fills `ciphertext`/`tag` lives in P4. This is a pure byte layout.
 *
 * Envelope wire layout (big-endian), fixed 14-byte header then three variable regions:
 *   offset size field
 *     0     8   seq            uint64 (strictly monotonic per direction, INV13)
 *     8     1   nonceLen       uint8
 *     9     4   ciphertextLen  uint32
 *    13     1   tagLen         uint8
 *    14   nonceLen        nonce
 *    ..   ciphertextLen   ciphertext
 *    ..   tagLen          tag
 */
import { ContractDecodeError, ContractEncodeError } from '../errors.js'
import {
  U32_MAX,
  concatBytes,
  readUint32BE,
  readUint64BE,
  writeUint32BE,
  writeUint64BE,
} from '../bytes.js'
import { NONCE_BYTES, type AeadAlg, type E2EEnvelope } from './types.js'

const ENVELOPE_HEADER_BYTES = 14
const U8_MAX = 0xff

/**
 * Deterministic per-`seq` nonce = f(seq) (§4.4): the 8-byte big-endian sequence number in the
 * LAST 8 bytes of a zero-filled nonce of the alg's width. No random branch (INV13). The
 * direction-split subkeys (c2h/h2c) ensure the same (seq → nonce) never reuses a (key, nonce).
 */
export function nonceForSeq(seq: bigint, alg: AeadAlg): Uint8Array {
  const width = NONCE_BYTES[alg]
  const nonce = new Uint8Array(width)
  writeUint64BE(nonce, width - 8, seq)
  return nonce
}

/** Encode an E2EEnvelope to its wire bytes. */
export function encodeEnvelope(env: E2EEnvelope): Uint8Array {
  if (env.nonce.length > U8_MAX) {
    throw new ContractEncodeError(`nonce too long: ${env.nonce.length} > ${U8_MAX}`)
  }
  if (env.tag.length > U8_MAX) {
    throw new ContractEncodeError(`tag too long: ${env.tag.length} > ${U8_MAX}`)
  }
  if (env.ciphertext.length > U32_MAX) {
    throw new ContractEncodeError(`ciphertext too long: ${env.ciphertext.length} > ${U32_MAX}`)
  }
  const header = new Uint8Array(ENVELOPE_HEADER_BYTES)
  let offset = writeUint64BE(header, 0, env.seq)
  header[offset] = env.nonce.length
  offset += 1
  offset = writeUint32BE(header, offset, env.ciphertext.length)
  header[offset] = env.tag.length
  return concatBytes([header, env.nonce, env.ciphertext, env.tag])
}

/** Decode wire bytes into an E2EEnvelope. Rejects truncated or trailing-byte inputs. */
export function decodeEnvelope(buf: Uint8Array): E2EEnvelope {
  if (buf.length < ENVELOPE_HEADER_BYTES) {
    throw new ContractDecodeError(
      `envelope shorter than header: ${buf.length} < ${ENVELOPE_HEADER_BYTES}`,
    )
  }
  const seq = readUint64BE(buf, 0)
  const nonceLen = buf[8]!
  const ciphertextLen = readUint32BE(buf, 9)
  const tagLen = buf[13]!
  const total = ENVELOPE_HEADER_BYTES + nonceLen + ciphertextLen + tagLen
  if (buf.length !== total) {
    throw new ContractDecodeError(
      `envelope length mismatch: buffer ${buf.length}, header implies ${total}`,
    )
  }
  let offset = ENVELOPE_HEADER_BYTES
  const nonce = buf.slice(offset, offset + nonceLen)
  offset += nonceLen
  const ciphertext = buf.slice(offset, offset + ciphertextLen)
  offset += ciphertextLen
  const tag = buf.slice(offset, offset + tagLen)
  return { seq, nonce, ciphertext, tag }
}

/** Read the uint32 ciphertext length directly (helper for callers that pre-parse). */
export function readCiphertextLen(buf: Uint8Array): number {
  if (buf.length < ENVELOPE_HEADER_BYTES) {
    throw new ContractDecodeError('envelope truncated before ciphertextLen')
  }
  return readUint32BE(buf, 9)
}
