"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createProjectIndexer = createProjectIndexer;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const micromatch_1 = __importDefault(require("micromatch"));
const embedder_1 = require("../lib/embedder");
const docs_1 = require("../lib/parsers/docs");
const docs_2 = require("../graphs/docs");
const code_1 = require("../lib/parsers/code");
const code_2 = require("../graphs/code");
const watcher_1 = require("../lib/watcher");
const defaults_1 = require("../lib/defaults");
const path_utils_1 = require("../lib/path-utils");
const knowledge_1 = require("../graphs/knowledge");
const task_1 = require("../graphs/task");
const skill_1 = require("../graphs/skill");
const file_index_1 = require("../graphs/file-index");
const logger_1 = require("../lib/logger");
const log = (0, logger_1.createLogger)('indexer');
function createProjectIndexer(docGraph, codeGraph, config, knowledgeGraph, fileIndexGraph, taskGraph, skillGraph) {
    // Three independent serial queues — docs, code, and file index.
    // Array-based to avoid promise chain memory accumulation during scan.
    function createSerialQueue(label) {
        const pending = [];
        let running = false;
        let errors = 0;
        let idleResolve = null;
        let idlePromise = Promise.resolve();
        async function pump() {
            running = true;
            while (pending.length > 0) {
                const fn = pending.shift();
                try {
                    await fn();
                }
                catch (err) {
                    errors++;
                    log.error({ err, queue: label }, 'queue error');
                }
            }
            running = false;
            if (idleResolve) {
                idleResolve();
                idleResolve = null;
            }
        }
        return {
            enqueue(fn) {
                pending.push(fn);
                if (!running) {
                    idlePromise = new Promise(r => { idleResolve = r; });
                    void pump();
                }
            },
            waitIdle() {
                return (pending.length === 0 && !running) ? Promise.resolve() : idlePromise;
            },
            get errors() { return errors; },
        };
    }
    const docsQueue = createSerialQueue('Doc');
    const codeQueue = createSerialQueue('Code');
    const fileQueue = createSerialQueue('File index');
    function enqueueDoc(fn) { docsQueue.enqueue(fn); }
    function enqueueCode(fn) { codeQueue.enqueue(fn); }
    function enqueueFile(fn) { fileQueue.enqueue(fn); }
    // ---------------------------------------------------------------------------
    // Per-file indexing
    // ---------------------------------------------------------------------------
    async function indexDocFile(absolutePath) {
        if (!docGraph)
            return;
        let stat;
        try {
            stat = fs_1.default.statSync(absolutePath);
        }
        catch {
            // File disappeared — remove stale node from graph if present
            const fileId = path_1.default.relative(config.projectDir, absolutePath);
            if (docGraph.hasNode(fileId)) {
                (0, docs_2.removeFile)(docGraph, fileId);
                if (knowledgeGraph)
                    (0, knowledge_1.cleanupProxies)(knowledgeGraph, 'docs', docGraph, config.projectId);
                if (taskGraph)
                    (0, task_1.cleanupProxies)(taskGraph, 'docs', docGraph, config.projectId);
                if (skillGraph)
                    (0, skill_1.cleanupProxies)(skillGraph, 'docs', docGraph, config.projectId);
            }
            return;
        }
        const mtime = stat.mtimeMs;
        const fileId = path_1.default.relative(config.projectDir, absolutePath);
        if ((0, docs_2.getFileMtime)(docGraph, fileId) === mtime)
            return;
        if (config.maxFileSize && stat.size > config.maxFileSize) {
            log.warn({ fileId, sizeMB: (stat.size / 1024 / 1024).toFixed(1) }, 'skip doc (exceeds size limit)');
            return;
        }
        const content = fs_1.default.readFileSync(absolutePath, 'utf-8');
        const chunks = await (0, docs_1.parseFile)(content, absolutePath, config.projectDir, config.chunkDepth);
        // Batch-embed all chunks + file-level in one forward pass
        const batchInputs = chunks.map(c => ({ title: c.title, content: c.content }));
        const rootChunk = chunks.find(c => c.level === 1);
        const normalizedPath = (0, path_utils_1.normalizePathForEmbed)(fileId);
        const embedText = rootChunk?.title
            ? `${normalizedPath} ${rootChunk.title}`
            : `${normalizedPath} ${rootChunk?.content.slice(0, defaults_1.INDEXER_PREVIEW_LEN) ?? ''}`;
        batchInputs.push({ title: embedText, content: '' });
        const embeddings = await (0, embedder_1.embedBatch)(batchInputs, config.docsModelName);
        for (let i = 0; i < chunks.length; i++) {
            chunks[i].embedding = embeddings[i];
        }
        (0, docs_2.updateFile)(docGraph, chunks, mtime);
        if (rootChunk && docGraph.hasNode(rootChunk.id)) {
            docGraph.setNodeAttribute(rootChunk.id, 'fileEmbedding', embeddings[chunks.length]);
        }
        log.info({ fileId, chunks: chunks.length }, 'indexed doc');
    }
    async function indexCodeFile(absolutePath) {
        if (!codeGraph)
            return;
        let stat;
        try {
            stat = fs_1.default.statSync(absolutePath);
        }
        catch {
            const fileId = path_1.default.relative(config.projectDir, absolutePath);
            if (codeGraph.hasNode(fileId)) {
                (0, code_2.removeCodeFile)(codeGraph, fileId);
                if (knowledgeGraph)
                    (0, knowledge_1.cleanupProxies)(knowledgeGraph, 'code', codeGraph, config.projectId);
                if (taskGraph)
                    (0, task_1.cleanupProxies)(taskGraph, 'code', codeGraph, config.projectId);
                if (skillGraph)
                    (0, skill_1.cleanupProxies)(skillGraph, 'code', codeGraph, config.projectId);
            }
            return;
        }
        const mtime = stat.mtimeMs;
        const fileId = path_1.default.relative(config.projectDir, absolutePath);
        if ((0, code_2.getCodeFileMtime)(codeGraph, fileId) === mtime)
            return;
        if (config.maxFileSize && stat.size > config.maxFileSize) {
            log.warn({ fileId, sizeMB: (stat.size / 1024 / 1024).toFixed(1) }, 'skip code (exceeds size limit)');
            return;
        }
        const parsed = await (0, code_1.parseCodeFile)(absolutePath, config.projectDir, mtime);
        // Batch-embed all symbols + file-level in one forward pass
        const batchInputs = parsed.nodes.map(({ attrs }) => ({
            title: attrs.signature,
            content: attrs.kind !== 'file'
                ? [attrs.docComment, attrs.body].filter(Boolean).join('\n')
                : attrs.docComment,
        }));
        // File-level embedding: path + exported symbol names + import summary
        const fileNode = parsed.nodes.find(n => n.attrs.kind === 'file');
        const exportedNames = parsed.nodes
            .filter(n => n.attrs.isExported && n.attrs.kind !== 'file')
            .map(n => n.attrs.name);
        const normalizedCodePath = (0, path_utils_1.normalizePathForEmbed)(fileId);
        const fileEmbedTitle = exportedNames.length > 0
            ? `${normalizedCodePath} ${exportedNames.join(' ')}`
            : normalizedCodePath;
        const fileEmbedContent = fileNode?.attrs.body ?? ''; // body = importSummary for file nodes
        batchInputs.push({ title: fileEmbedTitle, content: fileEmbedContent });
        const embeddings = await (0, embedder_1.embedBatch)(batchInputs, config.codeModelName);
        for (let i = 0; i < parsed.nodes.length; i++) {
            parsed.nodes[i].attrs.embedding = embeddings[i];
        }
        (0, code_2.updateCodeFile)(codeGraph, parsed);
        if (codeGraph.hasNode(fileId)) {
            codeGraph.setNodeAttribute(fileId, 'fileEmbedding', embeddings[parsed.nodes.length]);
        }
        log.info({ fileId, symbols: parsed.nodes.length }, 'indexed code');
    }
    async function indexFileEntry(absolutePath) {
        if (!fileIndexGraph)
            return;
        let stat;
        try {
            stat = fs_1.default.statSync(absolutePath);
        }
        catch {
            const filePath = path_1.default.relative(config.projectDir, absolutePath);
            if (fileIndexGraph.hasNode(filePath)) {
                (0, file_index_1.removeFileEntry)(fileIndexGraph, filePath);
                if (knowledgeGraph)
                    (0, knowledge_1.cleanupProxies)(knowledgeGraph, 'files', fileIndexGraph, config.projectId);
                if (taskGraph)
                    (0, task_1.cleanupProxies)(taskGraph, 'files', fileIndexGraph, config.projectId);
                if (skillGraph)
                    (0, skill_1.cleanupProxies)(skillGraph, 'files', fileIndexGraph, config.projectId);
            }
            return;
        }
        const mtime = stat.mtimeMs;
        const filePath = path_1.default.relative(config.projectDir, absolutePath);
        if ((0, file_index_1.getFileEntryMtime)(fileIndexGraph, filePath) === mtime)
            return;
        const embedding = await (0, embedder_1.embed)((0, path_utils_1.normalizePathForEmbed)(filePath), '', config.filesModelName);
        (0, file_index_1.updateFileEntry)(fileIndexGraph, filePath, stat.size, mtime, embedding);
    }
    // ---------------------------------------------------------------------------
    // Dispatch: match a file against both patterns, enqueue as needed
    // ---------------------------------------------------------------------------
    // Pre-accumulated exclude arrays (already includes server + workspace + project + graph)
    const docsExclude = config.docsExclude;
    const codeExclude = config.codeExclude;
    const filesExclude = config.filesExclude;
    // Union for directory pruning during scan
    const allExcludePatterns = [...new Set([...docsExclude, ...codeExclude, ...filesExclude])];
    function isExcluded(rel, patterns) {
        return patterns.length > 0 && micromatch_1.default.isMatch(rel, patterns);
    }
    // Phase filter — set before scan(), checked in dispatch.
    // undefined = all queues (watcher mode / legacy).
    let currentPhase;
    function dispatchAdd(absolutePath) {
        const rel = path_1.default.relative(config.projectDir, absolutePath);
        if ((!currentPhase || currentPhase === 'docs') && docGraph && config.docsInclude && !isExcluded(rel, docsExclude) && micromatch_1.default.isMatch(rel, config.docsInclude)) {
            if (rel.endsWith('.md'))
                (0, docs_1.clearWikiIndexCache)(config.projectDir);
            enqueueDoc(() => indexDocFile(absolutePath));
        }
        if ((!currentPhase || currentPhase === 'code') && codeGraph && config.codeInclude && !isExcluded(rel, codeExclude) && micromatch_1.default.isMatch(rel, config.codeInclude)) {
            enqueueCode(() => indexCodeFile(absolutePath));
        }
        if ((!currentPhase || currentPhase === 'files') && fileIndexGraph && !isExcluded(rel, filesExclude)) {
            enqueueFile(() => indexFileEntry(absolutePath));
        }
    }
    function dispatchRemove(absolutePath) {
        const rel = path_1.default.relative(config.projectDir, absolutePath);
        if ((!currentPhase || currentPhase === 'docs') && docGraph && config.docsInclude && !isExcluded(rel, docsExclude) && micromatch_1.default.isMatch(rel, config.docsInclude)) {
            if (rel.endsWith('.md'))
                (0, docs_1.clearWikiIndexCache)(config.projectDir);
            // Enqueue removal to avoid racing with in-flight indexDocFile tasks
            enqueueDoc(async () => {
                (0, docs_2.removeFile)(docGraph, rel);
                if (knowledgeGraph)
                    (0, knowledge_1.cleanupProxies)(knowledgeGraph, 'docs', docGraph, config.projectId);
                if (taskGraph)
                    (0, task_1.cleanupProxies)(taskGraph, 'docs', docGraph, config.projectId);
                if (skillGraph)
                    (0, skill_1.cleanupProxies)(skillGraph, 'docs', docGraph, config.projectId);
                log.info({ fileId: rel }, 'removed doc');
            });
        }
        if ((!currentPhase || currentPhase === 'code') && codeGraph && config.codeInclude && !isExcluded(rel, codeExclude) && micromatch_1.default.isMatch(rel, config.codeInclude)) {
            enqueueCode(async () => {
                (0, code_2.removeCodeFile)(codeGraph, rel);
                if (knowledgeGraph)
                    (0, knowledge_1.cleanupProxies)(knowledgeGraph, 'code', codeGraph, config.projectId);
                if (taskGraph)
                    (0, task_1.cleanupProxies)(taskGraph, 'code', codeGraph, config.projectId);
                if (skillGraph)
                    (0, skill_1.cleanupProxies)(skillGraph, 'code', codeGraph, config.projectId);
                log.info({ fileId: rel }, 'removed code');
            });
        }
        if ((!currentPhase || currentPhase === 'files') && fileIndexGraph && !isExcluded(rel, filesExclude)) {
            enqueueFile(async () => {
                (0, file_index_1.removeFileEntry)(fileIndexGraph, rel);
                if (knowledgeGraph)
                    (0, knowledge_1.cleanupProxies)(knowledgeGraph, 'files', fileIndexGraph, config.projectId);
                if (taskGraph)
                    (0, task_1.cleanupProxies)(taskGraph, 'files', fileIndexGraph, config.projectId);
                if (skillGraph)
                    (0, skill_1.cleanupProxies)(skillGraph, 'files', fileIndexGraph, config.projectId);
                log.info({ fileId: rel }, 'removed file');
            });
        }
    }
    // ---------------------------------------------------------------------------
    // Public API
    // ---------------------------------------------------------------------------
    function scan(phase) {
        currentPhase = phase;
        function walk(dir) {
            for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
                if (entry.name.startsWith('.') || watcher_1.ALWAYS_IGNORED.has(entry.name))
                    continue;
                if (entry.isSymbolicLink())
                    continue;
                const full = path_1.default.join(dir, entry.name);
                if (entry.isDirectory()) {
                    const relDir = path_1.default.relative(config.projectDir, full);
                    // prune directory if ALL graphs would exclude it
                    if (allExcludePatterns.length > 0 && (micromatch_1.default.isMatch(relDir, allExcludePatterns) ||
                        micromatch_1.default.isMatch(relDir + '/x', allExcludePatterns)))
                        continue;
                    walk(full);
                }
                else if (entry.isFile()) {
                    dispatchAdd(full);
                }
            }
        }
        walk(config.projectDir);
        currentPhase = undefined; // reset for watcher
    }
    function watch() {
        return (0, watcher_1.startWatcher)(config.projectDir, {
            onAdd: (f) => dispatchAdd(f),
            onChange: (f) => dispatchAdd(f),
            onUnlink: (f) => dispatchRemove(f),
        }, '**/*', allExcludePatterns.length > 0 ? allExcludePatterns : undefined);
    }
    async function drain(phase) {
        if (phase) {
            // Wait for a single queue — no finalize
            if (phase === 'docs')
                await docsQueue.waitIdle();
            else if (phase === 'code')
                await codeQueue.waitIdle();
            else if (phase === 'files')
                await fileQueue.waitIdle();
            return;
        }
        // No phase = wait all + finalize (legacy / after all phases)
        await Promise.all([docsQueue.waitIdle(), codeQueue.waitIdle(), fileQueue.waitIdle()]);
        if (fileIndexGraph)
            (0, file_index_1.rebuildDirectoryStats)(fileIndexGraph);
        // Resolve cross-file edges that were deferred during indexing
        if (docGraph) {
            const docLinks = (0, docs_2.resolvePendingLinks)(docGraph);
            if (docLinks > 0)
                log.info({ count: docLinks }, 'Resolved deferred doc cross-file links');
            // Clean up any remaining unresolved pending links so they don't persist to disk
            let docOrphans = 0;
            docGraph.forEachNode((nid, nattrs) => {
                if (nattrs.pendingLinks) {
                    docGraph.setNodeAttribute(nid, 'pendingLinks', undefined);
                    docOrphans++;
                }
            });
            if (docOrphans > 0)
                log.debug({ count: docOrphans }, 'Cleared unresolvable doc pending links');
        }
        if (codeGraph) {
            const codeImports = (0, code_2.resolvePendingImports)(codeGraph);
            if (codeImports > 0)
                log.info({ count: codeImports }, 'Resolved deferred code import edges');
            const codeEdges = (0, code_2.resolvePendingEdges)(codeGraph);
            if (codeEdges > 0)
                log.info({ count: codeEdges }, 'Resolved deferred code extends/implements edges');
            // Clean up any remaining unresolved pending edges/imports
            let codeOrphans = 0;
            codeGraph.forEachNode((nid, nattrs) => {
                if (nattrs.pendingEdges) {
                    codeGraph.setNodeAttribute(nid, 'pendingEdges', undefined);
                    codeOrphans++;
                }
                if (nattrs.pendingImports) {
                    codeGraph.setNodeAttribute(nid, 'pendingImports', undefined);
                    codeOrphans++;
                }
            });
            if (codeOrphans > 0)
                log.debug({ count: codeOrphans }, 'Cleared unresolvable code pending edges');
        }
        const totalErrors = docsQueue.errors + codeQueue.errors + fileQueue.errors;
        if (totalErrors > 0) {
            log.warn({ totalErrors, docsErrors: docsQueue.errors, codeErrors: codeQueue.errors, filesErrors: fileQueue.errors }, 'Completed with errors');
        }
    }
    return { scan, watch, drain };
}
