import { ValidationError } from '@shade/core';

export const STREAM_NONCE_BYTES = 12;

/** Maximum chunk seq value (u64 max). Hard-spec'd hard limit. */
export const MAX_SEQ = 0xffff_ffff_ffff_ffffn;

/**
 * Construct the deterministic AES-GCM nonce for a stream chunk.
 *
 *   nonce[0..4]  = u32_be(laneId)
 *   nonce[4..12] = u64_be(seq)
 *
 * Per (laneId, seq) is unique — combined with the lane-specific key, this
 * guarantees AES-GCM nonce-uniqueness even across multiple parallel lanes.
 */
export function buildChunkNonce(laneId: number, seq: number | bigint): Uint8Array {
  if (!Number.isInteger(laneId) || laneId < 0 || laneId > 0xffff_ffff) {
    throw new ValidationError(`laneId must fit in u32: ${laneId}`, 'laneId');
  }
  const seqBig = typeof seq === 'bigint' ? seq : BigInt(seq);
  if (seqBig < 0n || seqBig > MAX_SEQ) {
    throw new ValidationError(`seq must fit in u64 (>= 0): ${seq}`, 'seq');
  }
  const out = new Uint8Array(STREAM_NONCE_BYTES);
  const view = new DataView(out.buffer);
  view.setUint32(0, laneId, false);
  view.setBigUint64(4, seqBig, false);
  return out;
}

/**
 * Build the AAD bound to a stream chunk. Computed implicitly on both sides
 * from the chunk header (never transmitted as-is). Tampering with any header
 * field invalidates the AEAD tag.
 *
 *   aad = streamId(16) || u32_be(laneId) || u64_be(seq) || u8(isLast)
 */
export function buildChunkAad(
  streamId: Uint8Array,
  laneId: number,
  seq: number | bigint,
  isLast: boolean,
): Uint8Array {
  if (streamId.length !== 16) {
    throw new ValidationError('streamId must be 16 bytes', 'streamId');
  }
  if (!Number.isInteger(laneId) || laneId < 0 || laneId > 0xffff_ffff) {
    throw new ValidationError(`laneId must fit in u32: ${laneId}`, 'laneId');
  }
  const seqBig = typeof seq === 'bigint' ? seq : BigInt(seq);
  if (seqBig < 0n || seqBig > MAX_SEQ) {
    throw new ValidationError(`seq must fit in u64 (>= 0): ${seq}`, 'seq');
  }
  const out = new Uint8Array(16 + 4 + 8 + 1);
  out.set(streamId, 0);
  const view = new DataView(out.buffer);
  view.setUint32(16, laneId, false);
  view.setBigUint64(20, seqBig, false);
  out[28] = isLast ? 0x01 : 0x00;
  return out;
}
