/**
 * Stage: buildStructure + classifyRoles
 *
 * Builds directory structure, containment edges, metrics, and classifies node roles.
 */
import path from 'node:path';
import { performance } from 'node:perf_hooks';
import { debug } from '../../../../infrastructure/logger.js';
import { normalizePath } from '../../../../shared/constants.js';
import { readFileSafe } from '../helpers.js';
/** Populate `ctx.lineCountMap` from cached parser results, falling back to disk. */
function populateLineCountMap(ctx) {
    const { fileSymbols, rootDir } = ctx;
    ctx.lineCountMap = new Map();
    for (const [relPath, symbols] of fileSymbols) {
        const lineCount = symbols.lineCount ?? symbols._lineCount;
        if (lineCount) {
            ctx.lineCountMap.set(relPath, lineCount);
            continue;
        }
        const absPath = path.join(rootDir, relPath);
        try {
            const content = readFileSafe(absPath);
            ctx.lineCountMap.set(relPath, content.split('\n').length);
        }
        catch {
            ctx.lineCountMap.set(relPath, 0);
        }
    }
}
/** Count file-kind nodes already in the DB, preferring the native connection. */
function countExistingFiles(ctx) {
    const useNativeReads = ctx.engineName === 'native' && !!ctx.nativeDb;
    const row = (useNativeReads
        ? ctx.nativeDb.queryGet("SELECT COUNT(*) as c FROM nodes WHERE kind = 'file'", [])
        : ctx.db.prepare("SELECT COUNT(*) as c FROM nodes WHERE kind = 'file'").get());
    return row.c;
}
/**
 * Build directory structure + metrics. Chooses between the fast incremental
 * path (a handful of files changed on a large codebase) and the full path
 * (delegated to `features/structure`).
 */
async function buildDirectoryStructure(ctx, changedFileList, useSmallIncrementalFastPath) {
    if (useSmallIncrementalFastPath) {
        updateChangedFileMetrics(ctx, changedFileList);
        return;
    }
    const { db, fileSymbols, rootDir, discoveredDirs, allSymbols, isFullBuild } = ctx;
    const relDirs = new Set();
    for (const absDir of discoveredDirs) {
        relDirs.add(normalizePath(path.relative(rootDir, absDir)));
    }
    try {
        const { buildStructure: buildStructureFn } = (await import('../../../../features/structure.js'));
        const changedFilePaths = isFullBuild ? null : [...allSymbols.keys()];
        buildStructureFn(db, fileSymbols, rootDir, ctx.lineCountMap, relDirs, changedFilePaths);
    }
    catch (err) {
        debug(`Structure analysis failed: ${err.message}`);
    }
}
/** Convert a `NativeDatabase.classifyRoles*` result into the JS summary shape. */
function nativeRoleSummaryToRecord(nativeResult) {
    return {
        entry: nativeResult.entry,
        core: nativeResult.core,
        utility: nativeResult.utility,
        adapter: nativeResult.adapter,
        dead: nativeResult.dead,
        'dead-leaf': nativeResult.deadLeaf,
        'dead-entry': nativeResult.deadEntry,
        'dead-ffi': nativeResult.deadFfi,
        'dead-unresolved': nativeResult.deadUnresolved,
        'test-only': nativeResult.testOnly,
        leaf: nativeResult.leaf,
    };
}
async function classifyRoles(ctx, changedFileList) {
    const useNativeReads = ctx.engineName === 'native' && !!ctx.nativeDb;
    try {
        let roleSummary = null;
        // Use NativeDatabase persistent connection (Phase 6.15+).
        // Standalone napi functions were removed in 6.17 — falls through to JS if nativeDb unavailable.
        // Note: classifyRoles* both read (fan-in/fan-out) and write (UPDATE nodes SET role).
        if (useNativeReads && ctx.nativeDb?.classifyRolesFull) {
            const nativeResult = changedFileList && changedFileList.length > 0
                ? ctx.nativeDb.classifyRolesIncremental(changedFileList)
                : ctx.nativeDb.classifyRolesFull();
            if (nativeResult)
                roleSummary = nativeRoleSummaryToRecord(nativeResult);
        }
        if (!roleSummary) {
            const { classifyNodeRoles } = (await import('../../../../features/structure.js'));
            roleSummary = classifyNodeRoles(ctx.db, changedFileList);
        }
        debug(`Roles${changedFileList ? ` (incremental, ${changedFileList.length} files)` : ''}: ${Object.entries(roleSummary)
            .map(([r, c]) => `${r}=${c}`)
            .join(', ')}`);
    }
    catch (err) {
        debug(`Role classification failed: ${err.message}`);
    }
}
export async function buildStructure(ctx) {
    const { allSymbols, isFullBuild } = ctx;
    populateLineCountMap(ctx);
    const changedFileList = isFullBuild ? null : [...allSymbols.keys()];
    // For small incremental builds on large codebases, use a fast path that
    // updates only the changed files' metrics via targeted SQL instead of
    // loading ALL definitions from DB (~8ms) and recomputing ALL metrics (~15ms).
    // Gate: ≤smallFilesThreshold changed files AND significantly more existing files (>20) to
    // avoid triggering on small test fixtures where directory metrics matter.
    const existingFileCount = !isFullBuild ? countExistingFiles(ctx) : 0;
    const useSmallIncrementalFastPath = !isFullBuild &&
        changedFileList != null &&
        changedFileList.length <= ctx.config.build.smallFilesThreshold &&
        existingFileCount > 20;
    if (!isFullBuild && !useSmallIncrementalFastPath) {
        loadUnchangedFilesFromDb(ctx);
    }
    const t0 = performance.now();
    await buildDirectoryStructure(ctx, changedFileList, useSmallIncrementalFastPath);
    ctx.timing.structureMs = performance.now() - t0;
    const t1 = performance.now();
    await classifyRoles(ctx, changedFileList);
    ctx.timing.rolesMs = performance.now() - t1;
}
// ── Small incremental fast path ──────────────────────────────────────────
/**
 * For small incremental builds, update only the changed files' node_metrics
 * using targeted SQL queries. Skips the full DB load of all definitions
 * (~8ms) and full structure rebuild (~15ms), replacing them with per-file
 * indexed queries (~1-2ms total for 1-5 files).
 *
 * Directory metrics are not recomputed — a 1-5 file change won't
 * meaningfully alter directory-level cohesion or symbol counts.
 */
function updateChangedFileMetrics(ctx, changedFiles) {
    const { db } = ctx;
    const getFileNodeId = db.prepare("SELECT id FROM nodes WHERE name = ? AND kind = 'file' AND file = ? AND line = 0");
    const getSymbolCount = db.prepare("SELECT COUNT(*) as c FROM nodes WHERE file = ? AND kind != 'file' AND kind != 'directory'");
    const getImportCount = db.prepare(`
    SELECT COUNT(DISTINCT n2.file) AS cnt FROM edges e
    JOIN nodes n1 ON e.source_id = n1.id
    JOIN nodes n2 ON e.target_id = n2.id
    WHERE e.kind = 'imports' AND n1.file = ?
  `);
    const getFanIn = db.prepare(`
    SELECT COUNT(DISTINCT n_src.file) AS cnt FROM edges e
    JOIN nodes n_src ON e.source_id = n_src.id
    JOIN nodes n_tgt ON e.target_id = n_tgt.id
    WHERE e.kind IN ('imports', 'imports-type') AND n_tgt.file = ? AND n_src.file != n_tgt.file
  `);
    const getFanOut = db.prepare(`
    SELECT COUNT(DISTINCT n_tgt.file) AS cnt FROM edges e
    JOIN nodes n_src ON e.source_id = n_src.id
    JOIN nodes n_tgt ON e.target_id = n_tgt.id
    WHERE e.kind IN ('imports', 'imports-type') AND n_src.file = ? AND n_src.file != n_tgt.file
  `);
    const upsertMetric = db.prepare(`
    INSERT OR REPLACE INTO node_metrics
      (node_id, line_count, symbol_count, import_count, export_count, fan_in, fan_out, cohesion, file_count)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
  `);
    db.transaction(() => {
        for (const relPath of changedFiles) {
            const fileRow = getFileNodeId.get(relPath, relPath);
            if (!fileRow)
                continue;
            const lineCount = ctx.lineCountMap.get(relPath) || 0;
            const symbolCount = getSymbolCount.get(relPath).c;
            const importCount = getImportCount.get(relPath).cnt;
            const exportCount = ctx.fileSymbols.get(relPath)?.exports.length || 0;
            const fanIn = getFanIn.get(relPath).cnt;
            const fanOut = getFanOut.get(relPath).cnt;
            upsertMetric.run(fileRow.id, lineCount, symbolCount, importCount, exportCount, fanIn, fanOut, null, null);
        }
    })();
    debug(`Structure (fast path): updated metrics for ${changedFiles.length} files`);
}
// ── Full incremental DB load (medium/large changes) ──────────────────────
function loadUnchangedFilesFromDb(ctx) {
    const { db, fileSymbols, rootDir } = ctx;
    const existingFiles = db
        .prepare("SELECT DISTINCT file FROM nodes WHERE kind = 'file'")
        .all();
    // Batch load: all definitions, import counts, and line counts in single queries
    const allDefs = db
        .prepare("SELECT file, name, kind, line FROM nodes WHERE kind != 'file' AND kind != 'directory'")
        .all();
    const defsByFileMap = new Map();
    for (const row of allDefs) {
        let arr = defsByFileMap.get(row.file);
        if (!arr) {
            arr = [];
            defsByFileMap.set(row.file, arr);
        }
        arr.push({ name: row.name, kind: row.kind, line: row.line });
    }
    const allImportCounts = db
        .prepare(`SELECT n1.file, COUNT(DISTINCT n2.file) AS cnt FROM edges e
       JOIN nodes n1 ON e.source_id = n1.id
       JOIN nodes n2 ON e.target_id = n2.id
       WHERE e.kind = 'imports'
       GROUP BY n1.file`)
        .all();
    const importCountMap = new Map();
    for (const row of allImportCounts) {
        importCountMap.set(row.file, row.cnt);
    }
    const cachedLineCounts = new Map();
    for (const row of db
        .prepare(`SELECT n.name AS file, m.line_count
       FROM node_metrics m JOIN nodes n ON m.node_id = n.id
       WHERE n.kind = 'file'`)
        .all()) {
        cachedLineCounts.set(row.file, row.line_count);
    }
    let loadedFromDb = 0;
    for (const { file: relPath } of existingFiles) {
        if (!fileSymbols.has(relPath)) {
            const importCount = importCountMap.get(relPath) || 0;
            fileSymbols.set(relPath, {
                definitions: defsByFileMap.get(relPath) || [],
                imports: new Array(importCount),
                exports: [],
            });
            loadedFromDb++;
        }
        if (!ctx.lineCountMap.has(relPath)) {
            const cached = cachedLineCounts.get(relPath);
            if (cached != null) {
                ctx.lineCountMap.set(relPath, cached);
            }
            else {
                const absPath = path.join(rootDir, relPath);
                try {
                    const content = readFileSafe(absPath);
                    ctx.lineCountMap.set(relPath, content.split('\n').length);
                }
                catch {
                    ctx.lineCountMap.set(relPath, 0);
                }
            }
        }
    }
    debug(`Structure: ${fileSymbols.size} files (${loadedFromDb} loaded from DB)`);
}
//# sourceMappingURL=build-structure.js.map