import fs from 'node:fs';
import path from 'node:path';
import { closeDb, getNodeId as getNodeIdQuery, initSchema, openDb } from '../../db/index.js';
import { debug, info } from '../../infrastructure/logger.js';
import { isSupportedFile, normalizePath, shouldIgnore } from '../../shared/constants.js';
import { DbError } from '../../shared/errors.js';
import { createParseTreeCache, getActiveEngine } from '../parser.js';
import { rebuildFile } from './builder/incremental.js';
import { appendChangeEvents, buildChangeEvent, diffSymbols } from './change-journal.js';
import { appendJournalEntriesAndStampHeader } from './journal.js';
function shouldIgnorePath(filePath) {
    const parts = filePath.split(path.sep);
    return parts.some((p) => shouldIgnore(p));
}
/** Prepare all SQL statements needed by the watcher's incremental rebuild. */
function prepareWatcherStatements(db) {
    const stmts = {
        insertNode: db.prepare('INSERT OR IGNORE INTO nodes (name, kind, file, line, end_line) VALUES (?, ?, ?, ?, ?)'),
        getNodeId: {
            get: (name, kind, file, line) => {
                const id = getNodeIdQuery(db, name, kind, file, line);
                return id != null ? { id } : undefined;
            },
        },
        insertEdge: db.prepare('INSERT INTO edges (source_id, target_id, kind, confidence, dynamic) VALUES (?, ?, ?, ?, ?)'),
        deleteNodes: db.prepare('DELETE FROM nodes WHERE file = ?'),
        deleteEdgesForFile: null,
        countNodes: db.prepare('SELECT COUNT(*) as c FROM nodes WHERE file = ?'),
        countEdgesForFile: null,
        findNodeInFile: db.prepare("SELECT id, file FROM nodes WHERE name = ? AND kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module', 'constant') AND file = ?"),
        findNodeByName: db.prepare("SELECT id, file FROM nodes WHERE name = ? AND kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module', 'constant')"),
        listSymbols: db.prepare("SELECT name, kind, line FROM nodes WHERE file = ? AND kind != 'file'"),
    };
    const origDeleteEdges = db.prepare(`DELETE FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = @f) OR target_id IN (SELECT id FROM nodes WHERE file = @f)`);
    const origCountEdges = db.prepare(`SELECT COUNT(*) as c FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = @f) OR target_id IN (SELECT id FROM nodes WHERE file = @f)`);
    stmts.deleteEdgesForFile = { run: (f) => origDeleteEdges.run({ f }) };
    stmts.countEdgesForFile = {
        get: (f) => origCountEdges.get({ f }),
    };
    return stmts;
}
/** Process a batch of pending file changes: rebuild, journal, and log. */
async function processPendingFiles(files, db, rootDir, stmts, engineOpts, cache) {
    const results = [];
    for (const filePath of files) {
        const result = (await rebuildFile(db, rootDir, filePath, stmts, engineOpts, cache, {
            diffSymbols: diffSymbols,
        }));
        if (result)
            results.push(result);
    }
    if (results.length > 0) {
        writeJournalAndChangeEvents(rootDir, results);
    }
    logRebuildResults(results);
}
/** Write journal entries and change events for processed files. */
function writeJournalAndChangeEvents(rootDir, updates) {
    const entries = updates.map((r) => ({
        file: r.file,
        deleted: r.deleted || false,
    }));
    try {
        appendJournalEntriesAndStampHeader(rootDir, entries, Date.now());
    }
    catch (e) {
        debug(`Journal write failed (non-fatal): ${e.message}`);
    }
    const changeEvents = updates.map((r) => buildChangeEvent(r.file, r.event, r.symbolDiff, {
        nodesBefore: r.nodesBefore,
        nodesAfter: r.nodesAfter,
        edgesAdded: r.edgesAdded,
    }));
    try {
        appendChangeEvents(rootDir, changeEvents);
    }
    catch (e) {
        debug(`Change event write failed (non-fatal): ${e.message}`);
    }
}
/** Log rebuild results to the user. */
function logRebuildResults(updates) {
    for (const r of updates) {
        const nodeDelta = r.nodesAdded - r.nodesRemoved;
        const nodeStr = nodeDelta >= 0 ? `+${nodeDelta}` : `${nodeDelta}`;
        if (r.deleted) {
            info(`Removed: ${r.file} (-${r.nodesRemoved} nodes)`);
        }
        else {
            info(`Updated: ${r.file} (${nodeStr} nodes, +${r.edgesAdded} edges)`);
        }
    }
}
/** Recursively collect tracked source files for stat-based polling. */
function collectTrackedFiles(dir, result) {
    let entries;
    try {
        entries = fs.readdirSync(dir, { withFileTypes: true });
    }
    catch (e) {
        debug(`collectTrackedFiles: cannot read ${dir}: ${e.message}`);
        return;
    }
    for (const entry of entries) {
        if (shouldIgnore(entry.name))
            continue;
        const full = path.join(dir, entry.name);
        if (entry.isDirectory()) {
            collectTrackedFiles(full, result);
        }
        else if (isSupportedFile(entry.name)) {
            result.push(full);
        }
    }
}
/** Initialize DB, engine, cache, and statements for watch mode. */
function setupWatcher(rootDir, opts) {
    const dbPath = opts.dbPath ?? path.join(rootDir, '.codegraph', 'graph.db');
    if (!fs.existsSync(dbPath)) {
        throw new DbError('No graph.db found. Run `codegraph build` first.', { file: dbPath });
    }
    const db = openDb(dbPath);
    initSchema(db);
    const engineOpts = {
        engine: (opts.engine || 'auto'),
        dataflow: false,
        ast: false,
    };
    const { name: engineName, version: engineVersion } = getActiveEngine(engineOpts);
    info(`Watch mode using ${engineName} engine${engineVersion ? ` (v${engineVersion})` : ''}`);
    const cache = createParseTreeCache();
    info(cache
        ? 'Incremental parsing enabled (native tree cache)'
        : 'Incremental parsing unavailable (full re-parse)');
    const stmts = prepareWatcherStatements(db);
    return {
        rootDir,
        db,
        stmts,
        engineOpts,
        cache,
        pending: new Set(),
        timer: null,
        debounceMs: 300,
    };
}
/** Schedule debounced processing of pending files. */
function scheduleDebouncedProcess(ctx) {
    if (ctx.timer)
        clearTimeout(ctx.timer);
    ctx.timer = setTimeout(async () => {
        const files = [...ctx.pending];
        ctx.pending.clear();
        await processPendingFiles(files, ctx.db, ctx.rootDir, ctx.stmts, ctx.engineOpts, ctx.cache);
    }, ctx.debounceMs);
}
/** Start polling-based file watcher. Returns cleanup function. */
function startPollingWatcher(ctx, pollIntervalMs) {
    const mtimeMap = new Map();
    const initial = [];
    collectTrackedFiles(ctx.rootDir, initial);
    for (const f of initial) {
        try {
            mtimeMap.set(f, fs.statSync(f).mtimeMs);
        }
        catch {
            /* deleted between collect and stat */
        }
    }
    info(`Polling ${initial.length} tracked files every ${pollIntervalMs}ms`);
    const pollTimer = setInterval(() => {
        const current = [];
        collectTrackedFiles(ctx.rootDir, current);
        const currentSet = new Set(current);
        for (const f of current) {
            try {
                const mtime = fs.statSync(f).mtimeMs;
                const prev = mtimeMap.get(f);
                if (prev === undefined || mtime !== prev) {
                    mtimeMap.set(f, mtime);
                    ctx.pending.add(f);
                }
            }
            catch {
                /* deleted between collect and stat */
            }
        }
        for (const f of mtimeMap.keys()) {
            if (!currentSet.has(f)) {
                mtimeMap.delete(f);
                ctx.pending.add(f);
            }
        }
        if (ctx.pending.size > 0) {
            scheduleDebouncedProcess(ctx);
        }
    }, pollIntervalMs);
    return () => clearInterval(pollTimer);
}
/** Start native OS file watcher. Returns cleanup function. */
function startNativeWatcher(ctx) {
    const watcher = fs.watch(ctx.rootDir, { recursive: true }, (_eventType, filename) => {
        if (!filename)
            return;
        if (shouldIgnorePath(filename))
            return;
        if (!isSupportedFile(filename))
            return;
        ctx.pending.add(path.join(ctx.rootDir, filename));
        scheduleDebouncedProcess(ctx);
    });
    return () => watcher.close();
}
/**
 * Build journal entries for a pending-path set, detecting deletions by
 * existence check.
 *
 * `ctx.pending` is an untyped `Set<string>` — it carries no event-type
 * metadata. Without this check, a file deleted during the watch session
 * would be journaled as "changed", causing the next incremental build to
 * try to re-parse a non-existent file instead of removing it from the graph.
 * Mirrors the deletion detection in `rebuildFile` (see builder/incremental.ts).
 *
 * Exported for unit-testing; prefer `setupShutdownHandler` in production paths.
 */
export function buildFlushEntriesFromPending(rootDir, pending) {
    return [...pending].map((filePath) => ({
        file: normalizePath(path.relative(rootDir, filePath)),
        deleted: !fs.existsSync(filePath),
    }));
}
/** Register SIGINT handler to flush journal and clean up. */
function setupShutdownHandler(ctx, cleanup) {
    process.once('SIGINT', () => {
        info('Stopping watcher...');
        cleanup();
        if (ctx.pending.size > 0) {
            const entries = buildFlushEntriesFromPending(ctx.rootDir, ctx.pending);
            try {
                appendJournalEntriesAndStampHeader(ctx.rootDir, entries, Date.now());
            }
            catch (e) {
                debug(`Journal flush on exit failed (non-fatal): ${e.message}`);
            }
        }
        if (ctx.cache)
            ctx.cache.clear();
        closeDb(ctx.db);
        process.exit(0);
    });
}
export async function watchProject(rootDir, opts = {}) {
    const ctx = setupWatcher(rootDir, opts);
    const usePoll = opts.poll ?? process.platform === 'win32';
    const pollIntervalMs = opts.pollInterval ?? 2000;
    info(`Watching ${rootDir} for changes${usePoll ? ' (polling mode)' : ''}...`);
    info('Press Ctrl+C to stop.');
    const cleanup = usePoll ? startPollingWatcher(ctx, pollIntervalMs) : startNativeWatcher(ctx);
    setupShutdownHandler(ctx, cleanup);
}
//# sourceMappingURL=watcher.js.map