// Transport-agnostic realtime handle for the editor.
//
// WebSockets remain the preferred transport. When a WebSocket cannot be opened (IIS without
// the WebSocket feature, a proxy that strips the Upgrade header, etc.) the editor falls back
// to Server-Sent Events for server->client messages plus HTTP POST for client->server messages.
//
// Consumers (EditorShell send paths, agentSubscriptionRegistry, recovery code) talk to the
// `editorRealtime` handle instead of `globalThis.editorSocket` so they work in either mode.
import { post } from "../../services/serviceHelper";
let activeTransport = null;
// Registered by the connection hook so recovery paths (watchdog, presence recovery) can force a
// fresh connection in either transport mode. For SSE this must re-create the EventSource, since a
// deliberate close() does not auto-reconnect.
let reconnectHandler = null;
export function setReconnectHandler(fn) {
    reconnectHandler = fn;
}
function getHandle() {
    return {
        send(message) {
            const transport = activeTransport;
            if (!transport || !transport.isOpen())
                return false;
            transport.send(message);
            return true;
        },
        sendWithAck(message) {
            const transport = activeTransport;
            if (!transport || !transport.isOpen())
                return Promise.resolve(false);
            return transport.sendWithAck(message);
        },
        isOpen() {
            return !!activeTransport && activeTransport.isOpen();
        },
        mode() {
            return activeTransport ? activeTransport.mode : null;
        },
        reconnect(reason) {
            if (reconnectHandler) {
                reconnectHandler(reason);
                return;
            }
            activeTransport?.close();
        },
    };
}
/** Installs the global handle. Idempotent; call once at module load. */
export function ensureEditorRealtimeHandle() {
    const existing = globalThis
        .editorRealtime;
    if (existing)
        return existing;
    const handle = getHandle();
    globalThis.editorRealtime =
        handle;
    return handle;
}
export function getEditorRealtime() {
    return ensureEditorRealtimeHandle();
}
/** Sets the active transport (called by the connection hook when WS/SSE attaches). */
export function setActiveTransport(transport) {
    activeTransport = transport;
}
export function getActiveTransport() {
    return activeTransport;
}
// --- WebSocket adapter --------------------------------------------------------------------
export function createWebSocketAdapter(socket) {
    return {
        mode: "websocket",
        isOpen: () => socket.readyState === WebSocket.OPEN,
        send: (message) => socket.send(JSON.stringify(message)),
        sendWithAck: (message) => {
            if (socket.readyState !== WebSocket.OPEN)
                return Promise.resolve(false);
            socket.send(JSON.stringify(message));
            return Promise.resolve(true);
        },
        close: () => {
            try {
                socket.close();
            }
            catch {
                /* ignore */
            }
        },
    };
}
// --- SSE adapter --------------------------------------------------------------------------
const SSE_STREAM_URL = "/parhelia/realtime/stream";
const SSE_SEND_URL = "/parhelia/realtime/send";
/**
 * Opens an EventSource to the SSE stream and returns an adapter whose `send` POSTs client->server
 * messages through a per-session FIFO queue (HTTP POSTs have no ordering guarantee, unlike a
 * WebSocket). Consecutive pending `caret-position` messages collapse to the latest (the server
 * coalesces them anyway); all other message types preserve strict order.
 */
export function createSseTransport(sessionId, callbacks) {
    const url = `${SSE_STREAM_URL}?sessionId=${encodeURIComponent(sessionId)}`;
    const eventSource = new EventSource(url, { withCredentials: true });
    let closed = false;
    eventSource.addEventListener("open", () => {
        if (!closed)
            callbacks.onOpen();
    });
    eventSource.addEventListener("message", (event) => {
        if (closed)
            return;
        // An in-band session:revoked event must stop EventSource auto-reconnect.
        if (typeof event.data === "string" &&
            event.data.includes('"session:revoked"')) {
            try {
                const parsed = JSON.parse(event.data);
                if (parsed?.type === "session:revoked") {
                    callbacks.onMessage(event);
                    callbacks.onSessionRevoked();
                    return;
                }
            }
            catch {
                /* fall through to normal dispatch */
            }
        }
        callbacks.onMessage(event);
    });
    eventSource.addEventListener("error", (event) => {
        if (!closed)
            callbacks.onError(event);
    });
    const queue = [];
    let draining = false;
    const drain = async () => {
        if (draining)
            return;
        draining = true;
        try {
            while (queue.length > 0) {
                const entry = queue.shift();
                const result = await post(SSE_SEND_URL, entry.message, sessionId);
                if (result.type === "unauthorized") {
                    // 403 + session-revoked header — surface revocation even if the in-band event was missed.
                    entry.resolve(false);
                    callbacks.onSessionRevoked();
                    // Fail any further queued sends; the session is gone.
                    while (queue.length > 0)
                        queue.shift().resolve(false);
                    return;
                }
                entry.resolve(result.type !== "error");
            }
        }
        finally {
            draining = false;
        }
    };
    const enqueue = (message) => {
        if (closed)
            return Promise.resolve(false);
        return new Promise((resolve) => {
            if (message.type === "caret-position") {
                // Collapse consecutive pending caret-position updates to the latest; the superseded one is
                // resolved true (a newer position replaces it — callers ignore caret acks anyway).
                const lastIndex = queue.length - 1;
                const lastEntry = queue[lastIndex];
                if (lastEntry && lastEntry.message.type === "caret-position") {
                    lastEntry.resolve(true);
                    queue[lastIndex] = { message, resolve };
                    void drain();
                    return;
                }
            }
            queue.push({ message, resolve });
            void drain();
        });
    };
    return {
        mode: "sse",
        eventSource,
        isOpen: () => !closed && eventSource.readyState !== EventSource.CLOSED,
        send: (message) => {
            void enqueue(message);
        },
        sendWithAck: enqueue,
        close: () => {
            closed = true;
            try {
                eventSource.close();
            }
            catch {
                /* ignore */
            }
        },
    };
}
//# sourceMappingURL=realtimeTransport.js.map