/**
 * 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 { 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;
}
export function collectFiles(dir, files = [], config = {}, directories = null, _visited = new Set(), _rootDir, _includeRegexes, _excludeRegexes) {
    const trackDirs = directories instanceof Set;
    let hasFiles = false;
    // First call: compute root and compile include/exclude patterns once,
    // then pass them down recursive calls so we don't recompile per directory.
    const rootDir = _rootDir ?? dir;
    const includeRegexes = _includeRegexes ?? compileGlobs(config.include);
    const excludeRegexes = _excludeRegexes ?? compileGlobs(config.exclude);
    const hasGlobFilters = includeRegexes.length > 0 || excludeRegexes.length > 0;
    // Merge config ignoreDirs with defaults
    const extraIgnore = config.ignoreDirs ? new Set(config.ignoreDirs) : null;
    // Detect symlink loops (before I/O to avoid wasted readdirSync)
    let realDir;
    try {
        realDir = fs.realpathSync(dir);
    }
    catch {
        return trackDirs ? { files, directories: directories } : files;
    }
    if (_visited.has(realDir)) {
        warn(`Symlink loop detected, skipping: ${dir}`);
        return trackDirs ? { files, directories: directories } : files;
    }
    _visited.add(realDir);
    let entries;
    try {
        entries = fs.readdirSync(dir, { withFileTypes: true });
    }
    catch (err) {
        warn(`Cannot read directory ${dir}: ${err.message}`);
        return trackDirs ? { files, directories: directories } : files;
    }
    for (const entry of entries) {
        if (shouldSkipEntry(entry, extraIgnore))
            continue;
        const full = path.join(dir, entry.name);
        if (entry.isDirectory()) {
            if (trackDirs) {
                collectFiles(full, files, config, directories, _visited, rootDir, includeRegexes, excludeRegexes);
            }
            else {
                collectFiles(full, files, config, null, _visited, rootDir, includeRegexes, excludeRegexes);
            }
        }
        else if (EXTENSIONS.has(path.extname(entry.name))) {
            if (hasGlobFilters) {
                const rel = normalizePath(path.relative(rootDir, full));
                if (!passesIncludeExclude(rel, includeRegexes, excludeRegexes))
                    continue;
            }
            files.push(full);
            hasFiles = true;
        }
    }
    if (trackDirs && hasFiles) {
        directories.add(dir);
    }
    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 { mtimeMs, size } or null on error.
 */
export function fileStat(filePath) {
    try {
        const s = fs.statSync(filePath);
        return { mtimeMs: 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) 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]
 */
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]);
        }
        stmt.run(...vals);
    }
}
//# sourceMappingURL=helpers.js.map