/**
 * Verify helpers for MaterializedViewService.
 *
 * Provides deterministic sampling and adjacency-based ground-truth
 * comparison to confirm that a LogicalIndex is consistent with WarpState.
 *
 * @module domain/services/MaterializedViewVerifier
 */
import { decodeEdgeKey } from "./KeyCodec.js";
// ── PRNG ──────────────────────────────────────────────────────────────────────
/**
 * Mulberry32 PRNG — deterministic 32-bit generator from a seed.
 *
 * mulberry32 is a fast 32-bit PRNG by Tommy Ettinger. The magic constants
 * (0x6D2B79F5, shifts 15/13/16) are part of the published algorithm.
 * See: https://gist.github.com/tommyettinger/46a874533244883189143505d203312c
 */
function mulberry32(seed) {
    let t = (seed | 0) + 0x6D2B79F5;
    return () => {
        t = Math.imul(t ^ (t >>> 15), t | 1);
        t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
        return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
    };
}
/**
 * Selects a deterministic sample of nodes using a seeded PRNG.
 *
 * @param allNodes - All candidate node IDs.
 * @param sampleRate - Fraction of nodes to select (>0 and <=1).
 * @param seed - PRNG seed for determinism.
 */
function sampleNodes(allNodes, sampleRate, seed) {
    if (sampleRate >= 1) {
        return allNodes;
    }
    if (sampleRate <= 0 || allNodes.length === 0) {
        return [];
    }
    const rng = mulberry32(seed);
    const sampled = allNodes.filter(() => rng() < sampleRate);
    // When the initial sample is empty (e.g., graph has fewer nodes than
    // sample size), fall back to using all available nodes.
    if (sampled.length === 0) {
        sampled.push(allNodes[Math.floor(rng() * allNodes.length)]);
    }
    return sampled;
}
// ── Ground-truth adjacency ────────────────────────────────────────────────────
function pushAdjacencyEntry(map, key, entry) {
    let list = map.get(key);
    if (!list) {
        list = [];
        map.set(key, list);
    }
    list.push(entry);
}
/**
 * Builds adjacency maps from state for ground-truth verification.
 */
function buildGroundTruthAdjacency(state) {
    const outgoing = new Map();
    const incoming = new Map();
    for (const edgeKey of state.edgeAlive.elements()) {
        const { from, to, label } = decodeEdgeKey(edgeKey);
        if (!state.nodeAlive.contains(from) || !state.nodeAlive.contains(to)) {
            continue;
        }
        pushAdjacencyEntry(outgoing, from, { neighborId: to, label });
        pushAdjacencyEntry(incoming, to, { neighborId: from, label });
    }
    return { outgoing, incoming };
}
// ── Comparison helpers ────────────────────────────────────────────────────────
/**
 * Canonicalizes neighbor edges into deterministic, label-aware signatures.
 */
function canonicalizeNeighborSignatures(edges) {
    const byNeighbor = new Map();
    for (const { neighborId, label } of edges) {
        let labels = byNeighbor.get(neighborId);
        if (!labels) {
            labels = [];
            byNeighbor.set(neighborId, labels);
        }
        labels.push(label);
    }
    const signatures = [];
    for (const [neighborId, labels] of byNeighbor) {
        signatures.push(JSON.stringify([neighborId, labels.slice().sort()])); // nosemgrep: ts-no-json-stringify-in-core -- 0025B
    }
    signatures.sort();
    return signatures;
}
/**
 * Compares bitmap index neighbors against ground-truth adjacency for one node.
 */
function compareNodeDirection(params) {
    const { nodeId, direction, logicalIndex, truthMap } = params;
    const bitmapEdges = logicalIndex.getEdges(nodeId, direction);
    const actual = canonicalizeNeighborSignatures(bitmapEdges);
    const expected = canonicalizeNeighborSignatures(truthMap.get(nodeId) ?? []);
    if (actual.length !== expected.length) {
        return { nodeId, direction, expected, actual };
    }
    for (let i = 0; i < actual.length; i++) {
        if (actual[i] !== expected[i]) {
            return { nodeId, direction, expected, actual };
        }
    }
    return null;
}
function verifyOneNode(nodeId, logicalIndex, ctx) {
    if (!logicalIndex.isAlive(nodeId)) {
        ctx.acc.errors.push({ nodeId, direction: 'alive', expected: ['true'], actual: ['false'] });
        return;
    }
    for (const direction of ['out', 'in']) {
        const truthMap = direction === 'out' ? ctx.truth.outgoing : ctx.truth.incoming;
        const err = compareNodeDirection({ nodeId, direction, logicalIndex, truthMap });
        if (err) {
            ctx.acc.errors.push(err);
        }
        else {
            ctx.acc.passed++;
        }
    }
}
function verifySampledNodes(sampled, logicalIndex, truth) {
    const acc = { passed: 0, errors: [] };
    const ctx = { truth, acc };
    for (const nodeId of sampled) {
        verifyOneNode(nodeId, logicalIndex, ctx);
    }
    return acc;
}
// ── Crypto-grade seed ─────────────────────────────────────────────────────
/** Generates a 31-bit unsigned seed from crypto-grade randomness. */
function cryptoSeed() {
    const buf = new Uint8Array(4);
    globalThis.crypto.getRandomValues(buf);
    return ((buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3]) >>> 1;
}
/**
 * Verifies index integrity by sampling alive nodes and comparing
 * bitmap neighbor queries against adjacency-based ground truth.
 */
export function verifyIndex({ state, logicalIndex, options = {} }) {
    const seed = options.seed ?? cryptoSeed();
    const sampleRate = options.sampleRate ?? 0.1;
    const allNodes = [...state.nodeAlive.elements()].sort();
    const sampled = sampleNodes(allNodes, sampleRate, seed);
    const truth = buildGroundTruthAdjacency(state);
    const { passed, errors } = verifySampledNodes(sampled, logicalIndex, truth);
    return { passed, failed: errors.length, errors, seed };
}
