"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProjectManager = void 0;
const events_1 = require("events");
const embedder_1 = require("../lib/embedder");
const docs_1 = require("../graphs/docs");
const code_1 = require("../graphs/code");
const knowledge_1 = require("../graphs/knowledge");
const file_index_1 = require("../graphs/file-index");
const task_1 = require("../graphs/task");
const skill_1 = require("../graphs/skill");
const indexer_1 = require("../cli/indexer");
const code_2 = require("../lib/parsers/code");
const docs_2 = require("../lib/parsers/docs");
const promise_queue_1 = require("../lib/promise-queue");
const multi_config_1 = require("../lib/multi-config");
const mirror_watcher_1 = require("../lib/mirror-watcher");
const team_1 = require("../lib/team");
const path_1 = __importDefault(require("path"));
const defaults_1 = require("../lib/defaults");
const logger_1 = require("../lib/logger");
const log = (0, logger_1.createLogger)('project-manager');
// ---------------------------------------------------------------------------
// ProjectManager
// ---------------------------------------------------------------------------
class ProjectManager extends events_1.EventEmitter {
    serverConfig;
    projects = new Map();
    workspaces = new Map();
    autoSaveInterval;
    cacheFactory;
    constructor(serverConfig, cacheFactory) {
        super();
        this.serverConfig = serverConfig;
        this.cacheFactory = cacheFactory;
    }
    // ---------------------------------------------------------------------------
    // Workspaces
    // ---------------------------------------------------------------------------
    /**
     * Add a workspace: load shared knowledge/task/skill graphs.
     * Must be called before addProject for projects that belong to this workspace.
     */
    async addWorkspace(id, config, reindex = false) {
        if (this.workspaces.has(id)) {
            throw new Error(`Workspace "${id}" already exists`);
        }
        const gc = config.graphConfigs;
        const knowledgeGraph = (0, knowledge_1.loadKnowledgeGraph)(config.graphMemory, reindex, (0, multi_config_1.embeddingFingerprint)(gc.knowledge.model));
        const taskGraph = (0, task_1.loadTaskGraph)(config.graphMemory, reindex, (0, multi_config_1.embeddingFingerprint)(gc.tasks.model));
        const skillGraph = (0, skill_1.loadSkillGraph)(config.graphMemory, reindex, (0, multi_config_1.embeddingFingerprint)(gc.skills.model));
        const mutationQueue = new promise_queue_1.PromiseQueue();
        const mirrorTracker = new mirror_watcher_1.MirrorWriteTracker();
        const wsInstance = {
            id,
            config,
            knowledgeGraph,
            taskGraph,
            skillGraph,
            mutationQueue,
            mirrorTracker,
            dirty: false,
        };
        let _authorEnsured = false;
        const ctx = {
            markDirty: () => {
                wsInstance.dirty = true;
                if (!_authorEnsured) {
                    _authorEnsured = true;
                    (0, team_1.ensureAuthorInTeam)(path_1.default.join(config.mirrorDir, '.team'), config.author);
                }
            },
            emit: (event, data) => { this.emit(event, data); },
            projectId: id,
            mirrorDir: config.mirrorDir,
            author: (0, multi_config_1.formatAuthor)(config.author),
        };
        // ExternalGraphs for workspace — projectGraphs will be populated as projects are added
        const ext = {
            knowledgeGraph,
            taskGraph,
            skillGraph,
            projectGraphs: new Map(),
        };
        const knowledgeEmbedFns = {
            document: (q) => (0, embedder_1.embed)(q, '', `${id}:knowledge`),
            query: (q) => (0, embedder_1.embedQuery)(q, `${id}:knowledge`),
        };
        const taskEmbedFns = {
            document: (q) => (0, embedder_1.embed)(q, '', `${id}:tasks`),
            query: (q) => (0, embedder_1.embedQuery)(q, `${id}:tasks`),
        };
        const skillEmbedFns = {
            document: (q) => (0, embedder_1.embed)(q, '', `${id}:skills`),
            query: (q) => (0, embedder_1.embedQuery)(q, `${id}:skills`),
        };
        wsInstance.knowledgeManager = new knowledge_1.KnowledgeGraphManager(knowledgeGraph, knowledgeEmbedFns, ctx, ext);
        wsInstance.taskManager = new task_1.TaskGraphManager(taskGraph, taskEmbedFns, ctx, ext);
        wsInstance.skillManager = new skill_1.SkillGraphManager(skillGraph, skillEmbedFns, ctx, ext);
        wsInstance.knowledgeManager.setMirrorTracker(mirrorTracker);
        wsInstance.taskManager.setMirrorTracker(mirrorTracker);
        wsInstance.skillManager.setMirrorTracker(mirrorTracker);
        this.workspaces.set(id, wsInstance);
        log.info({ workspace: id }, 'Added workspace');
    }
    /**
     * Load embedding models for a workspace. Call after addWorkspace.
     */
    async loadWorkspaceModels(id) {
        const ws = this.workspaces.get(id);
        if (!ws)
            throw new Error(`Workspace "${id}" not found`);
        const gc = ws.config.graphConfigs;
        await (0, embedder_1.loadModel)(gc.knowledge.model, gc.knowledge.embedding, this.serverConfig.modelsDir, `${id}:knowledge`, this.cacheFactory);
        await (0, embedder_1.loadModel)(gc.tasks.model, gc.tasks.embedding, this.serverConfig.modelsDir, `${id}:tasks`, this.cacheFactory);
        await (0, embedder_1.loadModel)(gc.skills.model, gc.skills.embedding, this.serverConfig.modelsDir, `${id}:skills`, this.cacheFactory);
    }
    /**
     * Start mirror watcher for a workspace. Call after all workspace projects are indexed.
     */
    async startWorkspaceMirror(id) {
        const ws = this.workspaces.get(id);
        if (!ws)
            throw new Error(`Workspace "${id}" not found`);
        const mirrorConfig = {
            projectDir: ws.config.mirrorDir,
            knowledgeManager: ws.knowledgeManager,
            taskManager: ws.taskManager,
            skillManager: ws.skillManager,
            mutationQueue: ws.mutationQueue,
            tracker: ws.mirrorTracker,
        };
        await (0, mirror_watcher_1.scanMirrorDirs)(mirrorConfig);
        ws.mirrorWatcher = (0, mirror_watcher_1.startMirrorWatcher)(mirrorConfig);
    }
    getWorkspace(id) {
        return this.workspaces.get(id);
    }
    listWorkspaces() {
        return Array.from(this.workspaces.keys());
    }
    /** Find which workspace a project belongs to. */
    getProjectWorkspace(projectId) {
        const project = this.projects.get(projectId);
        if (!project?.workspaceId)
            return undefined;
        return this.workspaces.get(project.workspaceId);
    }
    /**
     * Add a project: load graphs, load models, create indexer, start watcher.
     */
    async addProject(id, config, reindex = false, workspaceId) {
        if (this.projects.has(id)) {
            throw new Error(`Project "${id}" already exists`);
        }
        const ws = workspaceId ? this.workspaces.get(workspaceId) : undefined;
        if (workspaceId && !ws)
            throw new Error(`Workspace "${workspaceId}" not found`);
        const gc = config.graphConfigs;
        // Load per-project graphs (gated by enabled flag)
        const docGraph = gc.docs.enabled ? (0, docs_1.loadGraph)(config.graphMemory, reindex, (0, multi_config_1.embeddingFingerprint)(gc.docs.model)) : undefined;
        const codeGraph = gc.code.enabled ? (0, code_1.loadCodeGraph)(config.graphMemory, reindex, (0, multi_config_1.embeddingFingerprint)(gc.code.model)) : undefined;
        const fileIndexGraph = gc.files.enabled ? (0, file_index_1.loadFileIndexGraph)(config.graphMemory, reindex, (0, multi_config_1.embeddingFingerprint)(gc.files.model)) : undefined;
        // Knowledge/tasks/skills: shared from workspace or per-project (gated by enabled)
        const knowledgeGraph = ws ? ws.knowledgeGraph
            : gc.knowledge.enabled ? (0, knowledge_1.loadKnowledgeGraph)(config.graphMemory, reindex, (0, multi_config_1.embeddingFingerprint)(gc.knowledge.model)) : undefined;
        const taskGraph = ws ? ws.taskGraph
            : gc.tasks.enabled ? (0, task_1.loadTaskGraph)(config.graphMemory, reindex, (0, multi_config_1.embeddingFingerprint)(gc.tasks.model)) : undefined;
        const skillGraph = ws ? ws.skillGraph
            : gc.skills.enabled ? (0, skill_1.loadSkillGraph)(config.graphMemory, reindex, (0, multi_config_1.embeddingFingerprint)(gc.skills.model)) : undefined;
        // Build embed functions (project-scoped model names)
        const embedFns = this.buildEmbedFns(id);
        const instance = {
            id,
            config,
            docGraph,
            codeGraph,
            knowledgeGraph,
            fileIndexGraph,
            taskGraph,
            skillGraph,
            embedFns,
            mutationQueue: ws ? ws.mutationQueue : new promise_queue_1.PromiseQueue(),
            dirty: false,
            workspaceId,
        };
        // Build graph manager context
        let _authorEnsured = false;
        const ctx = {
            markDirty: () => {
                instance.dirty = true;
                if (!_authorEnsured) {
                    _authorEnsured = true;
                    (0, team_1.ensureAuthorInTeam)(path_1.default.join(config.projectDir, '.team'), config.author);
                }
            },
            emit: (event, data) => { this.emit(event, data); },
            projectId: id,
            projectDir: config.projectDir,
            author: (0, multi_config_1.formatAuthor)(config.author),
        };
        const ext = { docGraph, codeGraph, knowledgeGraph, fileIndexGraph, taskGraph, skillGraph };
        // In workspace mode, register this project's graphs for cross-graph resolution
        if (ws) {
            const wsExt = ws.knowledgeManager.externalGraphs;
            if (wsExt?.projectGraphs) {
                wsExt.projectGraphs.set(id, { docGraph, codeGraph, fileIndexGraph });
            }
        }
        instance.docManager = docGraph ? new docs_1.DocGraphManager(docGraph, embedFns.docs, ext) : undefined;
        instance.codeManager = codeGraph ? new code_1.CodeGraphManager(codeGraph, embedFns.code, ext) : undefined;
        instance.fileIndexManager = fileIndexGraph ? new file_index_1.FileIndexGraphManager(fileIndexGraph, embedFns.files, ext) : undefined;
        if (ws) {
            // Use workspace-level shared managers
            instance.knowledgeManager = ws.knowledgeManager;
            instance.taskManager = ws.taskManager;
            instance.skillManager = ws.skillManager;
            instance.mirrorTracker = ws.mirrorTracker;
        }
        else {
            // Per-project managers (only if enabled)
            if (knowledgeGraph) {
                instance.knowledgeManager = new knowledge_1.KnowledgeGraphManager(knowledgeGraph, embedFns.knowledge, ctx, ext);
            }
            if (taskGraph) {
                instance.taskManager = new task_1.TaskGraphManager(taskGraph, embedFns.tasks, ctx, ext);
            }
            if (skillGraph) {
                instance.skillManager = new skill_1.SkillGraphManager(skillGraph, embedFns.skills, ctx, ext);
            }
            // Set up mirror write tracker for feedback loop prevention
            if (instance.knowledgeManager || instance.taskManager || instance.skillManager) {
                const mirrorTracker = new mirror_watcher_1.MirrorWriteTracker();
                instance.mirrorTracker = mirrorTracker;
                instance.knowledgeManager?.setMirrorTracker(mirrorTracker);
                instance.taskManager?.setMirrorTracker(mirrorTracker);
                instance.skillManager?.setMirrorTracker(mirrorTracker);
            }
        }
        this.projects.set(id, instance);
        log.info({ project: id, projectDir: config.projectDir, workspace: workspaceId }, 'Added project');
    }
    /**
     * Load embedding models for a project. Call after addProject.
     * Separated because model loading is slow and server can start before it's done.
     */
    async loadModels(id) {
        const instance = this.projects.get(id);
        if (!instance)
            throw new Error(`Project "${id}" not found`);
        const gc = instance.config.graphConfigs;
        // Skip knowledge/tasks/skills models for workspace projects (loaded by workspace)
        const skipGraphs = instance.workspaceId
            ? new Set(['knowledge', 'tasks', 'skills'])
            : new Set();
        for (const gn of multi_config_1.GRAPH_NAMES) {
            if (skipGraphs.has(gn))
                continue;
            if (!gc[gn].enabled)
                continue;
            await (0, embedder_1.loadModel)(gc[gn].model, gc[gn].embedding, this.serverConfig.modelsDir, `${id}:${gn}`, this.cacheFactory);
        }
    }
    /**
     * Start indexing + watching for a project. Call after loadModels.
     * Uses three sequential phases: docs → files → code, then finalize.
     */
    async startIndexing(id) {
        this.ensureIndexer(id);
        const instance = this.projects.get(id);
        // Three sequential phases
        for (const phase of ['docs', 'files', 'code']) {
            instance.indexer.scan(phase);
            try {
                await instance.indexer.drain(phase);
            }
            catch (err) {
                log.error({ project: id, phase, err }, 'Error draining phase');
            }
        }
        await this.finalizeIndexing(id);
    }
    /**
     * Create the indexer and watcher for a project (if not already created).
     * Call before startIndexingPhase or startIndexing.
     */
    ensureIndexer(id) {
        const instance = this.projects.get(id);
        if (!instance)
            throw new Error(`Project "${id}" not found`);
        if (instance.indexer)
            return; // already created
        // Clear parser caches to prevent cross-project leaks in multi-project mode
        (0, code_2.clearPathMappingsCache)();
        (0, docs_2.clearWikiIndexCache)();
        const gc = instance.config.graphConfigs;
        const indexer = (0, indexer_1.createProjectIndexer)(instance.docGraph, instance.codeGraph, {
            projectId: id,
            projectDir: instance.config.projectDir,
            docsInclude: gc.docs.enabled ? gc.docs.include : undefined,
            docsExclude: gc.docs.exclude,
            codeInclude: gc.code.enabled ? gc.code.include : undefined,
            codeExclude: gc.code.exclude,
            filesExclude: gc.files.exclude,
            chunkDepth: instance.config.chunkDepth,
            maxFileSize: instance.config.maxFileSize,
            docsModelName: `${id}:docs`,
            codeModelName: `${id}:code`,
            filesModelName: `${id}:files`,
        }, instance.knowledgeGraph, instance.fileIndexGraph, instance.taskGraph, instance.skillGraph);
        instance.indexer = indexer;
    }
    /**
     * Run a single indexing phase (docs, code, or files) for a project.
     * Call ensureIndexer first. Watcher is NOT started — call finalizeIndexing when done.
     */
    async startIndexingPhase(id, phase) {
        const instance = this.projects.get(id);
        if (!instance)
            throw new Error(`Project "${id}" not found`);
        if (!instance.indexer)
            throw new Error(`Indexer not created for "${id}". Call ensureIndexer() first.`);
        instance.indexer.scan(phase);
        await instance.indexer.drain(phase);
    }
    /**
     * Finalize indexing: run full drain (finalize edges), start watcher, save, mirror, emit.
     * Call after all phases are done.
     */
    async finalizeIndexing(id) {
        const instance = this.projects.get(id);
        if (!instance)
            throw new Error(`Project "${id}" not found`);
        if (!instance.indexer)
            throw new Error(`Indexer not created for "${id}". Call ensureIndexer() first.`);
        // Full drain with finalize (rebuildDirectoryStats, resolvePendingLinks, etc.)
        try {
            await instance.indexer.drain();
        }
        catch (err) {
            log.error({ project: id, err }, 'Error during finalize drain');
        }
        // Start watcher for live re-indexing
        if (!instance.watcher) {
            instance.watcher = instance.indexer.watch();
            await instance.watcher.whenReady;
        }
        // Save after initial scan
        this.saveProject(instance);
        instance.dirty = false;
        // Scan and watch .notes/ and .tasks/ for reverse import (skip for workspace projects — handled by workspace)
        // Skip mirror entirely if knowledge or tasks graph is readonly (mirror requires both)
        if (instance.mirrorTracker && !instance.workspaceId && instance.knowledgeManager && instance.taskManager) {
            const gc = instance.config.graphConfigs;
            if (!gc.knowledge.readonly && !gc.tasks.readonly) {
                const mirrorConfig = {
                    projectDir: instance.config.projectDir,
                    knowledgeManager: instance.knowledgeManager,
                    taskManager: instance.taskManager,
                    skillManager: gc.skills.readonly ? undefined : instance.skillManager,
                    mutationQueue: instance.mutationQueue,
                    tracker: instance.mirrorTracker,
                };
                await (0, mirror_watcher_1.scanMirrorDirs)(mirrorConfig);
                instance.mirrorWatcher = (0, mirror_watcher_1.startMirrorWatcher)(mirrorConfig);
            }
        }
        this.emit('project:indexed', { projectId: id });
        log.info({ project: id }, 'Project indexed');
    }
    /**
     * Remove a project: drain indexer, save graphs, close watcher.
     */
    async removeProject(id) {
        const instance = this.projects.get(id);
        if (!instance)
            return;
        if (instance.mirrorWatcher)
            await instance.mirrorWatcher.close();
        if (instance.watcher)
            await instance.watcher.close();
        if (instance.indexer)
            await instance.indexer.drain();
        if (instance.mcpClientCleanup)
            await instance.mcpClientCleanup();
        this.saveProject(instance);
        // Clean up workspace shared graphs: remove projectGraphs reference and orphaned proxies
        if (instance.workspaceId) {
            const ws = this.workspaces.get(instance.workspaceId);
            if (ws) {
                ws.knowledgeManager.externalGraphs?.projectGraphs?.delete(id);
                // Remove orphaned proxy nodes that reference this project
                for (const graph of [ws.knowledgeManager.graph, ws.taskManager?.graph, ws.skillManager?.graph]) {
                    if (!graph)
                        continue;
                    const toRemove = [];
                    graph.forEachNode((nodeId, attrs) => {
                        if (attrs.proxyFor?.projectId === id) {
                            toRemove.push(nodeId);
                        }
                        else if (attrs.proxyFor && !attrs.proxyFor.projectId && graph.degree(nodeId) === 0) {
                            // Legacy proxy (no projectId) — clean up if orphaned (zero edges)
                            toRemove.push(nodeId);
                        }
                    });
                    for (const nodeId of toRemove)
                        graph.dropNode(nodeId);
                }
            }
        }
        this.projects.delete(id);
        log.info({ project: id }, 'Removed project');
    }
    getProject(id) {
        return this.projects.get(id);
    }
    listProjects() {
        return Array.from(this.projects.keys());
    }
    markDirty(id) {
        const instance = this.projects.get(id);
        if (instance)
            instance.dirty = true;
    }
    /**
     * Start auto-save interval (every intervalMs, save dirty projects).
     * Safe despite running outside PromiseQueue: graph.export() and
     * JSON.stringify are synchronous, so the event loop won't interleave
     * them with async mutations (which yield at await points).
     */
    startAutoSave(intervalMs = defaults_1.AUTO_SAVE_INTERVAL_MS) {
        this.autoSaveInterval = setInterval(() => {
            for (const instance of this.projects.values()) {
                if (instance.dirty) {
                    try {
                        this.saveProject(instance);
                        instance.dirty = false;
                    }
                    catch (err) {
                        log.error({ project: instance.id, err }, 'Auto-save error');
                    }
                }
            }
            for (const ws of this.workspaces.values()) {
                if (ws.dirty) {
                    try {
                        this.saveWorkspace(ws);
                        ws.dirty = false;
                    }
                    catch (err) {
                        log.error({ workspace: ws.id, err }, 'Auto-save error for workspace');
                    }
                }
            }
        }, intervalMs);
        this.autoSaveInterval.unref();
    }
    /**
     * Save all projects and shut down.
     */
    async shutdown() {
        if (this.autoSaveInterval)
            clearInterval(this.autoSaveInterval);
        for (const instance of this.projects.values()) {
            try {
                if (instance.mirrorWatcher)
                    await instance.mirrorWatcher.close();
                if (instance.watcher)
                    await instance.watcher.close();
                if (instance.indexer)
                    await instance.indexer.drain();
                if (instance.mcpClientCleanup)
                    await instance.mcpClientCleanup();
                await instance.mutationQueue.waitForPending();
                this.saveProject(instance);
            }
            catch (err) {
                log.error({ project: instance.id, err }, 'Shutdown error');
            }
        }
        for (const ws of this.workspaces.values()) {
            try {
                if (ws.mirrorWatcher)
                    await ws.mirrorWatcher.close();
                await ws.mutationQueue.waitForPending();
                this.saveWorkspace(ws);
            }
            catch (err) {
                log.error({ workspace: ws.id, err }, 'Shutdown error for workspace');
            }
        }
        this.projects.clear();
        this.workspaces.clear();
        log.info('Shutdown complete');
    }
    // ---------------------------------------------------------------------------
    // Private helpers
    // ---------------------------------------------------------------------------
    saveProject(instance) {
        const gc = instance.config.graphConfigs;
        if (instance.docGraph)
            (0, docs_1.saveGraph)(instance.docGraph, instance.config.graphMemory, (0, multi_config_1.embeddingFingerprint)(gc.docs.model));
        if (instance.codeGraph)
            (0, code_1.saveCodeGraph)(instance.codeGraph, instance.config.graphMemory, (0, multi_config_1.embeddingFingerprint)(gc.code.model));
        if (instance.fileIndexGraph)
            (0, file_index_1.saveFileIndexGraph)(instance.fileIndexGraph, instance.config.graphMemory, (0, multi_config_1.embeddingFingerprint)(gc.files.model));
        // Skip knowledge/tasks/skills for workspace projects (saved by workspace)
        if (!instance.workspaceId) {
            if (instance.knowledgeGraph)
                (0, knowledge_1.saveKnowledgeGraph)(instance.knowledgeGraph, instance.config.graphMemory, (0, multi_config_1.embeddingFingerprint)(gc.knowledge.model));
            if (instance.taskGraph)
                (0, task_1.saveTaskGraph)(instance.taskGraph, instance.config.graphMemory, (0, multi_config_1.embeddingFingerprint)(gc.tasks.model));
            if (instance.skillGraph)
                (0, skill_1.saveSkillGraph)(instance.skillGraph, instance.config.graphMemory, (0, multi_config_1.embeddingFingerprint)(gc.skills.model));
        }
    }
    saveWorkspace(ws) {
        const gc = ws.config.graphConfigs;
        (0, knowledge_1.saveKnowledgeGraph)(ws.knowledgeGraph, ws.config.graphMemory, (0, multi_config_1.embeddingFingerprint)(gc.knowledge.model));
        (0, task_1.saveTaskGraph)(ws.taskGraph, ws.config.graphMemory, (0, multi_config_1.embeddingFingerprint)(gc.tasks.model));
        (0, skill_1.saveSkillGraph)(ws.skillGraph, ws.config.graphMemory, (0, multi_config_1.embeddingFingerprint)(gc.skills.model));
    }
    buildEmbedFns(projectId) {
        const pair = (gn) => ({
            document: (q) => (0, embedder_1.embed)(q, '', `${projectId}:${gn}`),
            query: (q) => (0, embedder_1.embedQuery)(q, `${projectId}:${gn}`),
        });
        return {
            docs: pair('docs'), code: pair('code'), knowledge: pair('knowledge'),
            tasks: pair('tasks'), files: pair('files'), skills: pair('skills'),
        };
    }
}
exports.ProjectManager = ProjectManager;
