import { CompressedBitvector } from './succinct/CompressedBitvector.js';
import { FlatWaveletTree } from './succinct/FlatWaveletTree.js';
import { WaveletAdjacency } from './WaveletAdjacency.js';
import { CondensedGraph } from '../CondensedGraph.js';
import { LazyNodeIdTable } from './LazyNodeIdTable.js';
import { MinimalPerfectHash } from './PerfectHash.js';

const MAGIC = 0x43474231; // CGB1

class BinaryWriter {
  constructor() {
    this.chunks = [];
    this.length = 0;
    this.encoder = new TextEncoder();
  }

  align(alignment) {
    if (alignment <= 1) return;
    const padding = (alignment - (this.length % alignment)) % alignment;
    if (!padding) return;
    this._push(new Uint8Array(padding));
  }

  writeUint8(value) {
    const buf = new Uint8Array(1);
    buf[0] = value & 0xFF;
    this._push(buf);
  }

  writeUint16(value) {
    const buf = new Uint8Array(2);
    new DataView(buf.buffer).setUint16(0, value, true);
    this._push(buf);
  }

  writeUint32(value) {
    const buf = new Uint8Array(4);
    new DataView(buf.buffer).setUint32(0, value >>> 0, true);
    this._push(buf);
  }

  writeInt32(value) {
    const buf = new Uint8Array(4);
    new DataView(buf.buffer).setInt32(0, value | 0, true);
    this._push(buf);
  }

  writeFloat64(value) {
    const buf = new Uint8Array(8);
    new DataView(buf.buffer).setFloat64(0, value, true);
    this._push(buf);
  }

  writeBytes(bytes) {
    this._push(bytes);
  }

  writeString(value) {
    if (value === undefined || value === null) {
      this.writeUint32(0xFFFFFFFF);
      return;
    }
    const encoded = this.encoder.encode(String(value));
    this.writeUint32(encoded.length);
    this._push(encoded);
  }

  _push(bytes) {
    this.chunks.push(bytes);
    this.length += bytes.byteLength;
  }

  toArrayBuffer() {
    const buffer = new Uint8Array(this.length);
    let offset = 0;
    for (const chunk of this.chunks) {
      buffer.set(chunk, offset);
      offset += chunk.byteLength;
    }
    return buffer.buffer;
  }
}

class BinaryReader {
  constructor(buffer) {
    // Restore input arrives as ArrayBuffer (serializer) or Buffer/Uint8Array
    // (fs, network, mutations) — normalize to a view over a real
    // ArrayBuffer so DataView never throws a caller-contract TypeError.
    if (buffer instanceof ArrayBuffer) {
      this.view = new DataView(buffer);
    } else if (ArrayBuffer.isView(buffer)) {
      this.view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
    } else {
      throw new Error('CondensedGraphBinary: snapshot input must be an ArrayBuffer, Buffer, or Uint8Array');
    }
    this.offset = 0;
    this.decoder = new TextDecoder();
  }

  /**
   * Bounds guard: the buffer is caller-supplied (snapshot restore input),
   * so every read must be validated BEFORE the DataView/Uint8Array throws
   * or over-allocates. A clean error here prevents RangeError crashes and
   * oversized allocations from malformed buffers.
   */
  ensure(bytes) {
    if (bytes < 0 || this.offset + bytes > this.view.byteLength) {
      throw new Error(
        `CondensedGraphBinary: read past end of buffer (offset ${this.offset}, need ${bytes}, length ${this.view.byteLength})`
      );
    }
  }

  align(alignment) {
    if (alignment <= 1) return;
    const padding = (alignment - (this.offset % alignment)) % alignment;
    this.offset += padding;
  }

  readUint8() {
    this.ensure(1);
    const value = this.view.getUint8(this.offset);
    this.offset += 1;
    return value;
  }

  readUint16() {
    this.ensure(2);
    const value = this.view.getUint16(this.offset, true);
    this.offset += 2;
    return value;
  }

  readUint32() {
    this.ensure(4);
    const value = this.view.getUint32(this.offset, true);
    this.offset += 4;
    return value;
  }

  readInt32() {
    this.ensure(4);
    const value = this.view.getInt32(this.offset, true);
    this.offset += 4;
    return value;
  }

  readFloat64() {
    this.ensure(8);
    const value = this.view.getFloat64(this.offset, true);
    this.offset += 8;
    return value;
  }

  readBytes(length) {
    this.ensure(length);
    // this.offset is relative to the VIEW; the underlying ArrayBuffer may
    // be a shared node pool with a nonzero byteOffset (Buffer inputs).
    // Building the slice from this.view.buffer without byteOffset would
    // read the wrong memory region entirely.
    const bytes = new Uint8Array(this.view.buffer, this.view.byteOffset + this.offset, length);
    this.offset += length;
    return bytes;
  }

  readString() {
    const length = this.readUint32();
    if (length === 0xFFFFFFFF) return null;
    if (length === 0) return '';
    const bytes = this.readBytes(length);
    return this.decoder.decode(bytes);
  }
}

function writeCompressedBitvector(writer, bitvector) {
  writer.writeUint32(bitvector.length);
  writer.writeUint32(bitvector.bitvector.length);
  writer.align(4);
  writer.writeBytes(new Uint8Array(bitvector.bitvector.buffer, bitvector.bitvector.byteOffset, bitvector.bitvector.byteLength));
  const blockSize = bitvector.rankSelect.blockSize;
  const blockRank = bitvector.rankSelect.blockRank;
  writer.writeUint32(blockSize);
  writer.writeUint32(blockRank.length);
  writer.align(4);
  writer.writeBytes(new Uint8Array(blockRank.buffer, blockRank.byteOffset, blockRank.byteLength));
}

function readCompressedBitvector(reader) {
  const length = reader.readUint32();
  const wordCount = reader.readUint32();
  const byteLength = wordCount * 4;
  reader.align(4);
  const bytes = reader.readBytes(byteLength);
  const bitvector = new Uint32Array(bytes.buffer, bytes.byteOffset, wordCount);
  const blockSize = reader.readUint32();
  const blockRankLength = reader.readUint32();
  const rankByteLength = blockRankLength * 4;
  reader.align(4);
  const rankBytes = reader.readBytes(rankByteLength);
  const blockRank = new Uint32Array(rankBytes.buffer, rankBytes.byteOffset, blockRankLength);
  return CompressedBitvector.fromPackedWithRank(bitvector, length, blockSize, blockRank);
}

// Recursive wavelet tree serialization removed (legacy v1-v3)

function writeWaveletTreeFlat(writer, tree) {
  const nodes = [];
  const stack = [tree];
  while (stack.length) {
    const node = stack.pop();
    if (!node) continue;
    nodes.push(node);
    if (node.rightChild) stack.push(node.rightChild);
    if (node.leftChild) stack.push(node.leftChild);
  }

  const nodeCount = nodes.length;
  const indexByNode = new Map();
  for (let i = 0; i < nodeCount; i++) {
    indexByNode.set(nodes[i], i);
  }

  let totalWords = 0;
  let totalRank = 0;
  for (const node of nodes) {
    if (node.bitvector) {
      totalWords += node.bitvector.bitvector.length;
      totalRank += node.bitvector.rankSelect.blockRank.length;
    }
  }

  const alphabetMin = new Uint32Array(nodeCount);
  const alphabetMax = new Uint32Array(nodeCount);
  const lengths = new Uint32Array(nodeCount);
  const isLeaf = new Uint8Array(nodeCount);
  const leftIndex = new Int32Array(nodeCount);
  const rightIndex = new Int32Array(nodeCount);
  const bitvectorWordOffset = new Uint32Array(nodeCount);
  const bitvectorWordCount = new Uint32Array(nodeCount);
  const bitvectorLength = new Uint32Array(nodeCount);
  const blockSize = new Uint32Array(nodeCount);
  const blockRankOffset = new Uint32Array(nodeCount);
  const blockRankLength = new Uint32Array(nodeCount);

  const words = new Uint32Array(totalWords);
  const rankWords = new Uint32Array(totalRank);
  let wordOffset = 0;
  let rankOffset = 0;

  for (let i = 0; i < nodeCount; i++) {
    const node = nodes[i];
    alphabetMin[i] = node.alphabetMin;
    alphabetMax[i] = node.alphabetMax;
    lengths[i] = node.length;
    isLeaf[i] = node.isLeaf ? 1 : 0;
    leftIndex[i] = node.leftChild ? indexByNode.get(node.leftChild) : -1;
    rightIndex[i] = node.rightChild ? indexByNode.get(node.rightChild) : -1;

    if (node.bitvector) {
      const bv = node.bitvector.bitvector;
      const rank = node.bitvector.rankSelect.blockRank;
      bitvectorWordOffset[i] = wordOffset;
      bitvectorWordCount[i] = bv.length;
      bitvectorLength[i] = node.bitvector.length;
      blockSize[i] = node.bitvector.rankSelect.blockSize;
      blockRankOffset[i] = rankOffset;
      blockRankLength[i] = rank.length;
      words.set(bv, wordOffset);
      rankWords.set(rank, rankOffset);
      wordOffset += bv.length;
      rankOffset += rank.length;
    } else {
      bitvectorWordOffset[i] = wordOffset;
      bitvectorWordCount[i] = 0;
      bitvectorLength[i] = 0;
      blockSize[i] = 0;
      blockRankOffset[i] = rankOffset;
      blockRankLength[i] = 0;
    }
  }

  writer.writeUint32(nodeCount);
  writer.align(4);
  writer.writeBytes(new Uint8Array(alphabetMin.buffer, alphabetMin.byteOffset, alphabetMin.byteLength));
  writer.align(4);
  writer.writeBytes(new Uint8Array(alphabetMax.buffer, alphabetMax.byteOffset, alphabetMax.byteLength));
  writer.align(4);
  writer.writeBytes(new Uint8Array(lengths.buffer, lengths.byteOffset, lengths.byteLength));
  writer.align(4);
  writer.writeBytes(new Uint8Array(leftIndex.buffer, leftIndex.byteOffset, leftIndex.byteLength));
  writer.align(4);
  writer.writeBytes(new Uint8Array(rightIndex.buffer, rightIndex.byteOffset, rightIndex.byteLength));
  writer.align(4);
  writer.writeBytes(new Uint8Array(bitvectorWordOffset.buffer, bitvectorWordOffset.byteOffset, bitvectorWordOffset.byteLength));
  writer.align(4);
  writer.writeBytes(new Uint8Array(bitvectorWordCount.buffer, bitvectorWordCount.byteOffset, bitvectorWordCount.byteLength));
  writer.align(4);
  writer.writeBytes(new Uint8Array(bitvectorLength.buffer, bitvectorLength.byteOffset, bitvectorLength.byteLength));
  writer.align(4);
  writer.writeBytes(new Uint8Array(blockSize.buffer, blockSize.byteOffset, blockSize.byteLength));
  writer.align(4);
  writer.writeBytes(new Uint8Array(blockRankOffset.buffer, blockRankOffset.byteOffset, blockRankOffset.byteLength));
  writer.align(4);
  writer.writeBytes(new Uint8Array(blockRankLength.buffer, blockRankLength.byteOffset, blockRankLength.byteLength));
  writer.writeUint32(isLeaf.length);
  writer.writeBytes(isLeaf);

  writer.writeUint32(words.length);
  writer.align(4);
  writer.writeBytes(new Uint8Array(words.buffer, words.byteOffset, words.byteLength));
  writer.writeUint32(rankWords.length);
  writer.align(4);
  writer.writeBytes(new Uint8Array(rankWords.buffer, rankWords.byteOffset, rankWords.byteLength));
}

function readWaveletTreeFlat(reader) {
  const nodeCount = reader.readUint32();
  reader.align(4);
  const alphaMinBytes = reader.readBytes(nodeCount * 4);
  const alphabetMin = new Uint32Array(alphaMinBytes.buffer, alphaMinBytes.byteOffset, nodeCount);
  reader.align(4);
  const alphaMaxBytes = reader.readBytes(nodeCount * 4);
  const alphabetMax = new Uint32Array(alphaMaxBytes.buffer, alphaMaxBytes.byteOffset, nodeCount);
  reader.align(4);
  const lengthBytes = reader.readBytes(nodeCount * 4);
  const lengths = new Uint32Array(lengthBytes.buffer, lengthBytes.byteOffset, nodeCount);
  reader.align(4);
  const leftBytes = reader.readBytes(nodeCount * 4);
  const leftIndex = new Int32Array(leftBytes.buffer, leftBytes.byteOffset, nodeCount);
  reader.align(4);
  const rightBytes = reader.readBytes(nodeCount * 4);
  const rightIndex = new Int32Array(rightBytes.buffer, rightBytes.byteOffset, nodeCount);
  reader.align(4);
  const wordOffsetBytes = reader.readBytes(nodeCount * 4);
  const bitvectorWordOffset = new Uint32Array(wordOffsetBytes.buffer, wordOffsetBytes.byteOffset, nodeCount);
  reader.align(4);
  const wordCountBytes = reader.readBytes(nodeCount * 4);
  const bitvectorWordCount = new Uint32Array(wordCountBytes.buffer, wordCountBytes.byteOffset, nodeCount);
  reader.align(4);
  const bitvectorLengthBytes = reader.readBytes(nodeCount * 4);
  const bitvectorLength = new Uint32Array(bitvectorLengthBytes.buffer, bitvectorLengthBytes.byteOffset, nodeCount);
  reader.align(4);
  const blockSizeBytes = reader.readBytes(nodeCount * 4);
  const blockSize = new Uint32Array(blockSizeBytes.buffer, blockSizeBytes.byteOffset, nodeCount);
  reader.align(4);
  const blockRankOffsetBytes = reader.readBytes(nodeCount * 4);
  const blockRankOffset = new Uint32Array(blockRankOffsetBytes.buffer, blockRankOffsetBytes.byteOffset, nodeCount);
  reader.align(4);
  const blockRankLengthBytes = reader.readBytes(nodeCount * 4);
  const blockRankLength = new Uint32Array(blockRankLengthBytes.buffer, blockRankLengthBytes.byteOffset, nodeCount);
  const isLeafLength = reader.readUint32();
  const isLeafBytes = reader.readBytes(isLeafLength);
  const isLeaf = new Uint8Array(isLeafBytes.buffer, isLeafBytes.byteOffset, isLeafLength);

  const wordsLength = reader.readUint32();
  reader.align(4);
  const wordsBytes = reader.readBytes(wordsLength * 4);
  const bitvectorWords = new Uint32Array(wordsBytes.buffer, wordsBytes.byteOffset, wordsLength);
  const rankLength = reader.readUint32();
  reader.align(4);
  const rankBytes = reader.readBytes(rankLength * 4);
  const blockRankWords = new Uint32Array(rankBytes.buffer, rankBytes.byteOffset, rankLength);

  return new FlatWaveletTree({
    nodeCount,
    alphabetMin,
    alphabetMax,
    lengths,
    isLeaf,
    leftIndex,
    rightIndex,
    bitvectorWordOffset,
    bitvectorWordCount,
    bitvectorLength,
    blockSize,
    blockRankOffset,
    blockRankLength,
    bitvectorWords,
    blockRankWords
  });
}

function writeWaveletAdjacency(writer, adjacency) {
  writer.writeUint32(adjacency.numNodes);
  writeCompressedBitvector(writer, adjacency.boundaryBitvector);
  writer.writeUint32(adjacency.edgeIndices.length);
  writer.align(4);
  writer.writeBytes(new Uint8Array(adjacency.edgeIndices.buffer, adjacency.edgeIndices.byteOffset, adjacency.edgeIndices.byteLength));
  if (adjacency.waveletTree) {
    writer.writeUint8(1);
    writeWaveletTreeFlat(writer, adjacency.waveletTree);
  } else {
    writer.writeUint8(0);
  }
}

function readWaveletAdjacency(reader) {
  const numNodes = reader.readUint32();
  const boundaryBitvector = readCompressedBitvector(reader);
  const edgeCount = reader.readUint32();
  const edgeByteLength = edgeCount * 4;
  reader.align(4);
  const edgeBytes = reader.readBytes(edgeByteLength);
  const edgeIndices = new Int32Array(edgeBytes.buffer, edgeBytes.byteOffset, edgeCount);
  const hasTree = reader.readUint8() === 1;
  const waveletTree = hasTree ? readWaveletTreeFlat(reader) : null;
  return new WaveletAdjacency(numNodes, waveletTree, boundaryBitvector, edgeIndices);
}

function serializeCondensedGraph(graph) {
  const writer = new BinaryWriter();

  writer.writeUint32(MAGIC);
  writer.writeUint32(5);

  if (!graph._dropAdjacencyList) {
    throw new Error('CondensedGraph serialization requires dropAdjacencyList=true.');
  }
  if (graph.nodeHeads) {
    throw new Error('CondensedGraph serialization no longer supports nodeHeads.');
  }
  let flags = 0;
  // waveletAll is no longer serialized
  if (graph._waveletByRelId) flags |= 1 << 1;
  if (graph._dropAdjacencyList) flags |= 1 << 2;
  if (graph._mphEnabled && graph._mph) flags |= 1 << 4;
  const omitNodeIdTable = graph._omitNodeIdTable === true;
  const useNodeIdTable = !omitNodeIdTable && graph._useNodeIdTable && graph._mphEnabled && graph._mph;
  if (useNodeIdTable) flags |= 1 << 5;
  if (omitNodeIdTable) flags |= 1 << 6;
  writer.writeUint32(flags);

  writer.writeUint32(graph.numNodes);
  writer.writeUint32(graph.numEdges);
  writer.writeUint32(graph.edgeIndex);
  writer.writeUint32(graph.nextRelationId);

  writer.writeUint32(graph.numNodes);
  if (omitNodeIdTable) {
    // No node ID table stored.
  } else if (useNodeIdTable) {
    const encoder = new TextEncoder();
    const offsets = new Uint32Array(graph.numNodes + 1);
    const chunks = [];
    let totalLength = 0;
    for (let i = 0; i < graph.numNodes; i++) {
      const key = graph.getNodeKey(i);
      const encoded = encoder.encode(key === undefined || key === null ? '' : String(key));
      offsets[i] = totalLength;
      totalLength += encoded.length;
      chunks.push(encoded);
    }
    offsets[graph.numNodes] = totalLength;
    const bytes = new Uint8Array(totalLength);
    let offset = 0;
    for (const chunk of chunks) {
      bytes.set(chunk, offset);
      offset += chunk.length;
    }
    writer.writeUint32(bytes.length);
    writer.writeBytes(bytes);
    writer.writeUint32(offsets.length);
    writer.align(4);
    writer.writeBytes(new Uint8Array(offsets.buffer, offsets.byteOffset, offsets.byteLength));
  } else {
    for (let i = 0; i < graph.numNodes; i++) {
      writer.writeString(graph.nodeIds[i]);
    }
  }

  writer.writeUint32(graph.nextRelationId);
  for (let i = 0; i < graph.nextRelationId; i++) {
    writer.writeString(graph.relationIdToName[i]);
  }

  const edgeCount = graph.edgeIndex;
  writer.writeUint32(edgeCount);
  writer.align(4);
  const edgeArrays = ensureEdgeArrays(graph);
  writer.writeBytes(new Uint8Array(edgeArrays.edgeSrcIds.buffer, edgeArrays.edgeSrcIds.byteOffset, edgeCount * 4));
  writer.writeBytes(new Uint8Array(edgeArrays.edgeRelIds.buffer, edgeArrays.edgeRelIds.byteOffset, edgeCount * 4));
  writer.writeBytes(new Uint8Array(edgeArrays.edgeDstIds.buffer, edgeArrays.edgeDstIds.byteOffset, edgeCount * 4));
  writer.align(2);
  writer.writeBytes(new Uint8Array(edgeArrays.edgePossibilityBits.buffer, edgeArrays.edgePossibilityBits.byteOffset, edgeCount * 2));
  writer.writeBytes(new Uint8Array(edgeArrays.edgeReliabilityBits.buffer, edgeArrays.edgeReliabilityBits.byteOffset, edgeCount * 2));

  writer.writeUint32(edgeCount);
  writer.align(8);
  const values = new Float64Array(graph.values.buffer, graph.values.byteOffset, edgeCount);
  writer.writeBytes(new Uint8Array(values.buffer, values.byteOffset, values.byteLength));

  writer.writeUint32(graph.numNodes);
  writer.align(2);
  const degrees = new Uint16Array(graph.nodeDegrees.buffer, graph.nodeDegrees.byteOffset, graph.numNodes);
  writer.writeBytes(new Uint8Array(degrees.buffer, degrees.byteOffset, degrees.byteLength));

  if (graph._waveletAll) {
    throw new Error('CondensedGraph serialization no longer supports waveletAll in snapshots.');
  }

  if (graph._waveletByRelId) {
    writer.align(4);
    writer.writeUint32(graph._waveletByRelId.length);
    for (const wavelet of graph._waveletByRelId) {
      writer.writeUint8(wavelet ? 1 : 0);
      if (wavelet) writeWaveletAdjacency(writer, wavelet);
    }
  }

  if (graph._mphEnabled && graph._mph) {
    writer.align(4);
    const mph = graph._mph;
    writer.writeUint32(mph.seed1);
    writer.writeUint32(mph.seed2);
    writer.writeUint32(mph.bucketCount);
    writer.writeUint32(mph.seeds.length);
    writer.align(4);
    writer.writeBytes(new Uint8Array(mph.seeds.buffer, mph.seeds.byteOffset, mph.seeds.byteLength));
    writer.writeUint32(mph.table.length);
    writer.align(4);
    writer.writeBytes(new Uint8Array(mph.table.buffer, mph.table.byteOffset, mph.table.byteLength));
    const values = mph.values instanceof Int32Array
      ? mph.values
      : mph.values
        ? Int32Array.from(mph.values)
        : new Int32Array(mph.table.length).fill(-1);
    writer.writeUint32(values.length);
    writer.align(4);
    writer.writeBytes(new Uint8Array(values.buffer, values.byteOffset, values.byteLength));
    const keyIndices = mph.keyIndices instanceof Int32Array
      ? mph.keyIndices
      : mph.keyIndices
        ? Int32Array.from(mph.keyIndices)
        : new Int32Array(0);
    writer.writeUint32(keyIndices.length);
    writer.align(4);
    writer.writeBytes(new Uint8Array(keyIndices.buffer, keyIndices.byteOffset, keyIndices.byteLength));
  }

  return writer.toArrayBuffer();
}

function ensureEdgeArrays(graph) {
  if (graph.edgeSrcIds && graph.edgeRelIds && graph.edgeDstIds
      && graph.edgePossibilityBits && graph.edgeReliabilityBits) {
    return {
      edgeSrcIds: graph.edgeSrcIds,
      edgeRelIds: graph.edgeRelIds,
      edgeDstIds: graph.edgeDstIds,
      edgePossibilityBits: graph.edgePossibilityBits,
      edgeReliabilityBits: graph.edgeReliabilityBits
    };
  }

  if (!graph.adjacency) {
    throw new Error('Adjacency storage missing; cannot derive edge arrays.');
  }

  const edgeCount = graph.edgeIndex;
  const edgeSrcIds = new Int32Array(edgeCount);
  const edgeRelIds = new Int32Array(edgeCount);
  const edgeDstIds = new Int32Array(edgeCount);
  const edgePossibilityBits = new Uint16Array(edgeCount);
  const edgeReliabilityBits = new Uint16Array(edgeCount);

  for (let edgeIdx = 0; edgeIdx < edgeCount; edgeIdx++) {
    const baseIdx = edgeIdx * 6;
    edgeSrcIds[edgeIdx] = graph.adjacency[baseIdx + 0];
    edgeRelIds[edgeIdx] = graph.adjacency[baseIdx + 1];
    edgeDstIds[edgeIdx] = graph.adjacency[baseIdx + 2];
    edgePossibilityBits[edgeIdx] = graph.adjacency[baseIdx + 3] & 0xffff;
    edgeReliabilityBits[edgeIdx] = graph.adjacency[baseIdx + 4] & 0xffff;
  }

  graph.edgeSrcIds = edgeSrcIds;
  graph.edgeRelIds = edgeRelIds;
  graph.edgeDstIds = edgeDstIds;
  graph.edgePossibilityBits = edgePossibilityBits;
  graph.edgeReliabilityBits = edgeReliabilityBits;

  return { edgeSrcIds, edgeRelIds, edgeDstIds, edgePossibilityBits, edgeReliabilityBits };
}

function deserializeCondensedGraph(buffer, graphFactory) {
  const reader = new BinaryReader(buffer);
  const magic = reader.readUint32();
  if (magic !== MAGIC) {
    throw new Error('Invalid condensed graph binary');
  }
  const version = reader.readUint32();
  if (version !== 5) {
    throw new Error(`Unsupported condensed graph version ${version}`);
  }

  const flags = reader.readUint32();
  const allowedFlags = (1 << 1) | (1 << 2) | (1 << 4) | (1 << 5) | (1 << 6);
  if (flags & ~allowedFlags) {
    throw new Error(`Unsupported condensed graph flags ${flags}`);
  }
  const hasWaveletByRel = (flags & (1 << 1)) !== 0;
  const dropAdjacency = (flags & (1 << 2)) !== 0;
  const hasPerfectHash = (flags & (1 << 4)) !== 0;

  const graph = graphFactory ? graphFactory() : {};
  graph.numNodes = reader.readUint32();
  graph.numEdges = reader.readUint32();
  graph.edgeIndex = reader.readUint32();
  graph.nextRelationId = reader.readUint32();

  const nodeCount = reader.readUint32();
  const hasNodeIdTable = (flags & (1 << 5)) !== 0;
  const omitNodeIdTable = (flags & (1 << 6)) !== 0;
  if (omitNodeIdTable) {
    graph.nodeIds = null;
  } else if (version >= 3 && hasNodeIdTable) {
    const byteLength = reader.readUint32();
    const bytes = reader.readBytes(byteLength);
    const offsetsLength = reader.readUint32();
    reader.align(4);
    const offsetBytes = reader.readBytes(offsetsLength * 4);
    const offsets = new Uint32Array(offsetBytes.buffer, offsetBytes.byteOffset, offsetsLength);
    graph.nodeIds = new LazyNodeIdTable(bytes, offsets, { cacheLimit: 0 });
    graph._useNodeIdTable = true;
  } else {
    graph.nodeIds = new Array(nodeCount);
    for (let i = 0; i < nodeCount; i++) {
      graph.nodeIds[i] = reader.readString();
    }
  }

  const relationCount = reader.readUint32();
  graph.relationIdToName = new Array(relationCount);
  graph.relationNameToId = new Map();
  for (let i = 0; i < relationCount; i++) {
    const name = reader.readString();
    graph.relationIdToName[i] = name;
    if (name !== null) graph.relationNameToId.set(name, i);
  }

  const edgeCount = reader.readUint32();
  reader.align(4);
  const srcBytes = reader.readBytes(edgeCount * 4);
  const relBytes = reader.readBytes(edgeCount * 4);
  const dstBytes = reader.readBytes(edgeCount * 4);
  reader.align(2);
  const possBytes = reader.readBytes(edgeCount * 2);
  const relBitsBytes = reader.readBytes(edgeCount * 2);
  graph.edgeSrcIds = new Int32Array(srcBytes.buffer, srcBytes.byteOffset, edgeCount);
  graph.edgeRelIds = new Int32Array(relBytes.buffer, relBytes.byteOffset, edgeCount);
  graph.edgeDstIds = new Int32Array(dstBytes.buffer, dstBytes.byteOffset, edgeCount);
  graph.edgePossibilityBits = new Uint16Array(possBytes.buffer, possBytes.byteOffset, edgeCount);
  graph.edgeReliabilityBits = new Uint16Array(relBitsBytes.buffer, relBitsBytes.byteOffset, edgeCount);
  graph.adjacency = null;

  // Structural integrity gate: the buffer is caller-supplied (snapshot
  // restore input). Garbage counts or ids must be rejected here — a graph
  // that silently accepts out-of-range ids crashes or loops in the first
  // downstream consumer (the indices build iterates edgeIndex, not the
  // array length — a desynced count is a hang, not a crash). Clean Error,
  // never a masked corruption.
  const numNodes = graph.numNodes;
  if (numNodes !== nodeCount) {
    throw new Error(
      `CondensedGraphBinary: node count mismatch (header ${numNodes}, node table ${nodeCount})`
    );
  }
  if (graph.numEdges !== edgeCount || graph.edgeIndex !== edgeCount) {
    throw new Error(
      `CondensedGraphBinary: edge count mismatch (numEdges ${graph.numEdges}, edgeIndex ${graph.edgeIndex}, arrays ${edgeCount})`
    );
  }
  if (graph.nextRelationId !== relationCount) {
    throw new Error(
      `CondensedGraphBinary: relation count mismatch (nextRelationId ${graph.nextRelationId}, names ${relationCount})`
    );
  }
  for (let i = 0; i < edgeCount; i++) {
    const src = graph.edgeSrcIds[i];
    const rel = graph.edgeRelIds[i];
    const dst = graph.edgeDstIds[i];
    if (src < 0 || src >= nodeCount || dst < 0 || dst >= nodeCount || rel < 0 || rel >= relationCount) {
      throw new Error(
        `CondensedGraphBinary: edge ${i} out of range (src ${src}, rel ${rel}, dst ${dst}; nodes ${nodeCount}, relations ${relationCount})`
      );
    }
  }

  const valueCount = reader.readUint32();
  if (valueCount !== edgeCount) {
    throw new Error(
      `CondensedGraphBinary: value count mismatch (values ${valueCount}, edges ${edgeCount})`
    );
  }
  reader.align(8);
  const valueBytes = reader.readBytes(valueCount * 8);
  graph.values = new Float64Array(valueBytes.buffer, valueBytes.byteOffset, valueCount);

  const degreeCount = reader.readUint32();
  if (degreeCount !== nodeCount) {
    throw new Error(
      `CondensedGraphBinary: degree count mismatch (degrees ${degreeCount}, nodes ${nodeCount})`
    );
  }
  reader.align(2);
  const degreeBytes = reader.readBytes(degreeCount * 2);
  graph.nodeDegrees = new Uint16Array(degreeBytes.buffer, degreeBytes.byteOffset, degreeCount);

  graph.nodeHeads = null;

  graph._dropAdjacencyList = dropAdjacency;
  graph._mphEnabled = false;
  graph._mph = null;

  if (hasWaveletByRel) {
    reader.align(4);
    const waveletCount = reader.readUint32();
    graph._waveletByRelId = new Array(waveletCount);
    for (let i = 0; i < waveletCount; i++) {
      const present = reader.readUint8() === 1;
      graph._waveletByRelId[i] = present ? readWaveletAdjacency(reader) : null;
    }
  }

  if (hasPerfectHash) {
    reader.align(4);
    const seed1 = reader.readUint32();
    const seed2 = reader.readUint32();
    const bucketCount = reader.readUint32();
    const seedsLength = reader.readUint32();
    const seedsByteLength = seedsLength * 4;
    reader.align(4);
    const seedsBytes = reader.readBytes(seedsByteLength);
    const seeds = new Int32Array(seedsBytes.buffer, seedsBytes.byteOffset, seedsLength);

    const tableLength = reader.readUint32();
    const tableByteLength = tableLength * 4;
    reader.align(4);
    const tableBytes = reader.readBytes(tableByteLength);
    const table = new Int32Array(tableBytes.buffer, tableBytes.byteOffset, tableLength);

    const valuesLength = reader.readUint32();
    const valuesByteLength = valuesLength * 4;
    reader.align(4);
    const valuesBytes = reader.readBytes(valuesByteLength);
    const values = new Int32Array(valuesBytes.buffer, valuesBytes.byteOffset, valuesLength);

    const keyIndicesLength = reader.readUint32();
    const keyByteLength = keyIndicesLength * 4;
    reader.align(4);
    const keyBytes = reader.readBytes(keyByteLength);
    const keyIndices = new Int32Array(keyBytes.buffer, keyBytes.byteOffset, keyIndicesLength);

    graph._mph = MinimalPerfectHash.fromSerialized({
      seed1,
      seed2,
      bucketCount,
      seeds,
      table,
      values,
      keyIndices
    }, graph.nodeIds);
    graph._mphEnabled = true;
  }

  return graph;
}

export { serializeCondensedGraph, deserializeCondensedGraph, BinaryWriter, BinaryReader };
