"use strict";
/**
 * WebSocket Server for Real-time Communication
 * Manages WebSocket connections and room-based message broadcasting
 * Issue #331: WebSocket authentication via Cookie header
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.setupWebSocket = setupWebSocket;
exports.broadcast = broadcast;
exports.broadcastMessage = broadcastMessage;
exports.cleanupRooms = cleanupRooms;
exports.closeWebSocket = closeWebSocket;
const ws_1 = require("ws");
const auth_1 = require("./auth");
const ip_restriction_1 = require("./ip-restriction");
// Global state
let wss = null;
const clients = new Map();
const rooms = new Map();
/**
 * Check if a WebSocket error is an expected non-fatal error.
 * Common causes include mobile browser disconnects sending malformed close frames.
 *
 * @param error - Error with optional code property
 * @returns true if the error is expected and can be silently handled
 */
function isExpectedWebSocketError(error) {
    return (error.code === 'WS_ERR_INVALID_CLOSE_CODE' ||
        error.message?.includes('Invalid WebSocket frame') ||
        error.message?.includes('write after end') ||
        error.message?.includes('ECONNRESET') ||
        error.message?.includes('EPIPE'));
}
/**
 * Setup WebSocket server on HTTP or HTTPS server
 * Issue #331: Added auth check on WebSocket upgrade
 *
 * @param server - HTTP or HTTPS server instance
 *
 * @example
 * ```typescript
 * const server = createServer();
 * setupWebSocket(server);
 * server.listen(3000);
 * ```
 */
function setupWebSocket(server) {
    wss = new ws_1.WebSocketServer({ noServer: true });
    // Handle upgrade requests - only accept app WebSocket connections, not Next.js HMR
    server.on('upgrade', (request, socket, head) => {
        const pathname = request.url || '/';
        // Let Next.js handle its own HMR WebSocket connections in development.
        // In production there are no /_next/ WebSocket connections (no HMR).
        // Leaving the socket unhandled in production can trigger the Node.js 'request'
        // event as a fallback on Node.js 19+, causing TypeError in handleRequestImpl
        // because the response has no setHeader (Issue #331).
        if (pathname.startsWith('/_next/')) {
            if (process.env.NODE_ENV !== 'development') {
                socket.write('HTTP/1.1 426 Upgrade Required\r\nContent-Length: 0\r\nConnection: close\r\n\r\n');
                socket.destroy();
            }
            return;
        }
        // Issue #332: WebSocket IP restriction
        // [S2-008] Uses request.socket.remoteAddress directly (not getClientIp()).
        // getClientIp() is for HTTP headers (X-Real-IP/X-Forwarded-For);
        // WebSocket upgrade gets IP from the socket connection directly.
        if ((0, ip_restriction_1.isIpRestrictionEnabled)()) {
            const wsClientIp = (0, ip_restriction_1.normalizeIp)(request.socket.remoteAddress || '');
            if (!(0, ip_restriction_1.isIpAllowed)(wsClientIp, (0, ip_restriction_1.getAllowedRanges)())) {
                // [S4-004] Log injection prevention: normalizeIp() + substring(0, 45)
                const safeIp = wsClientIp.substring(0, 45);
                console.warn(`[IP-RESTRICTION] WebSocket denied: ${safeIp}`);
                socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
                socket.destroy();
                return;
            }
        }
        // Issue #331: WebSocket authentication via Cookie header
        if ((0, auth_1.isAuthEnabled)()) {
            const cookieHeader = request.headers.cookie || '';
            const cookies = (0, auth_1.parseCookies)(cookieHeader);
            const token = cookies[auth_1.AUTH_COOKIE_NAME];
            if (!token || !(0, auth_1.verifyToken)(token)) {
                socket.write('HTTP/1.1 401 Unauthorized\r\nContent-Length: 0\r\nConnection: close\r\n\r\n');
                socket.destroy();
                return;
            }
        }
        wss.handleUpgrade(request, socket, head, (ws) => {
            wss.emit('connection', ws, request);
        });
    });
    // Handle WebSocket server errors (e.g., invalid frames from clients)
    wss.on('error', (error) => {
        console.error('[WS Server] Error:', error.message);
    });
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    wss.on('connection', (ws, req) => {
        // Connection logging removed to reduce noise
        // Initialize client info
        const clientInfo = {
            ws,
            worktreeIds: new Set(),
        };
        clients.set(ws, clientInfo);
        // Handle underlying socket errors (catches invalid frame errors earlier)
        // Force destroy the socket on error to prevent further frame processing
        const socket = ws._socket;
        if (socket) {
            socket.on('error', (err) => {
                if (!isExpectedWebSocketError(err)) {
                    console.error('[WS Socket] Error:', err.message);
                }
                // Immediately destroy the socket to prevent further errors
                try {
                    if (socket.destroy)
                        socket.destroy();
                }
                catch {
                    // Socket may already be destroyed
                }
                handleDisconnect(ws);
            });
        }
        // Handle messages
        ws.on('message', (data) => {
            try {
                const message = JSON.parse(data.toString());
                handleMessage(ws, message);
            }
            catch (parseError) {
                console.error('Error parsing WebSocket message:', parseError);
                // Don't close connection on parse error, just log it
            }
        });
        // Handle disconnection - silently clean up
        ws.on('close', () => {
            handleDisconnect(ws);
        });
        // Handle errors (including invalid close codes from mobile browsers)
        ws.on('error', (error) => {
            if (!isExpectedWebSocketError(error)) {
                console.error('[WS] WebSocket error:', error.message);
            }
            // Immediately terminate to prevent further errors
            try {
                ws.terminate();
            }
            catch {
                // WebSocket may already be closed
            }
            handleDisconnect(ws);
        });
    });
    // WebSocket server initialization complete (no log in production per CLAUDE.md)
}
/**
 * Handle incoming WebSocket message
 */
function handleMessage(ws, message) {
    switch (message.type) {
        case 'subscribe':
            handleSubscribe(ws, message.worktreeId);
            break;
        case 'unsubscribe':
            handleUnsubscribe(ws, message.worktreeId);
            break;
        case 'broadcast':
            handleBroadcast(message.worktreeId, message.data);
            break;
        default:
            console.warn('Unknown message type:', message);
    }
}
/**
 * Subscribe client to a worktree room
 */
function handleSubscribe(ws, worktreeId) {
    const clientInfo = clients.get(ws);
    if (!clientInfo) {
        return;
    }
    // Add worktreeId to client's subscriptions
    clientInfo.worktreeIds.add(worktreeId);
    // Add client to room
    if (!rooms.has(worktreeId)) {
        rooms.set(worktreeId, new Set());
    }
    const room = rooms.get(worktreeId);
    room.add(ws);
    // Client subscribed (no log in production per CLAUDE.md)
}
/**
 * Unsubscribe client from a worktree room
 */
function handleUnsubscribe(ws, worktreeId) {
    const clientInfo = clients.get(ws);
    if (!clientInfo)
        return;
    // Remove worktreeId from client's subscriptions
    clientInfo.worktreeIds.delete(worktreeId);
    // Remove client from room
    const room = rooms.get(worktreeId);
    if (room) {
        room.delete(ws);
        // Clean up empty rooms
        if (room.size === 0) {
            rooms.delete(worktreeId);
        }
    }
    // Client unsubscribed (no log in production per CLAUDE.md)
}
/**
 * Broadcast message to all clients in a worktree room
 */
function handleBroadcast(worktreeId, data) {
    const room = rooms.get(worktreeId);
    if (!room || room.size === 0) {
        return;
    }
    try {
        const message = JSON.stringify({
            type: 'broadcast',
            worktreeId,
            data,
        });
        room.forEach((client) => {
            if (client.readyState === ws_1.WebSocket.OPEN) {
                try {
                    client.send(message);
                }
                catch (sendError) {
                    console.error(`Error sending WebSocket message to client:`, sendError);
                }
            }
        });
    }
    catch (broadcastError) {
        console.error(`Error broadcasting to worktree ${worktreeId}:`, broadcastError);
        // Try to broadcast with sanitized data
        try {
            const sanitizedMessage = JSON.stringify({
                type: 'broadcast',
                worktreeId,
                data: { error: 'Message encoding error' },
            });
            room.forEach((client) => {
                if (client.readyState === ws_1.WebSocket.OPEN) {
                    try {
                        client.send(sanitizedMessage);
                    }
                    catch {
                        // Silent fail for fallback
                    }
                }
            });
        }
        catch (fallbackError) {
            console.error('Failed to send fallback message:', fallbackError);
        }
    }
}
/**
 * Handle client disconnection
 */
function handleDisconnect(ws) {
    const clientInfo = clients.get(ws);
    if (!clientInfo)
        return;
    // Remove client from all rooms
    clientInfo.worktreeIds.forEach((worktreeId) => {
        const room = rooms.get(worktreeId);
        if (room) {
            room.delete(ws);
            // Clean up empty rooms
            if (room.size === 0) {
                rooms.delete(worktreeId);
            }
        }
    });
    // Remove client from clients map
    clients.delete(ws);
}
/**
 * Broadcast message to a specific worktree room (for API use)
 *
 * @param worktreeId - Worktree identifier
 * @param data - Data to broadcast
 *
 * @example
 * ```typescript
 * broadcast('feature-foo', { type: 'message', content: 'New message' });
 * ```
 */
function broadcast(worktreeId, data) {
    handleBroadcast(worktreeId, data);
}
/**
 * Broadcast message with type to a specific worktree room
 *
 * @param type - Message type
 * @param data - Data to broadcast (should include worktreeId)
 *
 * @example
 * ```typescript
 * broadcastMessage('message', { worktreeId: 'feature-foo', message: {...} });
 * ```
 */
function broadcastMessage(type, data) {
    if (data.worktreeId) {
        handleBroadcast(data.worktreeId, { type, ...data });
    }
    else {
        console.warn('broadcastMessage called without worktreeId');
    }
}
/**
 * Clean up WebSocket rooms for deleted worktrees
 * Removes rooms from the rooms map (clients will naturally disconnect or resubscribe)
 *
 * @param worktreeIds - Array of worktree IDs to clean up
 *
 * @example
 * ```typescript
 * cleanupRooms(['wt-1', 'wt-2', 'wt-3']);
 * ```
 */
function cleanupRooms(worktreeIds) {
    for (const worktreeId of worktreeIds) {
        const room = rooms.get(worktreeId);
        if (room) {
            // Unsubscribe all clients from this room
            room.forEach((ws) => {
                const clientInfo = clients.get(ws);
                if (clientInfo) {
                    clientInfo.worktreeIds.delete(worktreeId);
                }
            });
            // Delete the room
            rooms.delete(worktreeId);
            // Room cleaned up (no log in production per CLAUDE.md)
        }
    }
}
/**
 * Close WebSocket server
 * Used for testing and graceful shutdown
 */
function closeWebSocket() {
    if (wss) {
        // Close all client connections
        clients.forEach((clientInfo) => {
            clientInfo.ws.close();
        });
        // Clear state
        clients.clear();
        rooms.clear();
        // Close server
        wss.close();
        wss = null;
        // WebSocket server closed (no log in production per CLAUDE.md)
    }
}
