"use strict";
/**
 * WebSocket Server for Real-time Communication
 * Manages WebSocket connections and room-based message broadcasting
 */
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");
// Global state
let wss = null;
const clients = new Map();
const rooms = new Map();
/**
 * Setup WebSocket server on HTTP server
 *
 * @param server - HTTP server instance
 *
 * @example
 * ```typescript
 * const server = createServer();
 * setupWebSocket(server);
 * server.listen(3000);
 * ```
 */
function setupWebSocket(server) {
    wss = new ws_1.WebSocketServer({ server });
    // 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) => {
                // Suppress common mobile browser disconnect errors
                const isExpectedError = err.code === 'WS_ERR_INVALID_CLOSE_CODE' ||
                    err.message?.includes('Invalid WebSocket frame') ||
                    err.message?.includes('write after end') ||
                    err.message?.includes('ECONNRESET') ||
                    err.message?.includes('EPIPE');
                if (!isExpectedError) {
                    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) => {
            // Suppress noisy errors from mobile browser disconnects
            const isExpectedError = error.code === 'WS_ERR_INVALID_CLOSE_CODE' ||
                error.message?.includes('Invalid WebSocket frame') ||
                error.message?.includes('write after end');
            if (!isExpectedError) {
                console.error('[WS] WebSocket error:', error.message);
            }
            // Immediately terminate to prevent further errors
            try {
                ws.terminate();
            }
            catch {
                // WebSocket may already be closed
            }
            handleDisconnect(ws);
        });
    });
    console.log('WebSocket server initialized');
}
/**
 * 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) {
        console.log(`[WS] handleSubscribe: clientInfo not found for worktreeId: ${worktreeId}`);
        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);
    console.log(`Client subscribed to worktree: ${worktreeId}, room size: ${room.size}, ws readyState: ${ws.readyState}`);
}
/**
 * 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);
        }
    }
    console.log(`Client unsubscribed from worktree: ${worktreeId}`);
}
/**
 * Broadcast message to all clients in a worktree room
 */
function handleBroadcast(worktreeId, data) {
    const room = rooms.get(worktreeId);
    console.log(`[WS] handleBroadcast called for ${worktreeId}, room size: ${room?.size || 0}`);
    if (!room) {
        console.log(`[WS] No room found for ${worktreeId}`);
        return;
    }
    if (room.size === 0) {
        console.log(`[WS] Room for ${worktreeId} is empty`);
        return;
    }
    try {
        const message = JSON.stringify({
            type: 'broadcast',
            worktreeId,
            data,
        });
        let successCount = 0;
        let errorCount = 0;
        room.forEach((client) => {
            if (client.readyState === ws_1.WebSocket.OPEN) {
                try {
                    client.send(message);
                    successCount++;
                }
                catch (sendError) {
                    errorCount++;
                    console.error(`Error sending WebSocket message to client:`, sendError);
                }
            }
        });
        console.log(`Broadcast to worktree ${worktreeId}: ${successCount}/${room.size} clients (${errorCount} errors)`);
    }
    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);
            console.log(`[WS] Cleaned up room for worktree: ${worktreeId}`);
        }
    }
}
/**
 * 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;
        console.log('WebSocket server closed');
    }
}
