/**
 * Builder helper functions — shared utilities used across pipeline stages.
 *
 * Extracted from the monolithic builder.js so stages can import individually.
 */
import { createHash } from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import { purgeFilesData } from '../../../db/index.js';
import { debug, warn } from '../../../infrastructure/logger.js';
import { EXTENSIONS, IGNORE_DIRS, normalizePath } from '../../../shared/constants.js';
import { compileGlobs, matchesAny } from '../../../shared/globs.js';
export const BUILTIN_RECEIVERS = new Set([
    'console',
    'Math',
    'JSON',
    'Object',
    'Array',
    'String',
    'Number',
    'Boolean',
    'Date',
    'RegExp',
    'Map',
    'Set',
    'WeakMap',
    'WeakSet',
    'Promise',
    'Symbol',
    'Error',
    'TypeError',
    'RangeError',
    'Proxy',
    'Reflect',
    'Intl',
    'globalThis',
    'window',
    'document',
    'process',
    'Buffer',
    'require',
]);
/** Check if a directory entry should be skipped (ignored dirs, dotfiles). */
function shouldSkipEntry(entry, extraIgnore) {
    if (entry.name.startsWith('.') && entry.name !== '.') {
        if (IGNORE_DIRS.has(entry.name))
            return true;
        if (entry.isDirectory())
            return true;
    }
    if (IGNORE_DIRS.has(entry.name))
        return true;
    if (extraIgnore?.has(entry.name))
        return true;
    return false;
}
/**
 * Check whether a source file passes the configured include/exclude globs.
 *
 * Patterns are matched against the path relative to the project root,
 * normalized to forward slashes (e.g. `src/foo/bar.ts`). When both lists
 * are set, a file must match at least one include and no exclude.
 */
export function passesIncludeExclude(relPath, includeRegexes, excludeRegexes) {
    if (includeRegexes.length > 0 && !matchesAny(includeRegexes, relPath))
        return false;
    if (excludeRegexes.length > 0 && matchesAny(excludeRegexes, relPath))
        return false;
    return true;
}
/** Detect a symlink loop for `dir`. Returns true if `dir` was already visited. */
function isSymlinkLoop(dir, visited) {
    let realDir;
    try {
        realDir = fs.realpathSync(dir);
    }
    catch {
        return true;
    }
    if (visited.has(realDir)) {
        warn(`Symlink loop detected, skipping: ${dir}`);
        return true;
    }
    visited.add(realDir);
    return false;
}
/** Read directory entries, returning null on error (already logged). */
function readDirSafe(dir) {
    try {
        return fs.readdirSync(dir, { withFileTypes: true });
    }
    catch (err) {
        warn(`Cannot read directory ${dir}: ${err.message}`);
        return null;
    }
}
/** True if `entry` is a source file we should collect under `ctx`. */
function isCollectableSourceFile(full, entry, ctx) {
    if (!EXTENSIONS.has(path.extname(entry.name)))
        return false;
    if (!ctx.hasGlobFilters)
        return true;
    const rel = normalizePath(path.relative(ctx.rootDir, full));
    return passesIncludeExclude(rel, ctx.includeRegexes, ctx.excludeRegexes);
}
function walkCollect(dir, files, directories, ctx) {
    if (isSymlinkLoop(dir, ctx.visited))
        return;
    const entries = readDirSafe(dir);
    if (!entries)
        return;
    let hasFiles = false;
    for (const entry of entries) {
        if (shouldSkipEntry(entry, ctx.extraIgnore))
            continue;
        const full = path.join(dir, entry.name);
        if (entry.isDirectory()) {
            walkCollect(full, files, directories, ctx);
        }
        else if (isCollectableSourceFile(full, entry, ctx)) {
            files.push(full);
            hasFiles = true;
        }
    }
    if (directories && hasFiles) {
        directories.add(dir);
    }
}
export function collectFiles(dir, files = [], config = {}, directories = null) {
    const trackDirs = directories instanceof Set;
    const includeRegexes = compileGlobs(config.include);
    const excludeRegexes = compileGlobs(config.exclude);
    const ctx = {
        rootDir: dir,
        includeRegexes,
        excludeRegexes,
        hasGlobFilters: includeRegexes.length > 0 || excludeRegexes.length > 0,
        extraIgnore: config.ignoreDirs ? new Set(config.ignoreDirs) : null,
        visited: new Set(),
    };
    walkCollect(dir, files, trackDirs ? directories : null, ctx);
    return trackDirs ? { files, directories: directories } : files;
}
/**
 * Load path aliases from tsconfig.json / jsconfig.json.
 */
export function loadPathAliases(rootDir) {
    const aliases = { baseUrl: null, paths: {} };
    for (const configName of ['tsconfig.json', 'jsconfig.json']) {
        const configPath = path.join(rootDir, configName);
        if (!fs.existsSync(configPath))
            continue;
        try {
            const raw = fs
                .readFileSync(configPath, 'utf-8')
                .replace(/("(?:[^"\\]|\\.)*")|\/\*[\s\S]*?\*\/|\/\/.*$/gm, (_, str) => str ?? '')
                .replace(/,\s*([\]}])/g, '$1');
            const config = JSON.parse(raw);
            const opts = config.compilerOptions || {};
            if (opts.baseUrl)
                aliases.baseUrl = path.resolve(rootDir, opts.baseUrl);
            if (opts.paths) {
                for (const [pattern, targets] of Object.entries(opts.paths)) {
                    aliases.paths[pattern] = targets.map((t) => path.resolve(aliases.baseUrl || rootDir, t));
                }
            }
            break;
        }
        catch (err) {
            warn(`Failed to parse ${configName}: ${err.message}`);
        }
    }
    return aliases;
}
/**
 * Compute MD5 hash of file contents for incremental builds.
 */
export function fileHash(content) {
    return createHash('md5').update(content).digest('hex');
}
/**
 * Stat a file, returning { mtime, size } or null on error.
 *
 * `mtime` is `Math.floor(stat.mtimeMs)` so it matches the integer column
 * stored in the DB. Floor-once-here keeps every consumer honest: storing or
 * comparing a non-floored `mtimeMs` against the integer DB column would cause
 * spurious fast-skip misses on the next build.
 */
export function fileStat(filePath) {
    try {
        const s = fs.statSync(filePath);
        return { mtime: Math.floor(s.mtimeMs), size: s.size };
    }
    catch {
        return null;
    }
}
/**
 * Read a file with retry on transient errors (EBUSY/EACCES/EPERM).
 */
const TRANSIENT_CODES = new Set(['EBUSY', 'EACCES', 'EPERM']);
const RETRY_DELAY_MS = 50;
export function readFileSafe(filePath, retries = 2) {
    for (let attempt = 0;; attempt++) {
        try {
            return fs.readFileSync(filePath, 'utf-8');
        }
        catch (err) {
            if (attempt < retries && TRANSIENT_CODES.has(err.code ?? '')) {
                const sharedBuf = new SharedArrayBuffer(4);
                Atomics.wait(new Int32Array(sharedBuf), 0, 0, RETRY_DELAY_MS);
                continue;
            }
            throw err;
        }
    }
}
/**
 * Purge all graph data for the specified files.
 */
export function purgeFilesFromGraph(db, files, options = {}) {
    // Double-cast needed: better-sqlite3 types don't declare `open`/`name` properties
    purgeFilesData(db, files, options);
}
/** Batch INSERT chunk size for multi-value INSERTs. */
const BATCH_CHUNK = 500;
// Statement caches keyed by chunk size — avoids recompiling for every batch.
const nodeStmtCache = new WeakMap();
const edgeStmtCache = new WeakMap();
function getNodeStmt(db, chunkSize) {
    let cache = nodeStmtCache.get(db);
    if (!cache) {
        cache = new Map();
        nodeStmtCache.set(db, cache);
    }
    let stmt = cache.get(chunkSize);
    if (!stmt) {
        const ph = '(?,?,?,?,?,?,?,?,?)';
        stmt = db.prepare('INSERT OR IGNORE INTO nodes (name,kind,file,line,end_line,parent_id,qualified_name,scope,visibility) VALUES ' +
            Array.from({ length: chunkSize }, () => ph).join(','));
        cache.set(chunkSize, stmt);
    }
    return stmt;
}
function getEdgeStmt(db, chunkSize) {
    let cache = edgeStmtCache.get(db);
    if (!cache) {
        cache = new Map();
        edgeStmtCache.set(db, cache);
    }
    let stmt = cache.get(chunkSize);
    if (!stmt) {
        const ph = '(?,?,?,?,?,?)';
        stmt = db.prepare('INSERT INTO edges (source_id,target_id,kind,confidence,dynamic,technique) VALUES ' +
            Array.from({ length: chunkSize }, () => ph).join(','));
        cache.set(chunkSize, stmt);
    }
    return stmt;
}
/**
 * Batch-insert node rows via multi-value INSERT statements.
 * Each row: [name, kind, file, line, end_line, parent_id, qualified_name, scope, visibility]
 */
export function batchInsertNodes(db, rows) {
    if (!rows.length)
        return;
    for (let i = 0; i < rows.length; i += BATCH_CHUNK) {
        const end = Math.min(i + BATCH_CHUNK, rows.length);
        const chunkSize = end - i;
        const stmt = getNodeStmt(db, chunkSize);
        const vals = [];
        for (let j = i; j < end; j++) {
            const r = rows[j];
            vals.push(r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8]);
        }
        stmt.run(...vals);
    }
}
/**
 * Batch-insert edge rows via multi-value INSERT statements.
 * Each row: [source_id, target_id, kind, confidence, dynamic, technique]
 */
export function batchInsertEdges(db, rows) {
    if (!rows.length)
        return;
    for (let i = 0; i < rows.length; i += BATCH_CHUNK) {
        const end = Math.min(i + BATCH_CHUNK, rows.length);
        const chunkSize = end - i;
        const stmt = getEdgeStmt(db, chunkSize);
        const vals = [];
        for (let j = i; j < end; j++) {
            const r = rows[j];
            vals.push(r[0], r[1], r[2], r[3], r[4], r[5] ?? null);
        }
        stmt.run(...vals);
    }
}
/**
 * CHA (Class Hierarchy Analysis) post-pass.
 *
 * Expands virtual-dispatch call edges for class hierarchies and interface
 * implementations already present in the DB:
 *
 *  1. Build implementors map: parent/interface → [child/implementing class] from
 *     `extends` and `implements` edges.
 *  2. Collect RTA evidence: class nodes that appear as `calls` targets (new X()).
 *  3. Find all `calls` edges to qualified method nodes (name contains '.').
 *  4. For each such call, expand to concrete overrides via the implementors map,
 *     filtered by RTA when evidence exists.
 *
 * Used by both the native orchestrator post-pass and the WASM build-edges pass.
 */
export function runChaPostPass(db) {
    const hasHierarchy = db
        .prepare(`SELECT 1 FROM edges WHERE kind IN ('extends', 'implements') LIMIT 1`)
        .get();
    if (!hasHierarchy)
        return 0;
    const hierarchyRows = db
        .prepare(`SELECT src.name AS child_name, tgt.name AS parent_name
       FROM edges e
       JOIN nodes src ON e.source_id = src.id
       JOIN nodes tgt ON e.target_id = tgt.id
       WHERE e.kind IN ('extends', 'implements')`)
        .all();
    const implementorSets = new Map();
    for (const row of hierarchyRows) {
        let set = implementorSets.get(row.parent_name);
        if (!set) {
            set = new Set();
            implementorSets.set(row.parent_name, set);
        }
        set.add(row.child_name);
    }
    if (implementorSets.size === 0)
        return 0;
    // Convert to arrays for iteration compatibility with the rest of the function
    const implementors = new Map([...implementorSets.entries()].map(([k, v]) => [k, [...v]]));
    // RTA: collect class names instantiated via constructor calls (`new X()`).
    let rtaRows = db
        .prepare(`SELECT DISTINCT tgt.name
       FROM edges e
       JOIN nodes tgt ON e.target_id = tgt.id
       WHERE e.kind = 'calls' AND tgt.kind = 'class'`)
        .all();
    if (rtaRows.length === 0) {
        // Fallback: some languages (e.g. TypeScript via WASM) record constructor calls as
        // 'function' or 'constructor' kind rather than 'class'. Restrict to names that are
        // actually known class names to avoid treating unrelated function calls like `logger()`
        // as class-instantiation evidence.
        // Include both parent/interface names AND implementor (child) names so that
        // `new UserRepository()` (a child class) is correctly detected as RTA evidence.
        const knownClassNames = [
            ...new Set([
                ...implementorSets.keys(),
                ...[...implementorSets.values()].flatMap((s) => [...s]),
            ]),
        ];
        if (knownClassNames.length > 0) {
            // Chunk to stay within SQLite SQLITE_MAX_VARIABLE_NUMBER (999 in many builds).
            const CHUNK = 999;
            for (let i = 0; i < knownClassNames.length; i += CHUNK) {
                const chunk = knownClassNames.slice(i, i + CHUNK);
                const placeholders = chunk.map(() => '?').join(',');
                const chunkRows = db
                    .prepare(`SELECT DISTINCT tgt.name
             FROM edges e
             JOIN nodes tgt ON e.target_id = tgt.id
             WHERE e.kind = 'calls' AND tgt.kind IN ('constructor', 'function')
             AND tgt.name IN (${placeholders})`)
                    .all(...chunk);
                rtaRows = rtaRows.concat(chunkRows);
            }
        }
    }
    const instantiated = new Set(rtaRows.map((r) => r.name));
    const noRtaEvidence = instantiated.size === 0;
    if (noRtaEvidence) {
        debug('runChaPostPass: no constructor-call evidence — proceeding without RTA filter');
    }
    const callToMethods = db
        .prepare(`SELECT e.source_id, tgt.name AS method_name
       FROM edges e
       JOIN nodes tgt ON e.target_id = tgt.id
       WHERE e.kind = 'calls' AND tgt.kind = 'method'
       AND INSTR(tgt.name, '.') > 0`)
        .all();
    const seen = new Set();
    // Scope deduplication to only the source_ids we are about to expand, avoiding
    // a full-table scan. CHA only inserts edges FROM callers that already call a
    // qualified method (the source_ids in callToMethods), so we only need to
    // check existing edges for those specific callers.
    const callerIds = [...new Set(callToMethods.map((r) => r.source_id))];
    if (callerIds.length > 0) {
        // Chunk to stay within SQLite SQLITE_MAX_VARIABLE_NUMBER (999 in many builds).
        const CHUNK = 999;
        for (let i = 0; i < callerIds.length; i += CHUNK) {
            const chunk = callerIds.slice(i, i + CHUNK);
            const placeholders = chunk.map(() => '?').join(',');
            const existingPairs = db
                .prepare(`SELECT source_id, target_id FROM edges WHERE kind = 'calls' AND source_id IN (${placeholders})`)
                .all(...chunk);
            for (const e of existingPairs)
                seen.add(`${e.source_id}|${e.target_id}`);
        }
    }
    // No LIMIT: multiple files can define the same qualified name in a monorepo.
    const findMethodStmt = db.prepare(`SELECT id FROM nodes WHERE name = ? AND kind = 'method'`);
    const newEdges = [];
    for (const { source_id, method_name } of callToMethods) {
        const dotIdx = method_name.indexOf('.');
        if (dotIdx === -1)
            continue;
        const typeName = method_name.slice(0, dotIdx);
        const methodSuffix = method_name.slice(dotIdx + 1);
        // BFS over the implementors map — handles multi-level hierarchies where
        // abstract/non-instantiated classes sit between the call-site type and
        // the concrete leaf implementations (matches runPostNativeCha, issue #1311).
        const bfsQueue = [typeName];
        const bfsVisited = new Set([typeName]);
        while (bfsQueue.length > 0) {
            const current = bfsQueue.shift();
            const children = implementors.get(current);
            if (!children?.length)
                continue;
            for (const cls of children) {
                if (bfsVisited.has(cls))
                    continue;
                bfsVisited.add(cls);
                if (noRtaEvidence || instantiated.has(cls)) {
                    const qualifiedName = `${cls}.${methodSuffix}`;
                    const methodNodes = findMethodStmt.all(qualifiedName);
                    for (const methodNode of methodNodes) {
                        const key = `${source_id}|${methodNode.id}`;
                        if (seen.has(key))
                            continue;
                        seen.add(key);
                        newEdges.push([source_id, methodNode.id, 'calls', 0.8, 0, 'cha']);
                    }
                }
                // Always traverse children — non-instantiated classes may have instantiated subclasses.
                bfsQueue.push(cls);
            }
        }
    }
    if (newEdges.length > 0) {
        db.transaction(() => batchInsertEdges(db, newEdges))();
        debug(`runChaPostPass: inserted ${newEdges.length} CHA dispatch edge(s)`);
    }
    return newEdges.length;
}
//# sourceMappingURL=helpers.js.map