"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.FileIndexGraphManager = void 0;
exports.ensureDirectoryChain = ensureDirectoryChain;
exports.updateFileEntry = updateFileEntry;
exports.removeFileEntry = removeFileEntry;
exports.getFileEntryMtime = getFileEntryMtime;
exports.listAllFiles = listAllFiles;
exports.getFileInfo = getFileInfo;
exports.rebuildDirectoryStats = rebuildDirectoryStats;
exports.saveFileIndexGraph = saveFileIndexGraph;
exports.loadFileIndexGraph = loadFileIndexGraph;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const file_index_types_1 = require("../graphs/file-index-types");
const file_lang_1 = require("../graphs/file-lang");
const manager_types_1 = require("../graphs/manager-types");
const file_index_1 = require("../lib/search/file-index");
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)('file-index');
// ---------------------------------------------------------------------------
// CRUD
// ---------------------------------------------------------------------------
/**
 * Ensure the full directory chain exists from `dir` up to root (`.`).
 * Creates directory nodes and `contains` edges as needed.
 */
function ensureDirectoryChain(graph, dir) {
    if (dir === '.') {
        ensureRootNode(graph);
        return;
    }
    if (!graph.hasNode(dir)) {
        const parent = path_1.default.dirname(dir);
        const parentDir = parent === '.' ? '.' : parent;
        graph.addNode(dir, {
            kind: 'directory',
            filePath: dir,
            fileName: path_1.default.basename(dir),
            directory: parentDir,
            extension: '',
            language: null,
            mimeType: null,
            size: 0,
            fileCount: 0,
            embedding: [],
            mtime: 0,
        });
        ensureDirectoryChain(graph, parentDir);
        if (!graph.hasEdge(parentDir, dir)) {
            graph.addEdge(parentDir, dir, { kind: 'contains' });
        }
    }
}
function ensureRootNode(graph) {
    if (!graph.hasNode('.')) {
        graph.addNode('.', {
            kind: 'directory',
            filePath: '.',
            fileName: '.',
            directory: '',
            extension: '',
            language: null,
            mimeType: null,
            size: 0,
            fileCount: 0,
            embedding: [],
            mtime: 0,
        });
    }
}
/**
 * Add or update a file entry in the graph.
 * Creates parent directory chain + `contains` edges automatically.
 */
function updateFileEntry(graph, filePath, size, mtime, embedding) {
    const ext = path_1.default.extname(filePath);
    const dir = path_1.default.dirname(filePath);
    const directory = dir === '.' ? '.' : dir;
    const attrs = {
        kind: 'file',
        filePath,
        fileName: path_1.default.basename(filePath),
        directory,
        extension: ext,
        language: (0, file_lang_1.getLanguage)(ext),
        mimeType: (0, file_lang_1.getMimeType)(ext),
        size,
        fileCount: 0,
        embedding,
        mtime,
    };
    if (graph.hasNode(filePath)) {
        graph.replaceNodeAttributes(filePath, attrs);
    }
    else {
        graph.addNode(filePath, attrs);
        ensureDirectoryChain(graph, directory);
        if (!graph.hasEdge(directory, filePath)) {
            graph.addEdge(directory, filePath, { kind: 'contains' });
        }
    }
}
/**
 * Remove a file node from the graph.
 * Cleans up empty directory nodes (directories with no remaining children).
 */
function removeFileEntry(graph, filePath) {
    if (!graph.hasNode(filePath))
        return;
    const dir = graph.getNodeAttribute(filePath, 'directory');
    graph.dropNode(filePath);
    cleanEmptyDirs(graph, dir);
}
/** Recursively remove directory nodes that have no children. */
function cleanEmptyDirs(graph, dir) {
    if (!dir || dir === '')
        return;
    if (!graph.hasNode(dir))
        return;
    if (graph.getNodeAttribute(dir, 'kind') !== 'directory')
        return;
    // Never remove root
    if (dir === '.')
        return;
    // Count outgoing `contains` edges
    const children = graph.outDegree(dir);
    if (children > 0)
        return;
    // No children — remove this directory
    const parent = graph.getNodeAttribute(dir, 'directory');
    graph.dropNode(dir);
    cleanEmptyDirs(graph, parent);
}
/**
 * Get mtime for a file node. Returns 0 if not found.
 */
function getFileEntryMtime(graph, filePath) {
    if (!graph.hasNode(filePath))
        return 0;
    return graph.getNodeAttribute(filePath, 'mtime');
}
/**
 * List files (and directories when browsing a directory).
 * When `directory` is provided, returns immediate children of that directory.
 * Otherwise returns all file nodes matching the filters.
 */
function listAllFiles(graph, options = {}) {
    const { directory, extension, language, filter, limit = defaults_1.LIST_PAGE_SIZE, offset = 0 } = options;
    const lowerFilter = filter?.toLowerCase();
    const results = [];
    if (directory !== undefined) {
        // List immediate children of the specified directory
        const dirId = directory || '.';
        if (!graph.hasNode(dirId))
            return { results: [], total: 0 };
        graph.forEachOutNeighbor(dirId, (childId) => {
            const attrs = graph.getNodeAttributes(childId);
            const edgeKey = graph.edge(dirId, childId);
            if (!edgeKey || graph.getEdgeAttribute(edgeKey, 'kind') !== 'contains')
                return;
            if (extension && attrs.extension !== extension)
                return;
            if (language && attrs.language !== language)
                return;
            if (lowerFilter && !attrs.filePath.toLowerCase().includes(lowerFilter))
                return;
            results.push(toEntry(attrs));
        });
    }
    else {
        // List all files matching filters (no dirs in flat mode)
        graph.forEachNode((_, attrs) => {
            if (attrs.kind !== 'file')
                return;
            if (extension && attrs.extension !== extension)
                return;
            if (language && attrs.language !== language)
                return;
            if (lowerFilter && !attrs.filePath.toLowerCase().includes(lowerFilter))
                return;
            results.push(toEntry(attrs));
        });
    }
    results.sort((a, b) => a.filePath.localeCompare(b.filePath));
    return { results: results.slice(offset, offset + limit), total: results.length };
}
function toEntry(attrs) {
    return {
        filePath: attrs.filePath,
        kind: attrs.kind,
        fileName: attrs.fileName,
        extension: attrs.extension,
        language: attrs.language,
        mimeType: attrs.mimeType,
        size: attrs.size,
        fileCount: attrs.fileCount,
    };
}
/**
 * Get full info for a specific file or directory.
 */
function getFileInfo(graph, filePath) {
    if (!graph.hasNode(filePath))
        return null;
    const attrs = graph.getNodeAttributes(filePath);
    return {
        ...toEntry(attrs),
        directory: attrs.directory,
        mtime: attrs.mtime,
    };
}
/**
 * Recompute `size` and `fileCount` aggregates on all directory nodes.
 * Call after a full scan/drain to ensure stats are up to date.
 */
function rebuildDirectoryStats(graph) {
    // Reset all directory stats
    graph.forEachNode((id, attrs) => {
        if (attrs.kind === 'directory') {
            graph.setNodeAttribute(id, 'size', 0);
            graph.setNodeAttribute(id, 'fileCount', 0);
        }
    });
    // Walk all file nodes and accumulate to their direct parent
    graph.forEachNode((_, attrs) => {
        if (attrs.kind !== 'file')
            return;
        const dir = attrs.directory;
        if (!graph.hasNode(dir))
            return;
        graph.setNodeAttribute(dir, 'size', graph.getNodeAttribute(dir, 'size') + attrs.size);
        graph.setNodeAttribute(dir, 'fileCount', graph.getNodeAttribute(dir, 'fileCount') + 1);
    });
}
// ---------------------------------------------------------------------------
// Persistence
// ---------------------------------------------------------------------------
function saveFileIndexGraph(graph, graphMemory, embeddingFingerprint) {
    fs_1.default.mkdirSync(graphMemory, { recursive: true });
    const file = path_1.default.join(graphMemory, 'file-index.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 loadFileIndexGraph(graphMemory, fresh = false, embeddingFingerprint) {
    const graph = (0, file_index_types_1.createFileIndexGraph)();
    if (fresh)
        return graph;
    const file = path_1.default.join(graphMemory, 'file-index.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 file index');
            return graph;
        }
        const stored = data.embeddingModel;
        if (embeddingFingerprint && stored !== embeddingFingerprint) {
            log.warn('Embedding config changed, re-indexing file index');
            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;
}
// ---------------------------------------------------------------------------
// FileIndexGraphManager — unified API for file index graph operations
// ---------------------------------------------------------------------------
class FileIndexGraphManager {
    _graph;
    embedFns;
    ext;
    _bm25Index = new bm25_1.BM25Index((attrs) => attrs.filePath);
    constructor(_graph, embedFns, ext = {}) {
        this._graph = _graph;
        this.embedFns = embedFns;
        this.ext = ext;
        _graph.forEachNode((id, attrs) => {
            if (attrs.kind === 'file')
                this._bm25Index.addDocument(id, attrs);
        });
    }
    get graph() { return this._graph; }
    // -- Write (used by indexer) --
    updateFileEntry(filePath, size, mtime, embedding) {
        updateFileEntry(this._graph, filePath, size, mtime, embedding);
        if (this._graph.hasNode(filePath)) {
            this._bm25Index.updateDocument(filePath, this._graph.getNodeAttributes(filePath));
        }
    }
    removeFileEntry(filePath) {
        this._bm25Index.removeDocument(filePath);
        removeFileEntry(this._graph, filePath);
    }
    rebuildDirectoryStats() {
        rebuildDirectoryStats(this._graph);
    }
    getFileEntryMtime(filePath) {
        return getFileEntryMtime(this._graph, filePath);
    }
    // -- Read --
    listAllFiles(options) {
        return listAllFiles(this._graph, options);
    }
    getFileInfo(filePath) {
        const info = getFileInfo(this._graph, filePath);
        if (!info)
            return null;
        const crossLinks = (0, manager_types_1.findIncomingCrossLinks)(this.ext, 'files', filePath);
        return crossLinks.length > 0 ? { ...info, crossLinks } : info;
    }
    async search(query, opts) {
        const embedding = await this.embedFns.query(query);
        return (0, file_index_1.searchFileIndex)(this._graph, embedding, { ...opts, queryText: query, bm25Index: this._bm25Index });
    }
}
exports.FileIndexGraphManager = FileIndexGraphManager;
