"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.attachWebSocket = attachWebSocket;
const ws_1 = require("ws");
const jwt_1 = require("../../lib/jwt");
const access_1 = require("../../lib/access");
const multi_config_1 = require("../../lib/multi-config");
const defaults_1 = require("../../lib/defaults");
const logger_1 = require("../../lib/logger");
const log = (0, logger_1.createLogger)('ws');
/**
 * Attach a WebSocket server to the HTTP server at /api/ws.
 * Broadcasts ProjectManager events to connected clients, filtered by user access.
 * Returns a handle with a cleanup function to remove all listeners.
 */
function attachWebSocket(httpServer, projectManager, options) {
    const wss = new ws_1.WebSocketServer({ noServer: true });
    const jwtSecret = options?.jwtSecret;
    const users = options?.users ?? {};
    const hasUsers = Object.keys(users).length > 0;
    const serverConfig = options?.serverConfig;
    // Store userId per WebSocket client
    const clientUserIds = new WeakMap();
    // Handle upgrade requests for /api/ws
    httpServer.on('upgrade', (req, socket, head) => {
        if (req.url !== '/api/ws') {
            socket.destroy();
            return;
        }
        // Auth: if users are configured, require valid JWT cookie or reject
        let userId;
        if (hasUsers && jwtSecret) {
            const cookie = req.headers.cookie ?? '';
            const match = cookie.match(/(?:^|;\s*)mgm_access=([^;]+)/);
            const token = match?.[1];
            if (!token) {
                socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
                socket.destroy();
                return;
            }
            const payload = (0, jwt_1.verifyToken)(token, jwtSecret);
            if (!payload || payload.type !== 'access' || !users[payload.userId]) {
                socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
                socket.destroy();
                return;
            }
            userId = payload.userId;
        }
        wss.handleUpgrade(req, socket, head, (ws) => {
            clientUserIds.set(ws, userId);
            ws.on('error', (err) => {
                log.error({ err }, 'client error');
            });
            wss.emit('connection', ws, req);
        });
    });
    // Check if a user can access any graph in a project
    function canAccessProject(userId, projectId) {
        if (!serverConfig || !hasUsers)
            return true;
        const project = projectManager.getProject(projectId);
        if (!project)
            return false;
        const ws = project.workspaceId ? projectManager.getWorkspace(project.workspaceId) : undefined;
        return multi_config_1.GRAPH_NAMES.some(gn => (0, access_1.canRead)((0, access_1.resolveAccess)(userId, gn, project.config, serverConfig, ws?.config)));
    }
    // Broadcast helper — filters by user access when auth is configured
    function broadcast(event) {
        const msg = JSON.stringify(event);
        for (const client of wss.clients) {
            if (client.readyState !== ws_1.WebSocket.OPEN)
                continue;
            if (hasUsers && serverConfig) {
                const uid = clientUserIds.get(client);
                if (!canAccessProject(uid, event.projectId))
                    continue;
            }
            client.send(msg, (err) => {
                if (err)
                    log.error({ err }, 'send error');
            });
        }
    }
    // Subscribe to ProjectManager events (track handlers for cleanup)
    const listeners = [];
    const events = [
        'note:created', 'note:updated', 'note:deleted',
        'note:relation:added', 'note:relation:deleted',
        'note:attachment:added', 'note:attachment:deleted',
        'task:created', 'task:updated', 'task:deleted', 'task:moved', 'task:reordered',
        'task:relation:added', 'task:relation:deleted',
        'task:attachment:added', 'task:attachment:deleted',
        'epic:created', 'epic:updated', 'epic:deleted',
        'epic:linked', 'epic:unlinked',
        'skill:created', 'skill:updated', 'skill:deleted',
        'skill:relation:added', 'skill:relation:deleted',
        'skill:attachment:added', 'skill:attachment:deleted',
        'project:indexed',
    ];
    for (const eventType of events) {
        const handler = (data) => {
            broadcast({ projectId: data.projectId, type: eventType, data });
        };
        projectManager.on(eventType, handler);
        listeners.push([eventType, handler]);
    }
    // Debounced graph:updated from indexer
    let graphUpdateTimer;
    let pendingGraphUpdates = new Map();
    const graphHandler = (data) => {
        const key = data.projectId;
        if (!pendingGraphUpdates.has(key))
            pendingGraphUpdates.set(key, []);
        pendingGraphUpdates.get(key).push(data.file);
        if (!graphUpdateTimer) {
            graphUpdateTimer = setTimeout(() => {
                for (const [projectId, files] of pendingGraphUpdates) {
                    broadcast({ projectId, type: 'graph:updated', data: { files } });
                }
                pendingGraphUpdates = new Map();
                graphUpdateTimer = undefined;
            }, defaults_1.WS_DEBOUNCE_MS);
        }
    };
    projectManager.on('graph:updated', graphHandler);
    listeners.push(['graph:updated', graphHandler]);
    function cleanup() {
        for (const [event, handler] of listeners) {
            projectManager.removeListener(event, handler);
        }
        if (graphUpdateTimer) {
            clearTimeout(graphUpdateTimer);
            graphUpdateTimer = undefined;
        }
        pendingGraphUpdates.clear();
    }
    return { wss, cleanup };
}
