"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.GRAPH_NAMES = void 0;
exports.embeddingFingerprint = embeddingFingerprint;
exports.formatAuthor = formatAuthor;
exports.loadMultiConfig = loadMultiConfig;
exports.defaultConfig = defaultConfig;
const fs_1 = __importDefault(require("fs"));
const os_1 = __importDefault(require("os"));
const path_1 = __importDefault(require("path"));
const yaml_1 = require("yaml");
const zod_1 = require("zod");
const redis_1 = require("../lib/redis");
const HOME = os_1.default.homedir();
// ---------------------------------------------------------------------------
// Zod schemas
// ---------------------------------------------------------------------------
const authorSchema = zod_1.z.object({
    name: zod_1.z.string(),
    email: zod_1.z.string(),
});
// Model config: taken as a whole from the first level that defines it (no field merge)
const modelConfigSchema = zod_1.z.object({
    name: zod_1.z.string(),
    pooling: zod_1.z.enum(['mean', 'cls']).optional(),
    normalize: zod_1.z.boolean().optional(),
    dtype: zod_1.z.string().optional(),
    queryPrefix: zod_1.z.string().optional(),
    documentPrefix: zod_1.z.string().optional(),
});
// Embedding config: each field individually inherits up the chain
const embeddingConfigSchema = zod_1.z.object({
    batchSize: zod_1.z.number().int().positive().optional(),
    maxChars: zod_1.z.number().int().positive().optional(),
    cacheSize: zod_1.z.number().int().min(0).optional(),
    remote: zod_1.z.string().optional(), // Remote embedding API URL
    remoteApiKey: zod_1.z.string().optional(), // API key for remote embedding
    remoteModel: zod_1.z.enum(['default', 'code']).optional(), // Which model to request from remote API
});
const accessLevelSchema = zod_1.z.enum(['deny', 'r', 'rw']);
const accessMapSchema = zod_1.z.record(zod_1.z.string(), accessLevelSchema).optional();
// Exclude accepts string ("a,b") or array (["a", "b"]) in YAML
const excludeSchema = zod_1.z.union([zod_1.z.string(), zod_1.z.array(zod_1.z.string())]).optional();
// Include accepts string ("a/**/*.md") or array (["a/**/*.md", "CLAUDE.md"]) in YAML
const includeSchema = zod_1.z.union([zod_1.z.string(), zod_1.z.array(zod_1.z.string())]).optional();
const userSchema = zod_1.z.object({
    name: zod_1.z.string(),
    email: zod_1.z.string(),
    apiKey: zod_1.z.string(),
    passwordHash: zod_1.z.string().optional(),
});
const graphConfigSchema = zod_1.z.object({
    enabled: zod_1.z.boolean().optional(),
    readonly: zod_1.z.boolean().optional(),
    include: includeSchema,
    exclude: excludeSchema,
    model: modelConfigSchema.optional(),
    embedding: embeddingConfigSchema.optional(),
    access: accessMapSchema,
});
const graphsConfigSchema = zod_1.z.object({
    docs: graphConfigSchema.optional(),
    code: graphConfigSchema.optional(),
    knowledge: graphConfigSchema.optional(),
    tasks: graphConfigSchema.optional(),
    files: graphConfigSchema.optional(),
    skills: graphConfigSchema.optional(),
});
const projectSchema = zod_1.z.object({
    projectDir: zod_1.z.string(),
    description: zod_1.z.string().optional(),
    graphMemory: zod_1.z.string().optional(),
    exclude: excludeSchema,
    chunkDepth: zod_1.z.number().int().positive().optional(),
    maxFileSize: zod_1.z.number().int().positive().optional(),
    model: modelConfigSchema.optional(),
    codeModel: modelConfigSchema.optional(),
    embedding: embeddingConfigSchema.optional(),
    graphs: graphsConfigSchema.optional(),
    author: authorSchema.optional(),
    access: accessMapSchema,
});
const embeddingApiSchema = zod_1.z.object({
    enabled: zod_1.z.boolean().optional(),
    apiKey: zod_1.z.string().optional(),
    maxTexts: zod_1.z.number().int().positive().optional(), // max texts per request
    maxTextChars: zod_1.z.number().int().positive().optional(), // max chars per text
});
const rateLimitSchema = zod_1.z.object({
    global: zod_1.z.number().int().min(0).optional(), // req/min per IP (0 = disabled)
    search: zod_1.z.number().int().min(0).optional(), // req/min per IP for search/embed
    auth: zod_1.z.number().int().min(0).optional(), // req/min per IP for login
});
const oauthSchema = zod_1.z.object({
    enabled: zod_1.z.boolean().optional(),
    accessTokenTtl: zod_1.z.string().optional(),
    refreshTokenTtl: zod_1.z.string().optional(),
    authCodeTtl: zod_1.z.string().optional(),
});
const redisSchema = zod_1.z.object({
    enabled: zod_1.z.boolean().optional(),
    url: zod_1.z.string().optional(),
    prefix: zod_1.z.string().optional(),
    embeddingCacheTtl: zod_1.z.string().optional(),
});
const serverSchema = zod_1.z.object({
    host: zod_1.z.string().optional(),
    port: zod_1.z.number().int().positive().optional(),
    sessionTimeout: zod_1.z.number().int().positive().optional(),
    modelsDir: zod_1.z.string().optional(),
    corsOrigins: zod_1.z.array(zod_1.z.string()).optional(),
    model: modelConfigSchema.optional(),
    codeModel: modelConfigSchema.optional(),
    embedding: embeddingConfigSchema.optional(),
    embeddingApi: embeddingApiSchema.optional(),
    defaultAccess: accessLevelSchema.optional(),
    access: accessMapSchema,
    jwtSecret: zod_1.z.string().min(32).optional(),
    cookieSecure: zod_1.z.boolean().optional(),
    accessTokenTtl: zod_1.z.string().optional(),
    refreshTokenTtl: zod_1.z.string().optional(),
    rateLimit: rateLimitSchema.optional(),
    maxFileSize: zod_1.z.number().int().positive().optional(),
    exclude: excludeSchema,
    redis: redisSchema.optional(),
    oauth: oauthSchema.optional(),
});
const wsGraphConfigSchema = zod_1.z.object({
    enabled: zod_1.z.boolean().optional(),
    readonly: zod_1.z.boolean().optional(),
    exclude: excludeSchema,
    model: modelConfigSchema.optional(),
    embedding: embeddingConfigSchema.optional(),
    access: accessMapSchema,
});
const wsGraphsConfigSchema = zod_1.z.object({
    knowledge: wsGraphConfigSchema.optional(),
    tasks: wsGraphConfigSchema.optional(),
    skills: wsGraphConfigSchema.optional(),
});
const workspaceSchema = zod_1.z.object({
    projects: zod_1.z.array(zod_1.z.string()),
    graphMemory: zod_1.z.string().optional(),
    mirrorDir: zod_1.z.string().optional(),
    model: modelConfigSchema.optional(),
    codeModel: modelConfigSchema.optional(),
    embedding: embeddingConfigSchema.optional(),
    graphs: wsGraphsConfigSchema.optional(),
    author: authorSchema.optional(),
    access: accessMapSchema,
    maxFileSize: zod_1.z.number().int().positive().optional(),
    exclude: excludeSchema,
});
const configFileSchema = zod_1.z.object({
    author: authorSchema.optional(),
    server: serverSchema.optional(),
    users: zod_1.z.record(zod_1.z.string(), userSchema).optional(),
    projects: zod_1.z.record(zod_1.z.string(), projectSchema),
    workspaces: zod_1.z.record(zod_1.z.string(), workspaceSchema).optional(),
});
exports.GRAPH_NAMES = ['docs', 'code', 'knowledge', 'tasks', 'files', 'skills'];
/**
 * Build a stable fingerprint string from model config fields that affect stored vectors.
 * queryPrefix excluded: only affects query-time, not stored document vectors.
 */
function embeddingFingerprint(model) {
    return `${model.name}|${model.pooling}|${model.normalize}|${model.dtype ?? ''}|${model.documentPrefix}`;
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
const AUTHOR_DEFAULT = { name: '', email: '' };
/**
 * Format an author as a git-style string: "Name <email>".
 * Returns empty string if name is not set.
 */
function formatAuthor(author) {
    if (!author.name)
        return '';
    return `${author.name} <${author.email}>`;
}
// ---------------------------------------------------------------------------
// Defaults
// ---------------------------------------------------------------------------
const MODEL_DEFAULTS = {
    name: 'Xenova/bge-m3',
    pooling: 'cls',
    normalize: true,
    dtype: 'q8',
    queryPrefix: '',
    documentPrefix: '',
};
const CODE_MODEL_DEFAULTS = {
    name: 'jinaai/jina-embeddings-v2-base-code',
    pooling: 'mean',
    normalize: true,
    dtype: 'q8',
    queryPrefix: '',
    documentPrefix: '',
};
const EMBEDDING_DEFAULTS = {
    batchSize: 1,
    maxChars: 24_000,
    cacheSize: 10_000,
};
const OAUTH_DEFAULTS = {
    enabled: false,
    accessTokenTtl: '1h',
    refreshTokenTtl: '7d',
    authCodeTtl: '10m',
};
const RATE_LIMIT_DEFAULTS = {
    global: 600,
    search: 120,
    auth: 10,
};
const SERVER_DEFAULTS = {
    host: '127.0.0.1',
    port: 3000,
    sessionTimeout: 3600,
    modelsDir: path_1.default.join(HOME, '.graph-memory/models'),
    model: MODEL_DEFAULTS,
    codeModel: CODE_MODEL_DEFAULTS,
    embedding: EMBEDDING_DEFAULTS,
    defaultAccess: 'rw',
    accessTokenTtl: '15m',
    refreshTokenTtl: '7d',
    rateLimit: RATE_LIMIT_DEFAULTS,
    maxFileSize: 1 * 1024 * 1024, // 1 MB
    exclude: ['**/node_modules/**', '**/dist/**'],
    redis: { ...redis_1.REDIS_DEFAULTS },
    oauth: { ...OAUTH_DEFAULTS },
};
const PROJECT_DEFAULTS = {
    docsInclude: '**/*.md',
    codeInclude: '**/*.{js,ts,jsx,tsx,mjs,mts,cjs,cts}',
    chunkDepth: 4,
};
/** Parse comma-separated exclude string into array of patterns. */
function parseExclude(raw) {
    if (!raw)
        return [];
    if (Array.isArray(raw))
        return raw.map(p => p.trim()).filter(Boolean);
    return raw.split(',').map(p => p.trim()).filter(Boolean);
}
/** Normalize include: array stays as array, string stays as string, undefined stays undefined. */
function parseInclude(raw) {
    if (!raw)
        return undefined;
    if (Array.isArray(raw))
        return raw.map(p => p.trim()).filter(Boolean);
    return raw;
}
// ---------------------------------------------------------------------------
// Resolution
// ---------------------------------------------------------------------------
/**
 * Resolve ModelConfig: take the whole object from the first level that defines it.
 * If undefined, returns parent (next level up the chain).
 */
function resolveModel(raw, parent) {
    if (!raw)
        return parent;
    return {
        name: raw.name,
        pooling: raw.pooling ?? MODEL_DEFAULTS.pooling,
        normalize: raw.normalize ?? MODEL_DEFAULTS.normalize,
        dtype: raw.dtype,
        queryPrefix: raw.queryPrefix ?? MODEL_DEFAULTS.queryPrefix,
        documentPrefix: raw.documentPrefix ?? MODEL_DEFAULTS.documentPrefix,
    };
}
/**
 * Resolve EmbeddingConfig: each field individually inherits up the chain.
 */
function resolveEmbedding(raw, parent) {
    if (!raw)
        return parent;
    return {
        batchSize: raw.batchSize ?? parent.batchSize,
        maxChars: raw.maxChars ?? parent.maxChars,
        cacheSize: raw.cacheSize ?? parent.cacheSize,
        remote: raw.remote ?? parent.remote,
        remoteApiKey: raw.remoteApiKey ?? parent.remoteApiKey,
        remoteModel: raw.remoteModel ?? parent.remoteModel,
    };
}
// ---------------------------------------------------------------------------
// Load
// ---------------------------------------------------------------------------
/**
 * Load and validate a `graph-memory.yaml` config file.
 * Resolves all paths to absolute, applies defaults.
 */
function loadMultiConfig(yamlPath) {
    const raw = fs_1.default.readFileSync(yamlPath, 'utf-8');
    const parsed = (0, yaml_1.parse)(raw);
    const validated = configFileSchema.parse(parsed);
    const srv = validated.server ?? {};
    const globalAuthor = validated.author ?? AUTHOR_DEFAULT;
    // Server-level: model + codeModel + embedding
    const serverModel = resolveModel(srv.model, MODEL_DEFAULTS);
    const serverCodeModel = resolveModel(srv.codeModel, CODE_MODEL_DEFAULTS);
    const serverEmbedding = resolveEmbedding(srv.embedding, EMBEDDING_DEFAULTS);
    const server = {
        host: srv.host ?? SERVER_DEFAULTS.host,
        port: srv.port ?? SERVER_DEFAULTS.port,
        sessionTimeout: srv.sessionTimeout ?? SERVER_DEFAULTS.sessionTimeout,
        modelsDir: path_1.default.resolve(srv.modelsDir ?? SERVER_DEFAULTS.modelsDir),
        corsOrigins: srv.corsOrigins,
        model: serverModel,
        codeModel: serverCodeModel,
        embedding: serverEmbedding,
        embeddingApi: srv.embeddingApi ? { enabled: !!srv.embeddingApi.enabled, apiKey: srv.embeddingApi.apiKey, maxTexts: srv.embeddingApi.maxTexts ?? 100, maxTextChars: srv.embeddingApi.maxTextChars ?? 10_000 } : undefined,
        defaultAccess: srv.defaultAccess ?? SERVER_DEFAULTS.defaultAccess,
        access: srv.access ?? undefined,
        jwtSecret: srv.jwtSecret,
        cookieSecure: srv.cookieSecure,
        accessTokenTtl: srv.accessTokenTtl ?? SERVER_DEFAULTS.accessTokenTtl,
        refreshTokenTtl: srv.refreshTokenTtl ?? SERVER_DEFAULTS.refreshTokenTtl,
        rateLimit: {
            global: srv.rateLimit?.global ?? RATE_LIMIT_DEFAULTS.global,
            search: srv.rateLimit?.search ?? RATE_LIMIT_DEFAULTS.search,
            auth: srv.rateLimit?.auth ?? RATE_LIMIT_DEFAULTS.auth,
        },
        maxFileSize: srv.maxFileSize ?? SERVER_DEFAULTS.maxFileSize,
        exclude: [...SERVER_DEFAULTS.exclude, ...parseExclude(srv.exclude)],
        redis: {
            enabled: srv.redis?.enabled ?? redis_1.REDIS_DEFAULTS.enabled,
            url: srv.redis?.url ?? redis_1.REDIS_DEFAULTS.url,
            prefix: srv.redis?.prefix ?? redis_1.REDIS_DEFAULTS.prefix,
            embeddingCacheTtl: srv.redis?.embeddingCacheTtl ?? redis_1.REDIS_DEFAULTS.embeddingCacheTtl,
        },
        oauth: {
            enabled: srv.oauth?.enabled ?? OAUTH_DEFAULTS.enabled,
            accessTokenTtl: srv.oauth?.accessTokenTtl ?? OAUTH_DEFAULTS.accessTokenTtl,
            refreshTokenTtl: srv.oauth?.refreshTokenTtl ?? OAUTH_DEFAULTS.refreshTokenTtl,
            authCodeTtl: srv.oauth?.authCodeTtl ?? OAUTH_DEFAULTS.authCodeTtl,
        },
    };
    // Users
    const users = {};
    if (validated.users) {
        for (const [id, raw] of Object.entries(validated.users)) {
            users[id] = { name: raw.name, email: raw.email, apiKey: raw.apiKey, passwordHash: raw.passwordHash };
        }
    }
    const projects = new Map();
    for (const [id, raw] of Object.entries(validated.projects)) {
        const projectDir = path_1.default.resolve(raw.projectDir);
        const graphMemory = raw.graphMemory
            ? path_1.default.resolve(projectDir, raw.graphMemory)
            : path_1.default.join(projectDir, '.graph-memory');
        const projectModel = resolveModel(raw.model, serverModel);
        const projectCodeModel = resolveModel(raw.codeModel, serverCodeModel);
        const projectEmbedding = resolveEmbedding(raw.embedding, serverEmbedding);
        // Exclude accumulates: server + project
        const projectExclude = [...server.exclude, ...parseExclude(raw.exclude)];
        const rawGraphs = raw.graphs ?? {};
        const graphConfigs = {};
        for (const gn of exports.GRAPH_NAMES) {
            const gc = rawGraphs[gn];
            // Exclude accumulates: server + project + graph
            const graphExclude = [...projectExclude, ...parseExclude(gc?.exclude)];
            const graphEmbedding = resolveEmbedding(gc?.embedding, projectEmbedding);
            // Auto-set remoteModel for code graph when using remote embedding
            if (gn === 'code' && graphEmbedding.remote && !graphEmbedding.remoteModel) {
                graphEmbedding.remoteModel = 'code';
            }
            graphConfigs[gn] = {
                enabled: gc?.enabled ?? true,
                readonly: gc?.readonly ?? false,
                include: parseInclude(gc?.include) ?? (gn === 'docs' ? PROJECT_DEFAULTS.docsInclude : gn === 'code' ? PROJECT_DEFAULTS.codeInclude : undefined),
                exclude: graphExclude,
                model: resolveModel(gc?.model, gn === 'code' ? projectCodeModel : projectModel),
                embedding: graphEmbedding,
                access: gc?.access ?? undefined,
            };
        }
        projects.set(id, {
            projectDir,
            description: raw.description,
            graphMemory,
            exclude: projectExclude,
            chunkDepth: raw.chunkDepth ?? PROJECT_DEFAULTS.chunkDepth,
            maxFileSize: raw.maxFileSize ?? -1,
            model: projectModel,
            codeModel: projectCodeModel,
            embedding: projectEmbedding,
            graphConfigs,
            author: raw.author ?? globalAuthor,
            access: raw.access ?? undefined,
        });
    }
    // --- Workspaces ---
    const workspaces = new Map();
    if (validated.workspaces) {
        for (const [wsId, raw] of Object.entries(validated.workspaces)) {
            for (const projId of raw.projects) {
                if (!projects.has(projId)) {
                    throw new Error(`Workspace "${wsId}" references unknown project "${projId}"`);
                }
            }
            const firstProject = projects.get(raw.projects[0]);
            const graphMemory = raw.graphMemory
                ? path_1.default.resolve(raw.graphMemory)
                : path_1.default.join(firstProject.projectDir, '.graph-memory', 'workspace');
            const mirrorDir = raw.mirrorDir
                ? path_1.default.resolve(raw.mirrorDir)
                : graphMemory;
            const wsModel = resolveModel(raw.model, serverModel);
            const wsCodeModel = resolveModel(raw.codeModel, serverCodeModel);
            const wsEmbedding = resolveEmbedding(raw.embedding, serverEmbedding);
            // Exclude accumulates: server + workspace
            const wsExclude = [...server.exclude, ...parseExclude(raw.exclude)];
            const rawGraphs = raw.graphs ?? {};
            const WS_GRAPH_NAMES = ['knowledge', 'tasks', 'skills'];
            const graphConfigs = {};
            for (const gn of WS_GRAPH_NAMES) {
                const gc = rawGraphs[gn];
                graphConfigs[gn] = {
                    enabled: gc?.enabled ?? true,
                    readonly: gc?.readonly ?? false,
                    exclude: [...wsExclude, ...parseExclude(gc?.exclude)],
                    model: resolveModel(gc?.model, wsModel),
                    embedding: resolveEmbedding(gc?.embedding, wsEmbedding),
                    access: gc?.access ?? undefined,
                };
            }
            workspaces.set(wsId, {
                projects: raw.projects,
                graphMemory,
                mirrorDir,
                model: wsModel,
                codeModel: wsCodeModel,
                embedding: wsEmbedding,
                graphConfigs,
                author: raw.author ?? globalAuthor,
                access: raw.access ?? undefined,
                maxFileSize: raw.maxFileSize,
                exclude: wsExclude,
            });
        }
    }
    // --- Post-parse: inject workspace-level settings into member projects ---
    const wsForProject = new Map();
    for (const ws of workspaces.values()) {
        for (const pid of ws.projects)
            wsForProject.set(pid, ws);
    }
    for (const [pid, proj] of projects) {
        const ws = wsForProject.get(pid);
        // maxFileSize: project → workspace → server
        if (proj.maxFileSize === -1) {
            proj.maxFileSize = ws?.maxFileSize ?? server.maxFileSize;
        }
        // Inject workspace excludes into project + graph excludes
        if (ws) {
            const wsExtra = ws.exclude.filter(e => !server.exclude.includes(e)); // ws-only patterns
            if (wsExtra.length > 0) {
                proj.exclude = [...proj.exclude, ...wsExtra];
                for (const gn of exports.GRAPH_NAMES) {
                    proj.graphConfigs[gn].exclude = [...proj.graphConfigs[gn].exclude, ...wsExtra];
                }
            }
        }
    }
    return { author: globalAuthor, server, users, projects, workspaces };
}
/**
 * Build a default MultiConfig for a single project rooted at `projectDir`.
 * Used when no config file is found — zero-config startup.
 */
function defaultConfig(projectDir) {
    const absDir = path_1.default.resolve(projectDir);
    const id = path_1.default.basename(absDir);
    const server = {
        host: SERVER_DEFAULTS.host,
        port: SERVER_DEFAULTS.port,
        sessionTimeout: SERVER_DEFAULTS.sessionTimeout,
        modelsDir: path_1.default.resolve(SERVER_DEFAULTS.modelsDir),
        model: MODEL_DEFAULTS,
        codeModel: CODE_MODEL_DEFAULTS,
        embedding: EMBEDDING_DEFAULTS,
        defaultAccess: SERVER_DEFAULTS.defaultAccess,
        accessTokenTtl: SERVER_DEFAULTS.accessTokenTtl,
        refreshTokenTtl: SERVER_DEFAULTS.refreshTokenTtl,
        rateLimit: RATE_LIMIT_DEFAULTS,
        maxFileSize: SERVER_DEFAULTS.maxFileSize,
        exclude: [...SERVER_DEFAULTS.exclude],
        redis: { ...redis_1.REDIS_DEFAULTS },
        oauth: { ...OAUTH_DEFAULTS },
    };
    const graphConfigs = {};
    for (const gn of exports.GRAPH_NAMES) {
        graphConfigs[gn] = {
            enabled: true,
            readonly: false,
            include: gn === 'docs' ? PROJECT_DEFAULTS.docsInclude : gn === 'code' ? PROJECT_DEFAULTS.codeInclude : undefined,
            exclude: [...server.exclude],
            model: gn === 'code' ? CODE_MODEL_DEFAULTS : MODEL_DEFAULTS,
            embedding: EMBEDDING_DEFAULTS,
        };
    }
    const project = {
        projectDir: absDir,
        graphMemory: path_1.default.join(absDir, '.graph-memory'),
        exclude: [...server.exclude],
        chunkDepth: PROJECT_DEFAULTS.chunkDepth,
        maxFileSize: server.maxFileSize,
        model: MODEL_DEFAULTS,
        codeModel: CODE_MODEL_DEFAULTS,
        embedding: EMBEDDING_DEFAULTS,
        graphConfigs,
        author: AUTHOR_DEFAULT,
    };
    const projects = new Map();
    projects.set(id, project);
    return {
        author: AUTHOR_DEFAULT,
        server,
        users: {},
        projects,
        workspaces: new Map(),
    };
}
