/**
 * Stage: insertNodes
 *
 * Batch-inserts file nodes, definitions, exports, children, and contains/parameter_of edges.
 * Updates file hashes for incremental builds.
 *
 * When the native engine is available, delegates all SQLite writes to Rust via
 * `bulkInsertNodes` — eliminating JS↔C boundary overhead. Falls back to the
 * JS implementation on failure or when native is unavailable.
 */
import path from 'node:path';
import { performance } from 'node:perf_hooks';
import { bulkNodeIdsByFile } from '../../../../db/index.js';
import { debug } from '../../../../infrastructure/logger.js';
import { normalizePath } from '../../../../shared/constants.js';
import { toErrorMessage } from '../../../../shared/errors.js';
import { batchInsertEdges, batchInsertNodes, fileHash, fileStat, readFileSafe, } from '../helpers.js';
/** Marshal allSymbols into the batch format expected by native bulkInsertNodes. */
function marshalSymbolBatches(allSymbols) {
    const batches = [];
    for (const [relPath, symbols] of allSymbols) {
        batches.push({
            file: relPath,
            definitions: symbols.definitions.map((def) => ({
                name: def.name,
                kind: def.kind,
                line: def.line,
                endLine: def.endLine ?? undefined,
                visibility: def.visibility ?? undefined,
                children: (def.children ?? []).map((c) => ({
                    name: c.name,
                    kind: c.kind,
                    line: c.line,
                    endLine: c.endLine ?? undefined,
                    visibility: c.visibility ?? undefined,
                })),
            })),
            exports: symbols.exports.map((exp) => ({
                name: exp.name,
                kind: exp.kind,
                line: exp.line,
            })),
        });
    }
    return batches;
}
/** Resolve the (hash, mtime, size) tuple for a relPath, reading from disk if needed. */
function resolveHashFromPrecomputed(relPath, precomputed, rootDir, caller) {
    if (precomputed.hash) {
        let mtime;
        let size;
        if (precomputed.stat) {
            mtime = precomputed.stat.mtime;
            size = precomputed.stat.size;
        }
        else {
            const rawStat = fileStat(path.join(rootDir, relPath));
            mtime = rawStat ? rawStat.mtime : 0;
            size = rawStat ? rawStat.size : 0;
        }
        return { file: relPath, hash: precomputed.hash, mtime, size };
    }
    const absPath = path.join(rootDir, relPath);
    let code;
    try {
        code = readFileSafe(absPath);
    }
    catch (e) {
        debug(`${caller}: readFileSafe failed for ${relPath}: ${toErrorMessage(e)}`);
        code = null;
    }
    if (code === null)
        return null;
    const stat = fileStat(absPath);
    return {
        file: relPath,
        hash: fileHash(code),
        mtime: stat ? stat.mtime : 0,
        size: stat ? stat.size : 0,
    };
}
/**
 * Walk every collected file once and yield a `FileHashRecord` for it, plus one
 * record per metadata-only update.  Shared by `buildFileHashes` (native path)
 * and `updateFileHashes` (JS fallback) so the iteration and hash-resolution
 * logic stays in one place.
 *
 * Files marked `_reverseDepOnly` are skipped — their hashes are already
 * correct in the DB.
 */
function* iterFileHashRecords(filesToParse, precomputedData, metadataUpdates, rootDir, caller) {
    const seen = new Set();
    for (const item of filesToParse) {
        const relPath = item.relPath ?? normalizePath(path.relative(rootDir, item.file));
        if (seen.has(relPath))
            continue;
        seen.add(relPath);
        const precomputed = precomputedData.get(relPath);
        if (precomputed?._reverseDepOnly)
            continue;
        const record = resolveHashFromPrecomputed(relPath, precomputed ?? {}, rootDir, caller);
        if (record)
            yield record;
    }
    // Metadata-only updates (self-heal mtime/size without re-parse)
    for (const item of metadataUpdates) {
        yield {
            file: item.relPath,
            hash: item.hash,
            mtime: item.stat ? item.stat.mtime : 0,
            size: item.stat ? item.stat.size : 0,
        };
    }
}
/**
 * Build file hash entries for every collected file, including those that
 * produced zero symbols (empty files, parsers that silently no-op'd, or
 * optional-language extensions whose grammar wasn't installed). Iterating the
 * symbol map instead would skip such files and leave them missing from
 * `file_hashes`, which permanently breaks the JS-side fast-skip pre-flight on
 * any subsequent no-op rebuild (#1068).
 *
 * Exported for unit testing.
 */
export function buildFileHashes(filesToParse, precomputedData, metadataUpdates, rootDir) {
    return [
        ...iterFileHashRecords(filesToParse, precomputedData, metadataUpdates, rootDir, 'buildFileHashes'),
    ];
}
// ── Native fast-path ─────────────────────────────────────────────────
function tryNativeInsert(ctx) {
    if (!ctx.nativeDb?.bulkInsertNodes)
        return false;
    const { allSymbols, filesToParse, metadataUpdates, rootDir, removed } = ctx;
    const batches = marshalSymbolBatches(allSymbols);
    const precomputedData = new Map();
    for (const item of filesToParse) {
        if (item.relPath)
            precomputedData.set(item.relPath, item);
    }
    const fileHashes = buildFileHashes(filesToParse, precomputedData, metadataUpdates, rootDir);
    // In native-first mode (single rusqlite connection), no WAL dance is needed.
    // In dual-connection mode, checkpoint JS side before native write, then
    // checkpoint native side after (#696, #709, #715, #717).
    let result;
    if (ctx.nativeFirstProxy) {
        result = ctx.nativeDb.bulkInsertNodes(batches, fileHashes, removed);
    }
    else {
        try {
            if (ctx.db) {
                ctx.db.pragma('wal_checkpoint(TRUNCATE)');
            }
            result = ctx.nativeDb.bulkInsertNodes(batches, fileHashes, removed);
        }
        finally {
            try {
                ctx.nativeDb?.exec('PRAGMA wal_checkpoint(TRUNCATE)');
            }
            catch (e) {
                debug(`tryNativeInsert: WAL checkpoint failed (nativeDb may already be closed): ${toErrorMessage(e)}`);
            }
        }
    }
    return result;
}
// ── JS fallback: Phase 1 ────────────────────────────────────────────
function insertDefinitionsAndExports(db, allSymbols) {
    const phase1Rows = [];
    const exportKeys = [];
    for (const [relPath, symbols] of allSymbols) {
        phase1Rows.push([relPath, 'file', relPath, 0, null, null, null, null, null]);
        for (const def of symbols.definitions) {
            const dotIdx = def.name.lastIndexOf('.');
            const scope = dotIdx !== -1 ? def.name.slice(0, dotIdx) : null;
            phase1Rows.push([
                def.name,
                def.kind,
                relPath,
                def.line,
                def.endLine || null,
                null,
                def.name,
                scope,
                def.visibility || null,
            ]);
        }
        for (const exp of symbols.exports) {
            phase1Rows.push([exp.name, exp.kind, relPath, exp.line, null, null, exp.name, null, null]);
            exportKeys.push([exp.name, exp.kind, relPath, exp.line]);
        }
    }
    batchInsertNodes(db, phase1Rows);
    // Mark exported symbols in batches (cache prepared statements by chunk size)
    if (exportKeys.length > 0) {
        const EXPORT_CHUNK = 500;
        const exportStmtCache = new Map();
        for (let i = 0; i < exportKeys.length; i += EXPORT_CHUNK) {
            const end = Math.min(i + EXPORT_CHUNK, exportKeys.length);
            const chunkSize = end - i;
            let updateStmt = exportStmtCache.get(chunkSize);
            if (!updateStmt) {
                const conditions = Array.from({ length: chunkSize }, () => '(name = ? AND kind = ? AND file = ? AND line = ?)').join(' OR ');
                updateStmt = db.prepare(`UPDATE nodes SET exported = 1 WHERE ${conditions}`);
                exportStmtCache.set(chunkSize, updateStmt);
            }
            const vals = [];
            for (let j = i; j < end; j++) {
                const k = exportKeys[j];
                vals.push(k[0], k[1], k[2], k[3]);
            }
            updateStmt.run(...vals);
        }
    }
}
// ── JS fallback: Phase 2+3 ──────────────────────────────────────────
/** Build the in-memory `name|kind|line` → node-id map for a single file. */
function loadFileNodeIdMap(db, relPath) {
    const map = new Map();
    for (const row of bulkNodeIdsByFile(db, relPath)) {
        map.set(`${row.name}|${row.kind}|${row.line}`, row.id);
    }
    return map;
}
/**
 * First pass: for every file, emit file→def containment edges and collect
 * the child-node insertion rows.
 */
function collectChildRowsAndFileEdges(db, allSymbols, childRows, edgeRows) {
    for (const [relPath, symbols] of allSymbols) {
        const nodeIdMap = loadFileNodeIdMap(db, relPath);
        const fileId = nodeIdMap.get(`${relPath}|file|0`);
        for (const def of symbols.definitions) {
            const defId = nodeIdMap.get(`${def.name}|${def.kind}|${def.line}`);
            if (fileId && defId) {
                edgeRows.push([fileId, defId, 'contains', 1.0, 0]);
            }
            if (!def.children?.length || !defId)
                continue;
            for (const child of def.children) {
                childRows.push([
                    child.name,
                    child.kind,
                    relPath,
                    child.line,
                    child.endLine || null,
                    defId,
                    `${def.name}.${child.name}`,
                    def.name,
                    child.visibility || null,
                ]);
            }
        }
    }
}
/**
 * Second pass (after child nodes have been inserted): emit def→child
 * containment edges and child→def `parameter_of` edges.
 */
function collectChildEdges(db, allSymbols, edgeRows) {
    for (const [relPath, symbols] of allSymbols) {
        const nodeIdMap = loadFileNodeIdMap(db, relPath);
        for (const def of symbols.definitions) {
            if (!def.children?.length)
                continue;
            const defId = nodeIdMap.get(`${def.name}|${def.kind}|${def.line}`);
            if (!defId)
                continue;
            for (const child of def.children) {
                const childId = nodeIdMap.get(`${child.name}|${child.kind}|${child.line}`);
                if (!childId)
                    continue;
                edgeRows.push([defId, childId, 'contains', 1.0, 0]);
                if (child.kind === 'parameter') {
                    edgeRows.push([childId, defId, 'parameter_of', 1.0, 0]);
                }
            }
        }
    }
}
function insertChildrenAndEdges(db, allSymbols) {
    const childRows = [];
    const edgeRows = [];
    collectChildRowsAndFileEdges(db, allSymbols, childRows, edgeRows);
    // Insert children first (so they exist for edge lookup)
    batchInsertNodes(db, childRows);
    collectChildEdges(db, allSymbols, edgeRows);
    batchInsertEdges(db, edgeRows);
}
// ── JS fallback: Phase 4 ────────────────────────────────────────────
function updateFileHashes(_db, filesToParse, precomputedData, metadataUpdates, rootDir, upsertHash) {
    if (!upsertHash)
        return;
    // Iterate every collected file (#1068): files that produced zero symbols
    // (empty, parser no-op, or grammar-missing optional language) still need a
    // hash row, otherwise the next no-op rebuild's fast-skip pre-flight rejects.
    for (const record of iterFileHashRecords(filesToParse, precomputedData, metadataUpdates, rootDir, 'updateFileHashes')) {
        upsertHash.run(record.file, record.hash, record.mtime, record.size);
    }
}
// ── Main entry point ────────────────────────────────────────────────
export async function insertNodes(ctx) {
    const { allSymbols, filesToParse, metadataUpdates, rootDir, removed } = ctx;
    // Populate fileSymbols before any DB writes (used by later stages)
    for (const [relPath, symbols] of allSymbols) {
        ctx.fileSymbols.set(relPath, symbols);
    }
    const t0 = performance.now();
    // Try native Rust path first — single transaction, no JS↔C overhead
    if (ctx.engineName === 'native') {
        try {
            if (tryNativeInsert(ctx)) {
                ctx.timing.insertMs = performance.now() - t0;
                // Removed-file hash cleanup is handled inside the native call
                return;
            }
        }
        catch (e) {
            debug(`insertNodes: native insert failed, falling back to JS: ${toErrorMessage(e)}`);
        }
    }
    // JS fallback
    const precomputedData = new Map();
    for (const item of filesToParse) {
        if (item.relPath)
            precomputedData.set(item.relPath, item);
    }
    let upsertHash;
    try {
        upsertHash = ctx.db.prepare('INSERT OR REPLACE INTO file_hashes (file, hash, mtime, size) VALUES (?, ?, ?, ?)');
    }
    catch (e) {
        debug(`insertNodes: file_hashes prepare failed (table may not exist): ${toErrorMessage(e)}`);
        upsertHash = null;
    }
    const insertAll = ctx.db.transaction(() => {
        insertDefinitionsAndExports(ctx.db, allSymbols);
        insertChildrenAndEdges(ctx.db, allSymbols);
        updateFileHashes(ctx.db, filesToParse, precomputedData, metadataUpdates, rootDir, upsertHash);
    });
    insertAll();
    ctx.timing.insertMs = performance.now() - t0;
    // Clean up removed file hashes
    if (upsertHash && removed.length > 0) {
        const deleteHash = ctx.db.prepare('DELETE FROM file_hashes WHERE file = ?');
        for (const relPath of removed) {
            deleteHash.run(relPath);
        }
    }
}
//# sourceMappingURL=insert-nodes.js.map