"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CodeGraphManager = exports.createCodeGraph = void 0;
exports.updateCodeFile = updateCodeFile;
exports.resolvePendingImports = resolvePendingImports;
exports.resolvePendingEdges = resolvePendingEdges;
exports.removeCodeFile = removeCodeFile;
exports.getFileSymbols = getFileSymbols;
exports.getCodeFileMtime = getCodeFileMtime;
exports.listCodeFiles = listCodeFiles;
exports.saveCodeGraph = saveCodeGraph;
exports.loadCodeGraph = loadCodeGraph;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const code_types_1 = require("../graphs/code-types");
Object.defineProperty(exports, "createCodeGraph", { enumerable: true, get: function () { return code_types_1.createCodeGraph; } });
const manager_types_1 = require("../graphs/manager-types");
const code_1 = require("../lib/search/code");
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)('code-graph');
// ---------------------------------------------------------------------------
// CRUD
// ---------------------------------------------------------------------------
/** Replace all nodes/edges for a given file. */
function updateCodeFile(graph, parsed) {
    removeCodeFile(graph, parsed.fileId);
    for (const { id, attrs } of parsed.nodes) {
        graph.addNode(id, attrs);
    }
    const pendingImports = [];
    const pendingEdges = [];
    for (const { from, to, attrs } of parsed.edges) {
        if (!graph.hasNode(to)) {
            if (attrs.kind === 'imports') {
                pendingImports.push(to);
            }
            else if (attrs.kind === 'extends' || attrs.kind === 'implements') {
                // Target class/interface may be in another file — defer resolution
                const toName = to.split('::').pop();
                pendingEdges.push({ from, toName, kind: attrs.kind });
            }
            continue;
        }
        if (graph.hasNode(from) && !graph.hasEdge(from, to)) {
            graph.addEdgeWithKey(`${from}→${to}`, from, to, attrs);
        }
    }
    // Store pending data on the file node for post-drain resolution
    if (graph.hasNode(parsed.fileId)) {
        if (pendingImports.length > 0) {
            graph.setNodeAttribute(parsed.fileId, 'pendingImports', pendingImports);
        }
        if (pendingEdges.length > 0) {
            graph.setNodeAttribute(parsed.fileId, 'pendingEdges', pendingEdges);
        }
    }
}
/**
 * Resolve pending import edges after all files have been indexed.
 * Creates 'imports' edges from file nodes to targets that are now in the graph.
 */
function resolvePendingImports(graph) {
    let created = 0;
    graph.forEachNode((id, attrs) => {
        if (!attrs.pendingImports || attrs.pendingImports.length === 0)
            return;
        const remaining = [];
        for (const targetId of attrs.pendingImports) {
            if (graph.hasNode(targetId) && id !== targetId) {
                const edgeKey = `${id}→${targetId}`;
                if (!graph.hasEdge(edgeKey)) {
                    graph.addEdgeWithKey(edgeKey, id, targetId, { kind: 'imports' });
                    created++;
                }
            }
            else {
                remaining.push(targetId);
            }
        }
        graph.setNodeAttribute(id, 'pendingImports', remaining.length > 0 ? remaining : undefined);
    });
    return created;
}
/**
 * Resolve pending extends/implements edges after all files have been indexed.
 * When multiple candidates share the same name, prefers the one whose file
 * is imported by the source file (falls back to first match).
 */
function resolvePendingEdges(graph) {
    const nameIndex = new Map();
    graph.forEachNode((id, attrs) => {
        if (attrs.kind === 'class' || attrs.kind === 'interface') {
            const list = nameIndex.get(attrs.name) ?? [];
            list.push(id);
            nameIndex.set(attrs.name, list);
        }
    });
    // Build file → imported file IDs index for disambiguation
    const fileImports = new Map();
    graph.forEachNode((id, attrs) => {
        if (attrs.kind === 'file') {
            const imported = new Set();
            graph.forEachOutEdge(id, (_edge, edgeAttrs, _src, target) => {
                if (edgeAttrs.kind === 'imports')
                    imported.add(target);
            });
            fileImports.set(id, imported);
        }
    });
    let created = 0;
    graph.forEachNode((id, attrs) => {
        if (!attrs.pendingEdges || attrs.pendingEdges.length === 0)
            return;
        const remaining = [];
        for (const edge of attrs.pendingEdges) {
            const candidates = nameIndex.get(edge.toName);
            if (candidates && candidates.length > 0 && graph.hasNode(edge.from)) {
                let toId;
                if (candidates.length === 1) {
                    toId = candidates[0];
                }
                else {
                    // Disambiguate: prefer candidate whose file is imported by edge.from's file
                    const fromFileId = edge.from.split('::')[0];
                    const imports = fileImports.get(fromFileId);
                    const match = imports && candidates.find(c => {
                        const cFileId = c.split('::')[0];
                        return imports.has(cFileId);
                    });
                    toId = match ?? candidates[0];
                }
                if (toId !== edge.from && !graph.hasEdge(edge.from, toId)) {
                    graph.addEdgeWithKey(`${edge.from}→${toId}`, edge.from, toId, { kind: edge.kind });
                    created++;
                }
            }
            else {
                remaining.push(edge);
            }
        }
        graph.setNodeAttribute(id, 'pendingEdges', remaining.length > 0 ? remaining : undefined);
    });
    return created;
}
/** Remove all nodes (and their incident edges) belonging to a file. */
function removeCodeFile(graph, fileId) {
    const toRemove = graph.filterNodes((_, attrs) => attrs.fileId === fileId);
    toRemove.forEach(id => graph.dropNode(id));
}
/** Return all nodes for a file, sorted by startLine. */
function getFileSymbols(graph, fileId) {
    return graph
        .filterNodes((_, attrs) => attrs.fileId === fileId)
        .map(id => ({ id, ...graph.getNodeAttributes(id) }))
        .sort((a, b) => a.startLine - b.startLine);
}
/** Return mtime for any node in the file (0 if not indexed). */
function getCodeFileMtime(graph, fileId) {
    const nodes = graph.filterNodes((_, attrs) => attrs.fileId === fileId);
    if (nodes.length === 0)
        return 0;
    return graph.getNodeAttribute(nodes[0], 'mtime');
}
/** List all indexed files with symbol counts. */
function listCodeFiles(graph, filter, limit = defaults_1.LIST_PAGE_SIZE, offset = 0) {
    const files = new Map();
    const lowerFilter = filter?.toLowerCase();
    graph.forEachNode((_, attrs) => {
        files.set(attrs.fileId, (files.get(attrs.fileId) ?? 0) + 1);
    });
    let result = [...files.entries()]
        .map(([fileId, symbolCount]) => ({ fileId, symbolCount }))
        .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 };
}
// ---------------------------------------------------------------------------
// Persistence
// ---------------------------------------------------------------------------
function saveCodeGraph(graph, graphMemory, embeddingFingerprint) {
    fs_1.default.mkdirSync(graphMemory, { recursive: true });
    const file = path_1.default.join(graphMemory, 'code.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 loadCodeGraph(graphMemory, fresh = false, embeddingFingerprint) {
    const graph = (0, code_types_1.createCodeGraph)();
    if (fresh)
        return graph;
    const file = path_1.default.join(graphMemory, 'code.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 code graph');
            return graph;
        }
        const stored = data.embeddingModel;
        if (embeddingFingerprint && stored !== embeddingFingerprint) {
            log.warn('Embedding config changed, re-indexing code 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;
}
// ---------------------------------------------------------------------------
// CodeGraphManager — unified API for code graph operations
// ---------------------------------------------------------------------------
class CodeGraphManager {
    _graph;
    embedFns;
    ext;
    _bm25Index = new bm25_1.BM25Index((attrs) => `${attrs.name} ${attrs.signature} ${attrs.docComment} ${attrs.body.slice(0, defaults_1.BM25_BODY_MAX_CHARS)}`);
    _fileBm25 = new bm25_1.BM25Index((attrs) => attrs.fileId);
    constructor(_graph, embedFns, ext = {}) {
        this._graph = _graph;
        this.embedFns = embedFns;
        this.ext = ext;
        _graph.forEachNode((id, attrs) => {
            this._bm25Index.addDocument(id, attrs);
            if (attrs.kind === 'file')
                this._fileBm25.addDocument(attrs.fileId, { fileId: attrs.fileId });
        });
    }
    get graph() { return this._graph; }
    get bm25Index() { return this._bm25Index; }
    // -- Write (used by indexer) --
    updateFile(parsed) {
        // Remove old nodes from BM25
        this._graph.forEachNode((id, attrs) => {
            if (attrs.fileId === parsed.fileId)
                this._bm25Index.removeDocument(id);
        });
        this._fileBm25.removeDocument(parsed.fileId);
        updateCodeFile(this._graph, parsed);
        // Add new nodes to BM25
        this._graph.forEachNode((id, attrs) => {
            if (attrs.fileId === parsed.fileId)
                this._bm25Index.addDocument(id, attrs);
        });
        this._fileBm25.addDocument(parsed.fileId, { fileId: parsed.fileId });
    }
    removeFile(fileId) {
        this._graph.forEachNode((id, attrs) => {
            if (attrs.fileId === fileId)
                this._bm25Index.removeDocument(id);
        });
        this._fileBm25.removeDocument(fileId);
        removeCodeFile(this._graph, fileId);
    }
    // -- Read --
    listFiles(filter, limit, offset) {
        return listCodeFiles(this._graph, filter, limit, offset);
    }
    getFileSymbols(fileId) {
        return getFileSymbols(this._graph, fileId);
    }
    getFileMtime(fileId) {
        return getCodeFileMtime(this._graph, fileId);
    }
    getSymbolEdges(nodeId) {
        if (!this._graph.hasNode(nodeId))
            return [];
        const edges = [];
        this._graph.forEachOutEdge(nodeId, (_edge, attrs, source, target) => {
            edges.push({ source, target, kind: attrs.kind });
        });
        this._graph.forEachInEdge(nodeId, (_edge, attrs, source, target) => {
            edges.push({ source, target, kind: attrs.kind });
        });
        return edges;
    }
    getSymbol(nodeId) {
        if (!this._graph.hasNode(nodeId))
            return null;
        const crossLinks = (0, manager_types_1.findIncomingCrossLinks)(this.ext, 'code', 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, code_1.searchCode)(this._graph, embedding, { ...opts, queryText: query, bm25Index: this._bm25Index });
    }
    async searchFiles(query, opts) {
        const embedding = await this.embedFns.query(query);
        return (0, files_1.searchCodeFiles)(this._graph, embedding, { ...opts, queryText: query, bm25Index: this._fileBm25 });
    }
}
exports.CodeGraphManager = CodeGraphManager;
