import WebSocket from 'ws';
import { isAllowedOrigin } from '../config.js';
import { parseClientTerminalMessage } from '../../shared/protocol.js';
import { authenticateTerminalRequest } from './terminalRoutes.js';
import { SocketBackpressure } from './socketBackpressure.js';
// Bytes pending in this socket's outbound buffer before we ask the PTY to
// pause. 1 MB is enough to absorb normal interactive spikes but small enough
// that a stalled client can't grow it unbounded.
const PAUSE_HIGH_WATER_BYTES = 1 << 20;
// Resume once the buffer drains back below this — gives the socket headroom
// before the next pause/resume cycle.
const PAUSE_LOW_WATER_BYTES = 64 << 10;
// How often to poll bufferedAmount while paused. ws doesn't emit a drain
// event we can hook here, so we sample on a coarse interval.
const DRAIN_POLL_MS = 50;
export async function registerTerminalWebSocket(app, config, services) {
    app.get('/api/terminals/:id/ws', { websocket: true }, async (socket, request) => {
        if (!request.headers.origin) {
            request.log.warn({ event: 'terminal_ws_rejected', terminalId: request.params.id, reason: 'origin_required' }, 'terminal websocket rejected');
            socket.close(1008, 'origin required');
            return;
        }
        if (!isAllowedOrigin(config, request.headers.origin)) {
            request.log.warn({ event: 'terminal_ws_rejected', terminalId: request.params.id, reason: 'origin_not_allowed' }, 'terminal websocket rejected');
            socket.close(1008, 'origin not allowed');
            return;
        }
        const auth = await authenticateTerminalRequest(services.authService, request);
        if (!auth) {
            request.log.warn({ event: 'terminal_ws_rejected', terminalId: request.params.id, reason: 'auth_failed' }, 'terminal websocket rejected');
            socket.close(1008, 'unauthorized');
            return;
        }
        const terminalId = request.params.id;
        let snapshotSent = false;
        const pendingMessages = [];
        // Forward-declared so the subscriber closure (registered below) and the
        // backpressure sink can both reference it. Assigned exactly once before
        // any send occurs, so the optional chains in the sink stay null-safe
        // without ever actually hitting null in practice.
        let attachment = null;
        const backpressure = new SocketBackpressure({
            get bufferedAmount() {
                return socket.bufferedAmount;
            },
            isOpen: () => socket.readyState === WebSocket.OPEN
        }, {
            pause: () => attachment?.pause(),
            resume: () => attachment?.resume()
        }, {
            highWaterBytes: PAUSE_HIGH_WATER_BYTES,
            lowWaterBytes: PAUSE_LOW_WATER_BYTES,
            pollMs: DRAIN_POLL_MS
        });
        const sendWithBackpressure = (message) => {
            sendJson(socket, message);
            backpressure.observe();
        };
        attachment = services.terminalManager.attachTerminal(terminalId, (message) => {
            if (!snapshotSent) {
                pendingMessages.push(message);
                return;
            }
            sendWithBackpressure(message);
        }, { replay: false });
        if (!attachment) {
            backpressure.dispose();
            socket.close(1008, 'terminal not found');
            return;
        }
        sendWithBackpressure({ type: 'snapshot', terminal: attachment.terminal, output: attachment.output, history: attachment.history });
        snapshotSent = true;
        for (const message of pendingMessages) {
            sendWithBackpressure(message);
        }
        pendingMessages.length = 0;
        let isAlive = true;
        const heartbeat = setInterval(() => {
            if (socket.readyState !== WebSocket.OPEN) {
                return;
            }
            if (!isAlive) {
                socket.terminate();
                return;
            }
            isAlive = false;
            socket.ping();
        }, 30_000);
        socket.on('pong', () => {
            isAlive = true;
        });
        socket.on('message', (raw) => {
            const message = parseClientTerminalMessage(raw.toString());
            if (!message) {
                sendJson(socket, { type: 'error', message: 'invalid terminal message' });
                return;
            }
            if (message.type === 'ping') {
                sendJson(socket, { type: 'pong', nonce: message.nonce });
                return;
            }
            if (message.terminalId !== terminalId) {
                sendJson(socket, { type: 'error', message: 'terminal id mismatch' });
                return;
            }
            if (message.type === 'refresh_cwd') {
                void services.terminalManager.refreshTerminalCwd(message.terminalId);
                return;
            }
            if (message.type === 'input') {
                if (!services.terminalManager.writeToTerminal(message.terminalId, message.data)) {
                    sendJson(socket, { type: 'error', message: 'terminal is not writable' });
                }
                return;
            }
            if (!services.terminalManager.resizeTerminal(message.terminalId, message.cols, message.rows)) {
                sendJson(socket, { type: 'error', message: 'terminal is not resizable' });
            }
        });
        socket.on('close', () => {
            clearInterval(heartbeat);
            backpressure.dispose();
            attachment?.dispose();
        });
    });
}
function sendJson(socket, payload) {
    if (socket.readyState !== WebSocket.CLOSING && socket.readyState !== WebSocket.CLOSED) {
        socket.send(JSON.stringify(payload));
    }
}
//# sourceMappingURL=terminalWebSocket.js.map