/**
 * WebSocket endpoint — authenticates connections, registers them with the hub,
 * and handles Yjs binary sync/awareness messages via YjsManager.
 */
import * as access from '../../services/access-control.js';
import { log } from '../../utils/log.js';
/**
 * Build a per-connection repo access predicate. In open mode (`user == null`)
 * everything is allowed. Otherwise we delegate to access-control's
 * hasRepoAccess so personal-folder restrictions and owner-on-shared-folder
 * semantics match the HTTP API.
 */
function makeCanAccess(user, workspaceManager, authService) {
    if (!user)
        return () => true;
    return (repo) => access.hasRepoAccess(user, repo, workspaceManager, authService);
}
/** docName encodes `${repo}/${entityType}/${entityId}`. Returns the repo
 *  segment, or null if the format is malformed. */
function parseRepoFromDocName(docName) {
    const slash = docName.indexOf('/');
    if (slash <= 0)
        return null;
    return docName.slice(0, slash);
}
export async function registerWsRoute(fastify, wsHub, authService, workspaceManager, yjsManager) {
    fastify.get('/ws', { websocket: true }, (socket, req) => {
        // Authenticate via JWT cookie or ?token= query param
        const cookies = req.cookies;
        const jwtFromCookie = cookies?.['__sg_token'];
        const jwtFromQuery = req.query?.token;
        const token = jwtFromCookie || jwtFromQuery;
        if (authService.hasUsers()) {
            if (!token) {
                socket.send(JSON.stringify({ error: 'Unauthorized' }));
                socket.close(4001, 'Unauthorized');
                return;
            }
            const user = authService.verifyToken(token);
            if (!user) {
                socket.send(JSON.stringify({ error: 'Invalid token' }));
                socket.close(4001, 'Invalid token');
                return;
            }
            // Folder snapshot feeds presence visibility + the hub's per-client
            // broadcast filter. Use the access-control accessible set (NOT raw
            // folder_access grants): it includes workspace_wide folders (the shared
            // asset store, which has no per-user grant) and owner-on-shared folders,
            // and excludes restricted personal folders — so broadcasts for the
            // Workspace Assets folder aren't dropped. Per-message access still goes
            // through canAccess (live hasRepoAccess) on every frame.
            const folders = access.getAccessibleRepos(user, workspaceManager, authService).map(r => r.name);
            const folderSet = new Set(folders);
            const canAccess = makeCanAccess(user, workspaceManager, authService);
            wsHub.addClient(socket, user, folders);
            wsHub.sendPresenceSnapshot(socket, folderSet);
            socket.on('message', (raw) => {
                // Binary messages = Yjs sync/awareness protocol
                if (Buffer.isBuffer(raw) && raw.length >= 3 && raw[0] !== 0x7b) {
                    handleYjsBinary(yjsManager, socket, raw, canAccess);
                    return;
                }
                handleClientMessage(wsHub, yjsManager, socket, raw, String(user.id), user.displayName, canAccess);
            });
            socket.on('close', () => yjsManager.removeClientFromAll(socket));
        }
        else {
            // Open mode — no auth, all folders
            const canAccess = makeCanAccess(null, workspaceManager, authService);
            wsHub.addClient(socket, null, null);
            wsHub.sendPresenceSnapshot(socket, null);
            socket.on('message', (raw) => {
                if (Buffer.isBuffer(raw) && raw.length >= 3 && raw[0] !== 0x7b) {
                    handleYjsBinary(yjsManager, socket, raw, canAccess);
                    return;
                }
                handleClientMessage(wsHub, yjsManager, socket, raw, 'anon', 'Anonymous', canAccess);
            });
            socket.on('close', () => yjsManager.removeClientFromAll(socket));
        }
    });
}
/**
 * Handle a binary Yjs frame. Parses the docName and message type from the
 * frame header and delegates to YjsManager.
 *
 * Frame format: [2B docName length, uint16 BE][docName UTF-8][1B msgType][payload]
 */
function handleYjsBinary(yjsManager, sender, raw, canAccess) {
    try {
        const docNameLen = raw.readUInt16BE(0);
        if (raw.length < 2 + docNameLen + 1)
            return;
        const docName = raw.subarray(2, 2 + docNameLen).toString('utf8');
        const repo = parseRepoFromDocName(docName);
        if (!repo || !canAccess(repo))
            return; // silently drop unauthorized frames
        const msgType = raw[2 + docNameLen];
        const payload = raw.subarray(2 + docNameLen + 1);
        yjsManager.handleMessage(docName, sender, msgType, payload);
    }
    catch {
        // ignore malformed binary frames
    }
}
function handleClientMessage(wsHub, yjsManager, socket, raw, userId, userName, canAccess) {
    // Binary frames → Yjs sync/awareness protocol (NOT JSON)
    if (Buffer.isBuffer(raw) && raw.length >= 3) {
        // Decode binary frame: [2B nameLen][name][1B msgType][payload]
        const nameLen = raw.readUInt16BE(0);
        if (nameLen > 0 && 2 + nameLen + 1 <= raw.length) {
            const docName = raw.subarray(2, 2 + nameLen).toString('utf-8');
            const repo = parseRepoFromDocName(docName);
            if (!repo || !canAccess(repo))
                return; // silently drop
            const msgType = raw[2 + nameLen];
            const payload = raw.subarray(2 + nameLen + 1);
            yjsManager.handleMessage(docName, socket, msgType, payload);
            return;
        }
    }
    try {
        const msg = JSON.parse(typeof raw === 'string' ? raw : raw.toString());
        if (msg?.type === 'pong') {
            wsHub.handlePong(socket);
            return;
        }
        if (msg?.type === 'presence' && msg.action && msg.repo && msg.entityType && msg.entityId) {
            // Drop presence writes / snapshot reads for repos the user can't see.
            // Without this, an attacker could pollute the presence map for any
            // folder and the snapshot reply discloses who's editing what.
            if (!canAccess(msg.repo))
                return;
            wsHub.handlePresence(msg.action, msg.repo, msg.entityType, msg.entityId, { id: userId, name: userName });
            // On enter: immediately send back current presence so the client
            // doesn't have to wait for others' 30s heartbeats.
            if (msg.action === 'enter') {
                const now = new Date().toISOString();
                for (const user of wsHub.getPresence(msg.repo, msg.entityType, msg.entityId)) {
                    if (user.id === userId)
                        continue;
                    socket.send(JSON.stringify({
                        type: 'presence',
                        action: 'enter',
                        repo: msg.repo,
                        entityType: msg.entityType,
                        entityId: msg.entityId,
                        user,
                        timestamp: now,
                    }));
                }
            }
            return;
        }
        // collab:join — register client for a Yjs doc. Server replies with a
        // collab:joined ack JSON message after the doc is loaded (including any
        // persisted snapshot); the client gates its sync-step-1 + awareness
        // sends behind the ack so binary frames don't race the async load.
        // Gate by per-repo access so docName can't be used to read/edit folders
        // the user doesn't have access to.
        if (msg?.type === 'collab:join' && msg.docName) {
            const repo = parseRepoFromDocName(msg.docName);
            if (!repo || !canAccess(repo))
                return;
            yjsManager.addClient(msg.docName, socket).catch(err => {
                log.error(`[ws] yjsManager.addClient(${msg.docName}) failed:`, err);
            });
            return;
        }
        // collab:leave — unregister from a Yjs doc
        if (msg?.type === 'collab:leave' && msg.docName) {
            const repo = parseRepoFromDocName(msg.docName);
            if (!repo || !canAccess(repo))
                return;
            yjsManager.removeClient(msg.docName, socket);
            return;
        }
    }
    catch {
        // ignore malformed messages
    }
}
//# sourceMappingURL=ws.js.map