import { BinaryWriter, BinaryReader } from './graph/CondensedGraphBinary.js';
import { CondensedGraph } from './CondensedGraph.js';

const MAGIC = 0x41524231; // ARB1

const CONFIG_STRIP_KEYS = new Set(['_compiled', '_compileErrors', '_compileWarnings']);
const COMPILED_STRIP_KEYS = new Set(['source']);

function toSnapshotSafe(value, stripKeys) {
  const seen = new WeakSet();
  return JSON.parse(JSON.stringify(value, (key, val) => {
    if (stripKeys && stripKeys.has(key)) return undefined;
    if (typeof val === 'function') return undefined;
    if (!val || typeof val !== 'object') return val;
    if (seen.has(val)) return undefined;
    seen.add(val);
    return val;
  }));
}

// Format version 2: per-relation metadata (validity, decayConfig) and the
// per-relation TTL settings are carried in the JSON payload. Version 1
// buffers are rejected with a clean error, never misparsed.
const SNAPSHOT_VERSION = 2;

function serializeArbiterSnapshot(arbiter, options = {}) {
  const writer = new BinaryWriter();
  writer.writeUint32(MAGIC);
  writer.writeUint32(SNAPSHOT_VERSION);

  const graph = options.graph || arbiter.snapshotGraph;
  if (!graph) {
    throw new Error('Snapshot graph is not available');
  }

  const graphBinary = graph.toBinary();
  writer.writeUint32(graphBinary.byteLength);
  writer.writeBytes(new Uint8Array(graphBinary));

  const relationEntries = [];
  for (const [name, config] of arbiter.relationConfigs.entries()) {
    const configPayload = config ? toSnapshotSafe(config, CONFIG_STRIP_KEYS) : null;
    const compiledPayload = config?._compiled ? toSnapshotSafe(config._compiled, COMPILED_STRIP_KEYS) : null;
    relationEntries.push({
      name,
      config: configPayload,
      compiled: compiledPayload
    });
  }

  const nodeEntries = [];
  for (const node of arbiter.nodes.values()) {
    if (!node) continue;
    nodeEntries.push({
      id: node.id,
      key: node.key,
      type: node.type || null,
      data: node.data ? toSnapshotSafe(node.data) : null
    });
  }

  // Per-relation metadata that the condensed graph edge channel does not
  // carry: validity labels and decay configs must survive persistence, or
  // a labeled relation silently downgrades to heuristic after a save/load
  // cycle. Keyed by the graph's numeric ids (stable within the snapshot).
  const relationMetadata = {};
  for (const rel of arbiter.relations || []) {
    if (!rel || rel.src === undefined || rel.dst === undefined || !rel.rel) continue;
    const key = `${rel.src}|${rel.rel}|${rel.dst}`;
    if (rel.validity !== undefined || rel.decayConfig !== undefined) {
      relationMetadata[key] = {
        ...(rel.validity !== undefined ? { validity: rel.validity } : {}),
        ...(rel.decayConfig !== undefined ? { decayConfig: rel.decayConfig } : {})
      };
    }
  }

  // Per-relation TTL settings are runtime value-manager state; without them
  // a restored arbiter silently applies the default TTL to every relation.
  const valueTtls = {};
  if (arbiter.valueManager && arbiter.valueManager.ttlConfigs) {
    for (const [relation, ttl] of arbiter.valueManager.ttlConfigs.entries()) {
      valueTtls[relation] = ttl;
    }
  }

  const snapshot = {
    relations: relationEntries,
    relationIdSnapshot: arbiter.keyManager.getRelationIdSnapshot(),
    nodes: nodeEntries,
    relationMetadata,
    valueTtls
  };

  const json = JSON.stringify(snapshot);
  const encoded = new TextEncoder().encode(json);
  writer.writeUint32(encoded.byteLength);
  writer.writeBytes(encoded);
  return writer.toArrayBuffer();
}

function deserializeArbiterSnapshot(buffer, arbiterFactory) {
  const reader = new BinaryReader(buffer);
  const magic = reader.readUint32();
  if (magic !== MAGIC) throw new Error('Invalid arbiter snapshot');
  const version = reader.readUint32();
  if (version !== SNAPSHOT_VERSION) throw new Error(`Unsupported arbiter snapshot version ${version}`);

  const graphLength = reader.readUint32();
  const graphBytes = reader.readBytes(graphLength);
  const graphCopy = new Uint8Array(graphLength);
  graphCopy.set(graphBytes);
  const graph = CondensedGraph.fromBinary(graphCopy.buffer);

  const jsonLength = reader.readUint32();
  const jsonBytes = reader.readBytes(jsonLength);
  const json = new TextDecoder().decode(jsonBytes);
  const payload = JSON.parse(json);
  if (payload === null || typeof payload !== 'object' || Array.isArray(payload)) {
    throw new Error('Invalid arbiter snapshot payload: expected a JSON object');
  }

  const arbiter = arbiterFactory();
  arbiter.snapshotEnabled = true;
  arbiter.snapshotGraph = graph;
  arbiter._snapshotReadOnly = true;
  arbiter.relationConfigs.clear();

  arbiter.nodes.clear();
  arbiter.nodeIdByKey.clear();
  arbiter.keyByNodeId.clear();
  let maxNodeId = -1;
  for (let nodeId = 0; nodeId < graph.nodeIds.length; nodeId++) {
    const key = graph.getNodeKey(nodeId);
    if (!key) continue;
    arbiter.nodeIdByKey.set(key, nodeId);
    arbiter.keyByNodeId.set(nodeId, key);
    // Key nodes by BOTH key and nodeId to match live engine (NodeManager uses nodeId)
    arbiter.nodes.set(nodeId, { id: nodeId, key, type: null, data: null });
    if (nodeId > maxNodeId) maxNodeId = nodeId;
  }
  if (maxNodeId >= 0) {
    arbiter.nextNodeId = maxNodeId + 1;
  }

  if (payload.relationIdSnapshot) {
    arbiter.keyManager.loadRelationIdSnapshot(payload.relationIdSnapshot);
  }

  if (Array.isArray(payload.relations)) {
    for (const entry of payload.relations) {
      if (!entry || !entry.name) continue;
      const config = entry.config || null;
      if (config && entry.compiled) {
        config._compiled = entry.compiled;
      }
      arbiter.relationConfigs.set(entry.name, config);
    }
  }

  if (Array.isArray(payload.nodes)) {
    for (const entry of payload.nodes) {
      if (!entry || entry.key === undefined || entry.key === null) continue;
      const nodeId = arbiter.nodeIdByKey.get(entry.key);
      if (nodeId === undefined) continue;
      const node = arbiter.nodes.get(nodeId) || { id: nodeId, key: entry.key };
      node.type = entry.type || null;
      node.data = entry.data === undefined ? null : entry.data;
      arbiter.nodes.set(nodeId, node);
    }
  }

  // Restore the metadata the edge channel cannot carry (consulted by the
  // snapshot access layer when it materializes relation objects).
  arbiter._snapshotRelationMetadata = (payload.relationMetadata && typeof payload.relationMetadata === 'object')
    ? payload.relationMetadata
    : {};

  if (payload.valueTtls && typeof payload.valueTtls === 'object' && arbiter.valueManager) {
    for (const [relation, ttl] of Object.entries(payload.valueTtls)) {
      arbiter.valueManager.setTTL(relation, ttl);
    }
  }

  return arbiter;
}

export { serializeArbiterSnapshot, deserializeArbiterSnapshot };
