"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.requireWriteAccess = requireWriteAccess;
exports.createRestApp = createRestApp;
const crypto_1 = __importDefault(require("crypto"));
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const express_1 = __importDefault(require("express"));
const cors_1 = __importDefault(require("cors"));
const cookie_parser_1 = __importDefault(require("cookie-parser"));
const express_rate_limit_1 = __importDefault(require("express-rate-limit"));
const logger_1 = require("../../lib/logger");
const log = (0, logger_1.createLogger)('rest');
const multi_config_1 = require("../../lib/multi-config");
const access_1 = require("../../lib/access");
const jwt_1 = require("../../lib/jwt");
const knowledge_1 = require("../../api/rest/knowledge");
const tasks_1 = require("../../api/rest/tasks");
const epics_1 = require("../../api/rest/epics");
const skills_1 = require("../../api/rest/skills");
const docs_1 = require("../../api/rest/docs");
const code_1 = require("../../api/rest/code");
const files_1 = require("../../api/rest/files");
const tools_1 = require("../../api/rest/tools");
const embed_1 = require("../../api/rest/embed");
const oauth_1 = require("../../api/rest/oauth");
const team_1 = require("../../lib/team");
const defaults_1 = require("../../lib/defaults");
/** Count real (non-proxy) nodes in a graph. */
function realNodeCount(graph) {
    let count = 0;
    graph.forEachNode((_id, attrs) => {
        if (!attrs.proxyFor)
            count++;
    });
    return count;
}
/**
 * Express middleware: reject if accessLevel (set by requireGraphAccess) is not 'rw'.
 * Use on POST/PUT/DELETE routes inside domain routers.
 */
function requireWriteAccess(req, res, next) {
    const level = req.accessLevel;
    if (level && level !== 'rw') {
        res.status(403).json({ error: 'Read-only access' });
        return;
    }
    next();
}
/**
 * Create an Express app with all REST routes mounted.
 * Each route uses the ProjectManager to look up project-specific graphs.
 */
function createRestApp(projectManager, options) {
    const app = (0, express_1.default)();
    app.set('trust proxy', 1);
    const serverConfig = options?.serverConfig;
    const users = options?.users ?? {};
    const hasUsers = Object.keys(users).length > 0;
    const corsOrigins = serverConfig?.corsOrigins;
    app.use((0, cors_1.default)(corsOrigins?.length ? { origin: corsOrigins, credentials: true } : { credentials: true }));
    app.use(express_1.default.json({ limit: '10mb' }));
    app.use((0, cookie_parser_1.default)());
    // Request ID tracking
    app.use((_req, res, next) => {
        const id = _req.headers['x-request-id'] || crypto_1.default.randomUUID();
        res.setHeader('X-Request-ID', id);
        _req.requestId = id;
        next();
    });
    // Security headers
    app.use((_req, res, next) => {
        res.setHeader('X-Content-Type-Options', 'nosniff');
        res.setHeader('X-Frame-Options', 'DENY');
        res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
        res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self' ws: wss:; frame-ancestors 'none'");
        if (serverConfig?.cookieSecure) {
            res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
        }
        next();
    });
    // --- Rate limiting ---
    const rl = serverConfig?.rateLimit;
    const rateLimitMsg = { error: 'Too many requests, please try again later' };
    if (rl && rl.global > 0) {
        app.use('/api/', (0, express_rate_limit_1.default)({ windowMs: defaults_1.RATE_LIMIT_WINDOW_MS, max: rl.global, standardHeaders: true, legacyHeaders: false, message: rateLimitMsg }));
    }
    if (rl && rl.search > 0) {
        const searchLimiter = (0, express_rate_limit_1.default)({ windowMs: defaults_1.RATE_LIMIT_WINDOW_MS, max: rl.search, standardHeaders: true, legacyHeaders: false, message: rateLimitMsg });
        app.use('/api/projects/:projectId/knowledge/search', searchLimiter);
        app.use('/api/projects/:projectId/tasks/search', searchLimiter);
        app.use('/api/projects/:projectId/skills/search', searchLimiter);
        app.use('/api/projects/:projectId/docs/search', searchLimiter);
        app.use('/api/projects/:projectId/code/search', searchLimiter);
        app.use('/api/projects/:projectId/files/search', searchLimiter);
        app.use('/api/embed', searchLimiter);
    }
    if (rl && rl.auth > 0) {
        const authLimiter = (0, express_rate_limit_1.default)({ windowMs: defaults_1.RATE_LIMIT_WINDOW_MS, max: rl.auth, standardHeaders: true, legacyHeaders: false, message: rateLimitMsg });
        app.use('/api/auth/login', authLimiter);
        app.use('/api/oauth/token', authLimiter);
    }
    const jwtSecret = serverConfig?.jwtSecret;
    const cookieSecure = serverConfig?.cookieSecure;
    const accessTokenTtl = serverConfig?.accessTokenTtl ?? '15m';
    const refreshTokenTtl = serverConfig?.refreshTokenTtl ?? '7d';
    // --- Auth endpoints (before auth middleware — always accessible) ---
    // Auth status: check cookie JWT or Bearer apiKey
    app.get('/api/auth/status', (req, res) => {
        if (!hasUsers)
            return res.json({ required: false, authenticated: false });
        // 1. Cookie JWT
        if (jwtSecret) {
            const accessToken = (0, jwt_1.getAccessToken)(req);
            if (accessToken) {
                const payload = (0, jwt_1.verifyToken)(accessToken, jwtSecret);
                if (payload?.type === 'access' && users[payload.userId]) {
                    const user = users[payload.userId];
                    return res.json({ required: true, authenticated: true, userId: payload.userId, name: user.name });
                }
            }
        }
        // 2. Bearer apiKey
        const auth = req.headers.authorization;
        if (auth?.startsWith('Bearer ')) {
            const result = (0, access_1.resolveUserFromApiKey)(auth.slice(7), users);
            if (result)
                return res.json({ required: true, authenticated: true, userId: result.userId, name: result.user.name });
        }
        return res.json({ required: true, authenticated: false });
    });
    // API key retrieval: requires valid JWT cookie (not exposed in /status)
    app.get('/api/auth/apikey', (req, res) => {
        if (!hasUsers || !jwtSecret) {
            return res.status(400).json({ error: 'Authentication not configured' });
        }
        const accessToken = (0, jwt_1.getAccessToken)(req);
        if (!accessToken) {
            return res.status(401).json({ error: 'Authentication required' });
        }
        const payload = (0, jwt_1.verifyToken)(accessToken, jwtSecret);
        if (!payload || payload.type !== 'access' || !users[payload.userId]) {
            return res.status(401).json({ error: 'Invalid or expired token' });
        }
        const user = users[payload.userId];
        res.json({ apiKey: user.apiKey ?? null });
    });
    // Login: email + password → set JWT cookies
    app.post('/api/auth/login', async (req, res) => {
        if (!hasUsers || !jwtSecret) {
            return res.status(400).json({ error: 'Authentication not configured' });
        }
        const { email, password } = req.body ?? {};
        if (!email || !password) {
            return res.status(400).json({ error: 'Email and password are required' });
        }
        const result = (0, jwt_1.resolveUserByEmail)(email, users);
        if (!result || !result.user.passwordHash) {
            return res.status(401).json({ error: 'Invalid credentials' });
        }
        const valid = await (0, jwt_1.verifyPassword)(password, result.user.passwordHash);
        if (!valid) {
            return res.status(401).json({ error: 'Invalid credentials' });
        }
        const accessToken = (0, jwt_1.signAccessToken)(result.userId, jwtSecret, accessTokenTtl);
        const refreshToken = (0, jwt_1.signRefreshToken)(result.userId, jwtSecret, refreshTokenTtl);
        (0, jwt_1.setAuthCookies)(res, accessToken, refreshToken, refreshTokenTtl, cookieSecure);
        res.json({ userId: result.userId, name: result.user.name });
    });
    // Refresh: refresh cookie → new access cookie
    app.post('/api/auth/refresh', (req, res) => {
        if (!hasUsers || !jwtSecret) {
            return res.status(400).json({ error: 'Authentication not configured' });
        }
        const refreshToken = (0, jwt_1.getRefreshToken)(req);
        if (!refreshToken) {
            return res.status(401).json({ error: 'No refresh token' });
        }
        const payload = (0, jwt_1.verifyToken)(refreshToken, jwtSecret);
        if (!payload || payload.type !== 'refresh') {
            return res.status(401).json({ error: 'Invalid refresh token' });
        }
        // Check user still exists
        if (!users[payload.userId]) {
            (0, jwt_1.clearAuthCookies)(res);
            return res.status(401).json({ error: 'User no longer exists' });
        }
        const newAccessToken = (0, jwt_1.signAccessToken)(payload.userId, jwtSecret, accessTokenTtl);
        const newRefreshToken = (0, jwt_1.signRefreshToken)(payload.userId, jwtSecret, refreshTokenTtl);
        (0, jwt_1.setAuthCookies)(res, newAccessToken, newRefreshToken, refreshTokenTtl, cookieSecure);
        res.json({ userId: payload.userId, name: users[payload.userId].name });
    });
    // Logout: clear cookies
    app.post('/api/auth/logout', (_req, res) => {
        (0, jwt_1.clearAuthCookies)(res);
        res.json({ ok: true });
    });
    // --- OAuth 2.0 endpoints (before auth middleware — unauthenticated) ---
    if (serverConfig?.oauth?.enabled !== false) {
        app.use('/', (0, oauth_1.createOAuthRouter)(users, serverConfig, options?.sessionStore));
    }
    // --- Auth middleware: cookie JWT → Bearer apiKey → anonymous ---
    if (hasUsers) {
        app.use('/api/', (req, _res, next) => {
            // 1. Cookie JWT (from UI login)
            let hasExpiredCookie = false;
            if (jwtSecret) {
                const accessToken = (0, jwt_1.getAccessToken)(req);
                if (accessToken) {
                    const payload = (0, jwt_1.verifyToken)(accessToken, jwtSecret);
                    if (payload?.type === 'access' && users[payload.userId]) {
                        req.userId = payload.userId;
                        req.user = users[payload.userId];
                        return next();
                    }
                    // Invalid/expired JWT cookie — try Bearer, otherwise 401
                    hasExpiredCookie = true;
                }
            }
            // 2. Bearer apiKey (from MCP/API clients)
            const auth = req.headers.authorization;
            if (auth?.startsWith('Bearer ') && auth.length > 7) {
                const apiKey = auth.slice(7);
                const result = (0, access_1.resolveUserFromApiKey)(apiKey, users);
                if (result) {
                    req.userId = result.userId;
                    req.user = result.user;
                    return next();
                }
                // Invalid Bearer token — reject (explicit auth attempt should not fall through)
                return _res.status(401).json({ error: 'Invalid API key' });
            }
            // 3. No valid credentials — reject when users are configured
            if (hasExpiredCookie) {
                return _res.status(401).json({ error: 'Token expired' });
            }
            return _res.status(401).json({ error: 'Authentication required' });
        });
    }
    // Project resolution middleware — injects project instance into req
    app.param('projectId', (req, _res, next, projectId) => {
        const project = projectManager.getProject(projectId);
        if (!project) {
            return _res.status(404).json({ error: `Project "${projectId}" not found` });
        }
        req.project = project;
        next();
    });
    // List projects
    app.get('/api/projects', (_req, res) => {
        const userId = _req.userId;
        const projects = projectManager.listProjects().map(id => {
            const p = projectManager.getProject(id);
            const gc = p.config.graphConfigs;
            const ws = p.workspaceId ? projectManager.getWorkspace(p.workspaceId) : undefined;
            // Per-graph info: enabled, readonly, access level for current user
            // Hide graphs the user cannot read — don't leak enabled/readonly/stats
            const graphs = {};
            const stats = {};
            const graphInstances = {
                docs: p.docGraph, code: p.codeGraph, knowledge: p.knowledgeGraph,
                files: p.fileIndexGraph, tasks: p.taskGraph, skills: p.skillGraph,
            };
            for (const gn of multi_config_1.GRAPH_NAMES) {
                let access = serverConfig
                    ? (0, access_1.resolveAccess)(userId, gn, p.config, serverConfig, ws?.config)
                    : 'rw';
                // Cap access for readonly graphs
                if (access === 'rw' && gc[gn].readonly)
                    access = 'r';
                if (!gc[gn].enabled) {
                    graphs[gn] = { enabled: false, readonly: gc[gn].readonly, access: null };
                    stats[gn] = 0;
                }
                else if ((0, access_1.canRead)(access)) {
                    graphs[gn] = { enabled: true, readonly: gc[gn].readonly, access };
                    stats[gn] = graphInstances[gn] ? realNodeCount(graphInstances[gn]) : 0;
                }
                else {
                    graphs[gn] = { enabled: true, readonly: gc[gn].readonly, access: 'deny' };
                    stats[gn] = 0;
                }
            }
            return {
                id,
                workspaceId: p.workspaceId ?? null,
                graphs,
                stats,
            };
        }).filter(p => {
            // Hide projects where the user has no read access to any enabled graph
            return multi_config_1.GRAPH_NAMES.some(gn => {
                const g = p.graphs[gn];
                return g && g.access !== null && (0, access_1.canRead)(g.access);
            });
        });
        res.json({ results: projects });
    });
    // List workspaces — only return workspaces where the user has access to at least one project
    app.get('/api/workspaces', (_req, res) => {
        const userId = _req.userId;
        const workspaces = projectManager.listWorkspaces().map(id => {
            const ws = projectManager.getWorkspace(id);
            // Filter projects within workspace by user access
            const accessibleProjects = ws.config.projects.filter(projectId => {
                const p = projectManager.getProject(projectId);
                if (!p)
                    return false;
                return multi_config_1.GRAPH_NAMES.some(gn => {
                    const access = serverConfig
                        ? (0, access_1.resolveAccess)(userId, gn, p.config, serverConfig, ws.config)
                        : 'rw';
                    return (0, access_1.canRead)(access);
                });
            });
            return { id, projects: accessibleProjects };
        }).filter(ws => ws.projects.length > 0);
        res.json({ results: workspaces });
    });
    // Project stats — only return stats for graphs the user can read
    app.get('/api/projects/:projectId/stats', (req, res) => {
        const p = req.project;
        const userId = req.userId;
        const ws = p.workspaceId ? projectManager.getWorkspace(p.workspaceId) : undefined;
        const graphData = {
            docs: { graph: p.docGraph, key: 'docs' },
            code: { graph: p.codeGraph, key: 'code' },
            knowledge: { graph: p.knowledgeGraph, key: 'knowledge' },
            fileIndex: { graph: p.fileIndexGraph, key: 'files' },
            tasks: { graph: p.taskGraph, key: 'tasks' },
            skills: { graph: p.skillGraph, key: 'skills' },
        };
        const result = {};
        for (const [outKey, { graph, key: graphName }] of Object.entries(graphData)) {
            const access = serverConfig
                ? (0, access_1.resolveAccess)(userId, graphName, p.config, serverConfig, ws?.config)
                : 'rw';
            if (!(0, access_1.canRead)(access)) {
                result[outKey] = null;
            }
            else {
                result[outKey] = graph ? { nodes: realNodeCount(graph), edges: graph.size } : null;
            }
        }
        res.json(result);
    });
    // Team members (workspace: shared .team/ in mirrorDir; standalone: .team/ in projectDir)
    // Requires at least read access to any graph in the project
    app.get('/api/projects/:projectId/team', (req, res) => {
        if (serverConfig && hasUsers) {
            const userId = req.userId;
            if (!userId)
                return res.status(401).json({ error: 'Authentication required' });
            const p = req.project;
            const ws = p.workspaceId ? projectManager.getWorkspace(p.workspaceId) : undefined;
            const hasAnyAccess = multi_config_1.GRAPH_NAMES.some(gn => (0, access_1.canRead)((0, access_1.resolveAccess)(userId, gn, p.config, serverConfig, ws?.config)));
            if (!hasAnyAccess)
                return res.status(403).json({ error: 'Access denied' });
        }
        const p = req.project;
        const ws = p.workspaceId ? projectManager.getWorkspace(p.workspaceId) : undefined;
        const baseDir = ws ? ws.config.mirrorDir : p.config.projectDir;
        const members = (0, team_1.scanTeamDir)(path_1.default.join(baseDir, '.team'));
        res.json({ results: members });
    });
    // Middleware: require a specific manager to be enabled, or return 404
    function requireManager(managerKey) {
        return (req, _res, next) => {
            const p = req.project;
            if (!p || !p[managerKey]) {
                return _res.status(404).json({ error: 'This graph is disabled for this project' });
            }
            next();
        };
    }
    // Middleware: check access level for a graph (read or read-write)
    function requireGraphAccess(graphName, level) {
        return (req, _res, next) => {
            const p = req.project;
            // Graph-level readonly: enforce even without auth config
            const isReadonly = p?.config.graphConfigs[graphName]?.readonly;
            if (isReadonly) {
                req.accessLevel = 'r';
            }
            if (!serverConfig)
                return next(); // no config = no auth enforcement
            if (!p)
                return next();
            const userId = req.userId;
            const ws = p.workspaceId ? projectManager.getWorkspace(p.workspaceId) : undefined;
            let access = (0, access_1.resolveAccess)(userId, graphName, p.config, serverConfig, ws?.config);
            // Graph-level readonly: cap to 'r' regardless of user permissions
            if (access === 'rw' && p.config.graphConfigs[graphName]?.readonly) {
                access = 'r';
            }
            if (!(0, access_1.canRead)(access)) {
                return _res.status(403).json({ error: 'Access denied' });
            }
            if (level === 'rw' && !(0, access_1.canWrite)(access)) {
                return _res.status(403).json({ error: 'Read-only access' });
            }
            req.accessLevel = access;
            next();
        };
    }
    // Helper: combine requireManager + read access check
    function graphMiddleware(managerKey, graphName) {
        return [requireManager(managerKey), requireGraphAccess(graphName, 'r')];
    }
    // Mount domain routers (gated by manager existence + read access)
    // Mutation endpoints (POST/PUT/DELETE) inside routers check req.accessLevel for write access
    app.use('/api/projects/:projectId/knowledge', ...graphMiddleware('knowledgeManager', 'knowledge'), (0, knowledge_1.createKnowledgeRouter)());
    app.use('/api/projects/:projectId/tasks', ...graphMiddleware('taskManager', 'tasks'), (0, tasks_1.createTasksRouter)());
    app.use('/api/projects/:projectId/epics', ...graphMiddleware('taskManager', 'tasks'), (0, epics_1.createEpicsRouter)());
    app.use('/api/projects/:projectId/skills', ...graphMiddleware('skillManager', 'skills'), (0, skills_1.createSkillsRouter)());
    app.use('/api/projects/:projectId/docs', ...graphMiddleware('docManager', 'docs'), (0, docs_1.createDocsRouter)());
    app.use('/api/projects/:projectId/code', ...graphMiddleware('codeManager', 'code'), (0, code_1.createCodeRouter)());
    app.use('/api/projects/:projectId/files', ...graphMiddleware('fileIndexManager', 'files'), (0, files_1.createFilesRouter)());
    app.use('/api/projects/:projectId/tools', (0, tools_1.createToolsRouter)(projectManager, (req, graphName, level) => {
        if (!serverConfig)
            return true;
        const p = req.project;
        if (!p)
            return true;
        const userId = req.userId;
        const ws = p.workspaceId ? projectManager.getWorkspace(p.workspaceId) : undefined;
        let access = (0, access_1.resolveAccess)(userId, graphName, p.config, serverConfig, ws?.config);
        if (access === 'rw' && p.config.graphConfigs[graphName]?.readonly)
            access = 'r';
        if (level === 'rw')
            return (0, access_1.canWrite)(access);
        return (0, access_1.canRead)(access);
    }));
    // Embedding API (optional, gated by server.embeddingApi.enabled)
    if (serverConfig?.embeddingApi?.enabled && options?.embeddingApiModelNames) {
        app.use('/api/embed', (0, embed_1.createEmbedRouter)(serverConfig.embeddingApi, options.embeddingApiModelNames));
    }
    // Serve UI at /ui/ path — check dist/ui/ (npm package) then ui/dist/ (dev)
    const uiDistPkg = path_1.default.resolve(__dirname, '../../ui');
    const uiDistDev = path_1.default.resolve(__dirname, '../../../ui/dist');
    const uiDist = fs_1.default.existsSync(uiDistPkg) ? uiDistPkg : uiDistDev;
    // Redirect root to /ui/
    app.get('/', (_req, res) => { res.redirect('/ui/'); });
    // Static files under /ui/
    app.use('/ui', express_1.default.static(uiDist, { redirect: false, index: false }));
    // SPA fallback: serve index.html for all /ui/* routes
    const indexHtml = path_1.default.join(uiDist, 'index.html');
    app.use('/ui', (_req, res, next) => {
        // Skip requests for actual files (assets with extensions like .js, .css, .png)
        if (_req.path.includes('.') && !_req.path.endsWith('.html'))
            return next();
        res.sendFile(indexHtml, { dotfiles: 'allow' }, (err) => {
            if (err)
                next();
        });
    });
    // Error handler
    app.use((err, _req, res, _next) => {
        if (err.name === 'ZodError') {
            const fields = (err.issues ?? []).map((i) => ({
                path: i.path?.join('.'),
                message: i.message,
            }));
            return res.status(400).json({ error: 'Validation error', fields });
        }
        if (err.type === 'entity.parse.failed' || (err instanceof SyntaxError && 'body' in err)) {
            return res.status(400).json({ error: 'Invalid JSON' });
        }
        log.error({ err }, 'unhandled error');
        res.status(500).json({ error: 'Internal server error' });
    });
    return app;
}
