/** Canonical JSON and a small cross-runtime checksum (no Node or Web APIs). */

function canonical(value: unknown, stack: Set<object>): string {
  if (value === null) return "null";
  if (typeof value === "string" || typeof value === "boolean") return JSON.stringify(value);
  if (typeof value === "number") {
    if (!Number.isFinite(value)) throw new TypeError("arena checksums require finite numbers");
    return Object.is(value, -0) ? "0" : JSON.stringify(value);
  }
  if (Array.isArray(value)) {
    if (stack.has(value)) throw new TypeError("arena checksums do not accept cycles");
    stack.add(value);
    const result = `[${value.map((entry) => canonical(entry, stack)).join(",")}]`;
    stack.delete(value);
    return result;
  }
  if (typeof value === "object") {
    if (stack.has(value)) throw new TypeError("arena checksums do not accept cycles");
    stack.add(value);
    const record = value as Record<string, unknown>;
    const entries = Object.keys(record)
      .filter((key) => record[key] !== undefined)
      .sort()
      .map((key) => `${JSON.stringify(key)}:${canonical(record[key], stack)}`);
    stack.delete(value);
    return `{${entries.join(",")}}`;
  }
  throw new TypeError(`arena checksums do not accept ${typeof value}`);
}

export function canonicalJson(value: unknown): string {
  return canonical(value, new Set());
}

/** 64-bit FNV-1a over UTF-8, encoded with an algorithm prefix. */
export function arenaChecksum(value: unknown): string {
  const bytes = new TextEncoder().encode(canonicalJson(value));
  let hash = 0xcbf29ce484222325n;
  for (const byte of bytes) {
    hash ^= BigInt(byte);
    hash = BigInt.asUintN(64, hash * 0x100000001b3n);
  }
  return `fnv1a64:${hash.toString(16).padStart(16, "0")}`;
}
