"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DocGraphManager = void 0;
exports.createGraph = createGraph;
exports.updateFile = updateFile;
exports.resolvePendingLinks = resolvePendingLinks;
exports.removeFile = removeFile;
exports.getFileChunks = getFileChunks;
exports.getFileMtime = getFileMtime;
exports.listFiles = listFiles;
exports.saveGraph = saveGraph;
exports.loadGraph = loadGraph;
const graphology_1 = require("graphology");
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const manager_types_1 = require("../graphs/manager-types");
const docs_1 = require("../lib/search/docs");
const files_1 = require("../lib/search/files");
const bm25_1 = require("../lib/search/bm25");
const embedding_codec_1 = require("../lib/embedding-codec");
const graph_persistence_1 = require("../lib/graph-persistence");
const defaults_1 = require("../lib/defaults");
const logger_1 = require("../lib/logger");
const log = (0, logger_1.createLogger)('docs-graph');
function createGraph() {
    return new graphology_1.DirectedGraph({ multi: false, allowSelfLoops: false });
}
// Replace all chunks for a given file
function updateFile(graph, chunks, mtime) {
    if (chunks.length === 0)
        return;
    const fileId = chunks[0].fileId;
    removeFile(graph, fileId);
    // Add nodes
    for (const chunk of chunks) {
        graph.addNode(chunk.id, {
            fileId,
            title: chunk.title,
            content: chunk.content,
            embedding: chunk.embedding,
            fileEmbedding: [],
            level: chunk.level,
            mtime,
            language: chunk.language,
            symbols: chunk.symbols ?? [],
        });
    }
    // Sibling edges: consecutive chunks within the same file
    for (let i = 0; i < chunks.length - 1; i++) {
        graph.addEdge(chunks[i].id, chunks[i + 1].id);
    }
    // Cross-file link edges: chunk → root chunk of target file
    for (const chunk of chunks) {
        const pending = [];
        for (const targetFileId of chunk.links) {
            const targetRootId = targetFileId; // root chunk id === fileId
            if (graph.hasNode(targetRootId) && chunk.id !== targetRootId) {
                if (!graph.hasEdge(chunk.id, targetRootId)) {
                    graph.addEdge(chunk.id, targetRootId);
                }
            }
            else if (chunk.id !== targetRootId) {
                pending.push(targetRootId);
            }
        }
        if (pending.length > 0 && graph.hasNode(chunk.id)) {
            graph.setNodeAttribute(chunk.id, 'pendingLinks', pending);
        }
    }
}
/**
 * Resolve pending cross-file link edges after all files have been indexed.
 * Iterates all chunks with pendingLinks and creates edges for targets now in graph.
 */
function resolvePendingLinks(graph) {
    let created = 0;
    graph.forEachNode((id, attrs) => {
        if (!attrs.pendingLinks || attrs.pendingLinks.length === 0)
            return;
        const remaining = [];
        for (const targetId of attrs.pendingLinks) {
            if (graph.hasNode(targetId) && id !== targetId) {
                if (!graph.hasEdge(id, targetId)) {
                    graph.addEdge(id, targetId);
                    created++;
                }
            }
            else {
                remaining.push(targetId);
            }
        }
        graph.setNodeAttribute(id, 'pendingLinks', remaining.length > 0 ? remaining : undefined);
    });
    return created;
}
function removeFile(graph, fileId) {
    const toRemove = graph.filterNodes((_, attrs) => attrs.fileId === fileId);
    toRemove.forEach(id => graph.dropNode(id));
}
function getFileChunks(graph, fileId) {
    return graph
        .filterNodes((_, attrs) => attrs.fileId === fileId)
        .map(id => ({ id, ...graph.getNodeAttributes(id) }))
        .sort((a, b) => a.level - b.level);
}
function getFileMtime(graph, fileId) {
    const nodes = graph.filterNodes((_, attrs) => attrs.fileId === fileId);
    if (nodes.length === 0)
        return 0;
    return graph.getNodeAttribute(nodes[0], 'mtime');
}
function listFiles(graph, filter, limit = defaults_1.LIST_PAGE_SIZE, offset = 0) {
    const files = new Map();
    const lowerFilter = filter?.toLowerCase();
    graph.forEachNode((_, attrs) => {
        const entry = files.get(attrs.fileId);
        if (!entry) {
            files.set(attrs.fileId, { title: attrs.title, chunks: 1 });
        }
        else {
            if (attrs.level === 1)
                entry.title = attrs.title;
            entry.chunks++;
        }
    });
    let result = [...files.entries()]
        .map(([fileId, { title, chunks }]) => ({ fileId, title, chunks }))
        .sort((a, b) => a.fileId.localeCompare(b.fileId));
    if (lowerFilter) {
        result = result.filter(f => f.fileId.toLowerCase().includes(lowerFilter));
    }
    return { results: result.slice(offset, offset + limit), total: result.length };
}
function saveGraph(graph, graphMemory, embeddingFingerprint) {
    fs_1.default.mkdirSync(graphMemory, { recursive: true });
    const file = path_1.default.join(graphMemory, 'docs.json');
    const tmp = file + '.tmp';
    try {
        const exported = graph.export();
        (0, embedding_codec_1.compressEmbeddings)(exported);
        fs_1.default.writeFileSync(tmp, JSON.stringify({ version: defaults_1.GRAPH_DATA_VERSION, embeddingModel: embeddingFingerprint, graph: exported }));
        fs_1.default.renameSync(tmp, file);
    }
    catch (err) {
        try {
            fs_1.default.unlinkSync(tmp);
        }
        catch { /* ignore cleanup error */ }
        throw err;
    }
}
function loadGraph(graphMemory, fresh = false, embeddingFingerprint) {
    const graph = createGraph();
    if (fresh)
        return graph;
    const file = path_1.default.join(graphMemory, 'docs.json');
    const data = (0, graph_persistence_1.readJsonWithTmpFallback)(file);
    if (!data)
        return graph;
    try {
        const storedVersion = data.version;
        if (storedVersion !== defaults_1.GRAPH_DATA_VERSION) {
            log.warn({ storedVersion: storedVersion ?? 'none', currentVersion: defaults_1.GRAPH_DATA_VERSION }, 'Data version changed, re-indexing docs graph');
            return graph;
        }
        const stored = data.embeddingModel;
        if (embeddingFingerprint && stored !== embeddingFingerprint) {
            log.warn('Embedding config changed, re-indexing docs graph');
            return graph;
        }
        if (!(0, graph_persistence_1.validateGraphStructure)(data.graph)) {
            log.warn({ file }, 'Invalid graph structure, starting fresh');
            return graph;
        }
        (0, embedding_codec_1.decompressEmbeddings)(data.graph);
        graph.import(data.graph);
        log.info({ nodes: graph.order, edges: graph.size }, 'Loaded graph');
    }
    catch (err) {
        log.error({ err }, 'Failed to load graph, starting fresh');
    }
    return graph;
}
// ---------------------------------------------------------------------------
// DocGraphManager — unified API for docs graph operations
// ---------------------------------------------------------------------------
class DocGraphManager {
    _graph;
    embedFns;
    ext;
    _bm25Index = new bm25_1.BM25Index((attrs) => `${attrs.title} ${attrs.content}`);
    _fileBm25 = new bm25_1.BM25Index((attrs) => `${attrs.fileId} ${attrs.title}`);
    constructor(_graph, embedFns, ext = {}) {
        this._graph = _graph;
        this.embedFns = embedFns;
        this.ext = ext;
        _graph.forEachNode((id, attrs) => {
            this._bm25Index.addDocument(id, attrs);
            if (attrs.level === 1)
                this._fileBm25.addDocument(attrs.fileId, { fileId: attrs.fileId, title: attrs.title });
        });
    }
    get graph() { return this._graph; }
    get bm25Index() { return this._bm25Index; }
    // -- Write (used by indexer) --
    updateFile(chunks, mtime) {
        // Remove old nodes from BM25
        if (chunks.length > 0) {
            const fileId = chunks[0].fileId;
            this._graph.forEachNode((id, attrs) => {
                if (attrs.fileId === fileId)
                    this._bm25Index.removeDocument(id);
            });
            this._fileBm25.removeDocument(fileId);
        }
        updateFile(this._graph, chunks, mtime);
        // Add new nodes to BM25
        if (chunks.length > 0) {
            const fileId = chunks[0].fileId;
            this._graph.forEachNode((id, attrs) => {
                if (attrs.fileId === fileId)
                    this._bm25Index.addDocument(id, attrs);
            });
            const root = chunks.find(c => c.level === 1);
            if (root)
                this._fileBm25.addDocument(fileId, { fileId, title: root.title });
        }
    }
    removeFile(fileId) {
        this._graph.forEachNode((id, attrs) => {
            if (attrs.fileId === fileId)
                this._bm25Index.removeDocument(id);
        });
        this._fileBm25.removeDocument(fileId);
        removeFile(this._graph, fileId);
    }
    // -- Read --
    listFiles(filter, limit, offset) {
        return listFiles(this._graph, filter, limit, offset);
    }
    getFileChunks(fileId) {
        return getFileChunks(this._graph, fileId);
    }
    getFileMtime(fileId) {
        return getFileMtime(this._graph, fileId);
    }
    getNode(nodeId) {
        if (!this._graph.hasNode(nodeId))
            return null;
        const crossLinks = (0, manager_types_1.findIncomingCrossLinks)(this.ext, 'docs', nodeId);
        return { id: nodeId, ...this._graph.getNodeAttributes(nodeId), ...(crossLinks.length > 0 ? { crossLinks } : {}) };
    }
    async search(query, opts) {
        const embedding = opts?.searchMode === 'keyword' ? [] : await this.embedFns.query(query);
        return (0, docs_1.search)(this._graph, embedding, { ...opts, queryText: query, bm25Index: this._bm25Index });
    }
    async searchFiles(query, opts) {
        const embedding = await this.embedFns.query(query);
        return (0, files_1.searchDocFiles)(this._graph, embedding, { ...opts, queryText: query, bm25Index: this._fileBm25 });
    }
}
exports.DocGraphManager = DocGraphManager;
