/**
 * Stage: finalize
 *
 * WASM cleanup, stats logging, drift detection, build metadata, registry, journal.
 */
import fs from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { performance } from 'node:perf_hooks';
import { closeDbPair, closeDbPairDeferred, getBuildMeta, setBuildMeta, } from '../../../../db/index.js';
import { debug, info, warn } from '../../../../infrastructure/logger.js';
import { CODEGRAPH_VERSION } from '../../../../shared/version.js';
import { writeJournalHeader } from '../../journal.js';
/** Release cached WASM parse trees to free memory. */
function releaseWasmTrees(allSymbols) {
    for (const [, symbols] of allSymbols) {
        const tree = symbols._tree;
        if (tree && typeof tree.delete === 'function') {
            try {
                tree.delete();
            }
            catch {
                /* ignore cleanup errors */
            }
        }
        symbols._tree = undefined;
        symbols._langId = undefined;
    }
}
/**
 * Detect significant drift between current and previous node/edge counts.
 * Skipped for small incremental changes where count fluctuation is expected.
 */
function detectIncrementalDrift(ctx, nodeCount, actualEdgeCount) {
    const { db, allSymbols, config } = ctx;
    const useNativeDb = ctx.engineName === 'native' && !!ctx.nativeDb;
    if (ctx.isFullBuild || allSymbols.size <= 3)
        return;
    const prevNodes = useNativeDb
        ? ctx.nativeDb.getBuildMeta('node_count')
        : getBuildMeta(db, 'node_count');
    const prevEdges = useNativeDb
        ? ctx.nativeDb.getBuildMeta('edge_count')
        : getBuildMeta(db, 'edge_count');
    if (!prevNodes || !prevEdges)
        return;
    const prevN = Number(prevNodes);
    const prevE = Number(prevEdges);
    if (prevN <= 0)
        return;
    const nodeDrift = Math.abs(nodeCount - prevN) / prevN;
    const edgeDrift = prevE > 0 ? Math.abs(actualEdgeCount - prevE) / prevE : 0;
    const driftThreshold = config.build?.driftThreshold ?? 0.2;
    if (nodeDrift > driftThreshold || edgeDrift > driftThreshold) {
        warn(`Incremental build diverged significantly from previous counts (nodes: ${prevN}\u2192${nodeCount} [${(nodeDrift * 100).toFixed(1)}%], edges: ${prevE}\u2192${actualEdgeCount} [${(edgeDrift * 100).toFixed(1)}%], threshold: ${(driftThreshold * 100).toFixed(0)}%). Consider rebuilding with --no-incremental.`);
    }
}
/**
 * Persist build metadata (engine, version, counts, timestamp).
 * Skipped for small incremental builds to avoid WAL fsync cost.
 */
function persistBuildMetadata(ctx, nodeCount, actualEdgeCount, buildNow) {
    const useNativeDb = ctx.engineName === 'native' && !!ctx.nativeDb;
    if (!ctx.isFullBuild && ctx.allSymbols.size <= 3)
        return;
    // When the native engine is active, persist the Rust addon version so that
    // checkEngineSchemaMismatch compares against the same value on the next build.
    // Writing CODEGRAPH_VERSION (the npm package version) here would create a
    // permanent mismatch whenever npm and crate versions diverge, forcing every
    // subsequent build to be a full rebuild.
    const codeVersionToWrite = ctx.engineName === 'native' && ctx.engineVersion ? ctx.engineVersion : CODEGRAPH_VERSION;
    // Persist the repo root so downstream commands (e.g. `codegraph embed`)
    // can resolve relative file paths regardless of the invoking cwd.
    // Use realpathSync (symlink-resolving) to match the Rust engine's
    // std::fs::canonicalize — otherwise the JS write here would overwrite the
    // canonical path Rust wrote for native full builds and could re-introduce
    // a non-canonical path when the project root is behind a symlink.
    const resolvedRootDir = path.resolve(ctx.rootDir);
    let rootDirToWrite = resolvedRootDir;
    try {
        rootDirToWrite = fs.realpathSync(resolvedRootDir);
    }
    catch {
        /* realpath can fail (e.g. path no longer exists); fall back to resolve() */
    }
    try {
        if (useNativeDb) {
            ctx.nativeDb.setBuildMeta(Object.entries({
                engine: ctx.engineName,
                engine_version: codeVersionToWrite,
                codegraph_version: codeVersionToWrite,
                schema_version: String(ctx.schemaVersion),
                built_at: buildNow.toISOString(),
                node_count: String(nodeCount),
                edge_count: String(actualEdgeCount),
                root_dir: rootDirToWrite,
            }).map(([key, value]) => ({ key, value: String(value) })));
        }
        else {
            setBuildMeta(ctx.db, {
                engine: ctx.engineName,
                engine_version: codeVersionToWrite,
                codegraph_version: codeVersionToWrite,
                schema_version: String(ctx.schemaVersion),
                built_at: buildNow.toISOString(),
                node_count: nodeCount,
                edge_count: actualEdgeCount,
                root_dir: rootDirToWrite,
            });
        }
    }
    catch (err) {
        warn(`Failed to write build metadata: ${err.message}`);
    }
}
/**
 * Run advisory checks on full builds: orphaned embeddings, stale embeddings,
 * and unused exports. Informational only — does not affect correctness.
 */
function runAdvisoryChecks(ctx, hasEmbeddings, buildNow) {
    // Batched native path: single napi call for all 3 advisory checks
    if (ctx.engineName === 'native' && ctx.nativeDb?.runAdvisoryChecks) {
        const result = ctx.nativeDb.runAdvisoryChecks(hasEmbeddings);
        if (result.orphanedEmbeddings > 0) {
            warn(`${result.orphanedEmbeddings} embeddings are orphaned (nodes changed). Run "codegraph embed" to refresh.`);
        }
        if (result.embedBuiltAt) {
            const embedTime = new Date(result.embedBuiltAt).getTime();
            if (!Number.isNaN(embedTime) && embedTime < buildNow.getTime()) {
                warn('Embeddings were built before the last graph rebuild. Run "codegraph embed" to update.');
            }
        }
        if (result.unusedExports > 0) {
            warn(`${result.unusedExports} exported symbol${result.unusedExports > 1 ? 's have' : ' has'} zero cross-file consumers. Run "codegraph exports <file> --unused" to inspect.`);
        }
        return;
    }
    const { db } = ctx;
    // Orphaned embeddings warning
    if (hasEmbeddings) {
        try {
            const orphaned = db
                .prepare('SELECT COUNT(*) as c FROM embeddings WHERE node_id NOT IN (SELECT id FROM nodes)')
                .get().c;
            if (orphaned > 0) {
                warn(`${orphaned} embeddings are orphaned (nodes changed). Run "codegraph embed" to refresh.`);
            }
        }
        catch {
            /* ignore - embeddings table may have been dropped */
        }
    }
    // Stale embeddings warning (built before current graph rebuild)
    if (hasEmbeddings) {
        try {
            const embedBuiltAt = db.prepare("SELECT value FROM embedding_meta WHERE key = 'built_at'").get()?.value;
            if (embedBuiltAt) {
                const embedTime = new Date(embedBuiltAt).getTime();
                if (!Number.isNaN(embedTime) && embedTime < buildNow.getTime()) {
                    warn('Embeddings were built before the last graph rebuild. Run "codegraph embed" to update.');
                }
            }
        }
        catch {
            /* ignore - embedding_meta table may not exist */
        }
    }
    // Unused exports warning
    try {
        const unusedCount = db
            .prepare(`SELECT COUNT(*) as c FROM nodes
         WHERE exported = 1 AND kind != 'file'
           AND id NOT IN (
             SELECT DISTINCT e.target_id FROM edges e
             JOIN nodes caller ON e.source_id = caller.id
             JOIN nodes target ON e.target_id = target.id
             WHERE e.kind = 'calls' AND caller.file != target.file
           )`)
            .get().c;
        if (unusedCount > 0) {
            warn(`${unusedCount} exported symbol${unusedCount > 1 ? 's have' : ' has'} zero cross-file consumers. Run "codegraph exports <file> --unused" to inspect.`);
        }
    }
    catch {
        /* exported column may not exist on older DBs */
    }
}
export async function finalize(ctx) {
    const { allSymbols, rootDir, isFullBuild, hasEmbeddings, opts } = ctx;
    const t0 = performance.now();
    releaseWasmTrees(allSymbols);
    // Capture a single wall-clock timestamp for the current build — used for
    // both the stale-embeddings comparison and the persisted built_at metadata.
    const buildNow = new Date();
    const useNative = ctx.engineName === 'native' && !!ctx.nativeDb?.getFinalizeCounts;
    let nodeCount;
    let actualEdgeCount;
    if (useNative) {
        const counts = ctx.nativeDb.getFinalizeCounts();
        nodeCount = counts.nodeCount;
        actualEdgeCount = counts.edgeCount;
    }
    else {
        nodeCount = ctx.db.prepare('SELECT COUNT(*) as c FROM nodes').get().c;
        actualEdgeCount = ctx.db.prepare('SELECT COUNT(*) as c FROM edges').get().c;
    }
    info(`Graph built: ${nodeCount} nodes, ${actualEdgeCount} edges`);
    info(`Stored in ${ctx.dbPath}`);
    detectIncrementalDrift(ctx, nodeCount, actualEdgeCount);
    persistBuildMetadata(ctx, nodeCount, actualEdgeCount, buildNow);
    // Skip expensive advisory queries for incremental builds — these are
    // informational warnings that don't affect correctness and cost ~40-60ms.
    if (!isFullBuild) {
        debug('Finalize: skipping advisory queries (orphaned/stale embeddings, unused exports) for incremental build');
    }
    else {
        runAdvisoryChecks(ctx, hasEmbeddings, buildNow);
    }
    // Intentionally measured before closeDb / writeJournalHeader / auto-registration:
    // for the deferred-close path the close is async (setImmediate), and for full
    // builds the metric captures finalize logic only — DB close cost is tracked
    // separately via timing.closeDbMs when available.
    ctx.timing.finalizeMs = performance.now() - t0;
    // Close NativeDatabase (fast, ~1ms) then better-sqlite3 (WAL checkpoint).
    // For small incremental builds, defer the expensive WAL checkpoint to the
    // next event loop tick. Skip for temp directories (tests) — they rmSync
    // immediately after build.
    const pair = { db: ctx.db, nativeDb: ctx.nativeDb };
    const isTempDir = path.resolve(rootDir).startsWith(path.resolve(tmpdir()));
    if (!isFullBuild && allSymbols.size <= ctx.config.build.smallFilesThreshold && !isTempDir) {
        closeDbPairDeferred(pair);
    }
    else {
        closeDbPair(pair);
    }
    // Write journal header after successful build
    writeJournalHeader(rootDir, Date.now());
    // Skip auto-registration for incremental builds — the repo was already
    // registered during the initial full build. The dynamic import + file I/O
    // costs ~100ms which dominates incremental finalize time.
    if (!opts.skipRegistry && isFullBuild) {
        const tmpDir = path.resolve(tmpdir());
        const resolvedRoot = path.resolve(rootDir);
        if (resolvedRoot.startsWith(tmpDir)) {
            debug(`Skipping auto-registration for temp directory: ${resolvedRoot}`);
        }
        else {
            try {
                const { registerRepo } = (await import('../../../../infrastructure/registry.js'));
                registerRepo(rootDir);
            }
            catch (err) {
                debug(`Auto-registration failed: ${err.message}`);
            }
        }
    }
}
//# sourceMappingURL=finalize.js.map