import { useEffect, useRef } from "react";
import { checkConnectionAvailability, } from "../../services/editService";
import { createEditorSocketDiagnostics, getEditorSocketReadyStateLabel, } from "../socketDiagnostics";
import { createSseTransport, createWebSocketAdapter, ensureEditorRealtimeHandle, getEditorRealtime, setActiveTransport, setReconnectHandler, } from "../realtime/realtimeTransport";
// Availability recheck cadence while the concurrent-user-limit latch is set. Deliberately slow:
// the ConcurrentUserLimitDialog polls at 7.5s while open, so this timer only has to cover tabs
// where the dialog was dismissed.
const CONCURRENT_LIMIT_RECHECK_MS = 60_000;
// While on the SSE fallback, how often to probe whether a WebSocket can be opened again. At
// fallback time an app-pool recycle looks identical to a missing IIS WebSocket feature, so SSE
// mode must not be permanent — but one probe per interval keeps genuinely WS-less environments
// free of retry storms.
const WS_REPROBE_INTERVAL_MS = 5 * 60_000;
// A probe socket that has not reached OPEN within this window counts as failed.
const WS_PROBE_TIMEOUT_MS = 5_000;
// Used when the server rejects the connection without complete limit metadata; the dialog must
// still appear or the tab has no recovery path besides a reload.
const CONCURRENT_LIMIT_FALLBACK_MESSAGE = "The concurrent user limit for this license has been reached. Editing becomes available again once another user disconnects.";
/**
 * Closes the active editor websocket so the hook's own reconnect machinery picks it back
 * up. Used by recovery code that wants the freshly opened socket to re-issue agent
 * subscriptions via the existing onOpen callback chain — not for routine close handling.
 *
 * Returns whether a reconnect was actually initiated. The hook's close handler does NOT null
 * `globalThis.editorSocket`; the next `connect()` call detects `readyState === CLOSED`
 * and overwrites the global, so we must not delete it from this helper. When the socket
 * is missing or already CLOSING/CLOSED there is no close event to drive recovery, so we route
 * through the hook's reconnect handler, which re-enters the connect path directly.
 */
export function forceEditorSocketReconnect(reason) {
    // In SSE fallback mode there is no globalThis.editorSocket; route through the transport handle,
    // which re-establishes the EventSource via the hook's registered reconnect handler.
    const realtime = getEditorRealtime();
    if (realtime.mode() === "sse") {
        realtime.reconnect(reason);
        return true;
    }
    const socket = globalThis.editorSocket;
    if (socket &&
        (socket.readyState === WebSocket.OPEN ||
            socket.readyState === WebSocket.CONNECTING)) {
        try {
            socket.close(4000, reason);
            return true;
        }
        catch {
            return false;
        }
    }
    realtime.reconnect(reason);
    return true;
}
export function useEditorWebSocket(params) {
    const { sessionId, onMessage, onOpen, onError, onConcurrentUserLimit, onSessionRevoked, connectSocket, requestQuota, sendClientInfo, setSocketDiagnostics, enableWebSocketTransport = true, enableSseFallback = true, transportSettingsReady = true, } = params;
    const socketInstanceRef = useRef(null);
    const attachedMessageHandlerRef = useRef(null);
    const reconnectTimeoutRef = useRef(null);
    const reconnectAttemptsRef = useRef(0);
    const reconnectCountRef = useRef(0);
    const isConcurrentUserLimitRef = useRef(false);
    const modeRef = useRef(null);
    const sseTransportRef = useRef(null);
    const hadOpenedRef = useRef(false);
    const fallbackTimerRef = useRef(null);
    const limitRecheckTimerRef = useRef(null);
    const sseReconnectTimeoutRef = useRef(null);
    const sseReconnectAttemptsRef = useRef(0);
    const wsReprobeTimerRef = useRef(null);
    // Tears down an in-flight WebSocket probe (its socket and timeout); null when none is running.
    const wsProbeCleanupRef = useRef(null);
    // Stable refs for callbacks to avoid re-connecting on every render
    const onMessageRef = useRef(onMessage);
    const onOpenRef = useRef(onOpen);
    const onErrorRef = useRef(onError);
    const onConcurrentUserLimitRef = useRef(onConcurrentUserLimit);
    const onSessionRevokedRef = useRef(onSessionRevoked);
    const requestQuotaRef = useRef(requestQuota);
    const sendClientInfoRef = useRef(sendClientInfo);
    const wsEnabledRef = useRef(enableWebSocketTransport);
    const sseEnabledRef = useRef(enableSseFallback);
    useEffect(() => {
        wsEnabledRef.current = enableWebSocketTransport;
    }, [enableWebSocketTransport]);
    useEffect(() => {
        sseEnabledRef.current = enableSseFallback;
    }, [enableSseFallback]);
    useEffect(() => {
        onMessageRef.current = onMessage;
    }, [onMessage]);
    useEffect(() => {
        onOpenRef.current = onOpen;
    }, [onOpen]);
    useEffect(() => {
        onErrorRef.current = onError;
    }, [onError]);
    useEffect(() => {
        onConcurrentUserLimitRef.current = onConcurrentUserLimit;
    }, [onConcurrentUserLimit]);
    useEffect(() => {
        onSessionRevokedRef.current = onSessionRevoked;
    }, [onSessionRevoked]);
    useEffect(() => {
        requestQuotaRef.current = requestQuota;
    }, [requestQuota]);
    useEffect(() => {
        sendClientInfoRef.current = sendClientInfo;
    }, [sendClientInfo]);
    useEffect(() => {
        setSocketDiagnostics?.(createEditorSocketDiagnostics(sessionId));
    }, [sessionId, setSocketDiagnostics]);
    useEffect(() => {
        if (!transportSettingsReady) {
            return;
        }
        let disposed = false;
        const shouldRecordSocketMessageActivity = (event) => {
            if (typeof event?.data !== "string") {
                return true;
            }
            try {
                const parsed = JSON.parse(event.data);
                if (!parsed) {
                    return true;
                }
                if (parsed.type !== "agent:run:delta") {
                    return true;
                }
                const deltaType = parsed.payload?.type;
                return String(deltaType || "").toLowerCase() !== "heartbeat";
            }
            catch {
                return true;
            }
        };
        const updateSocketDiagnostics = (patch, socket) => {
            if (!setSocketDiagnostics) {
                return;
            }
            setSocketDiagnostics((previous) => {
                const nextReadyState = socket?.readyState ??
                    (patch.readyState !== undefined
                        ? patch.readyState
                        : previous.readyState);
                return {
                    ...previous,
                    sessionId,
                    readyState: nextReadyState,
                    readyStateLabel: getEditorSocketReadyStateLabel(nextReadyState),
                    ...patch,
                };
            });
        };
        const markSocketEvent = (eventType, socket, patch) => {
            const now = new Date().toISOString();
            updateSocketDiagnostics({
                lastEventType: eventType,
                lastEventAt: now,
                ...patch,
            }, socket);
        };
        const scheduleReconnect = (socket) => {
            if (reconnectTimeoutRef.current || modeRef.current === "sse") {
                return;
            }
            const attempts = reconnectAttemptsRef.current;
            const delay = Math.min(1000 * Math.pow(2, Math.min(attempts, 5)), 30000);
            reconnectCountRef.current += 1;
            markSocketEvent("reconnect-scheduled", socket, {
                reconnectCount: reconnectCountRef.current,
            });
            reconnectTimeoutRef.current = setTimeout(() => {
                reconnectTimeoutRef.current = null;
                reconnectAttemptsRef.current = attempts + 1;
                connect();
            }, delay);
        };
        const startConnect = () => {
            connect().catch((error) => {
                console.error("Error in WebSocket connection:", error);
                onErrorRef.current?.(new Event("error"));
            });
        };
        const clearLimitRecheckTimer = () => {
            if (limitRecheckTimerRef.current) {
                clearTimeout(limitRecheckTimerRef.current);
                limitRecheckTimerRef.current = null;
            }
        };
        // While the concurrent-user-limit latch is set no close event will ever fire again, so this
        // chained timeout (at most one check in flight) is the only automatic way back in once a
        // seat frees up.
        const scheduleLimitRecheck = () => {
            if (disposed || limitRecheckTimerRef.current) {
                return;
            }
            limitRecheckTimerRef.current = setTimeout(() => {
                limitRecheckTimerRef.current = null;
                if (disposed || !isConcurrentUserLimitRef.current) {
                    return;
                }
                checkConnectionAvailability(sessionId)
                    .then((availability) => {
                    if (disposed || !isConcurrentUserLimitRef.current) {
                        return;
                    }
                    if (availability.allowed) {
                        isConcurrentUserLimitRef.current = false;
                        reconnectAttemptsRef.current = 0;
                        if (modeRef.current === "sse") {
                            connectSse();
                        }
                        else {
                            startConnect();
                        }
                        return;
                    }
                    scheduleLimitRecheck();
                })
                    .catch(() => {
                    // Availability unknown (server restarting?) — keep polling.
                    if (!disposed && isConcurrentUserLimitRef.current) {
                        scheduleLimitRecheck();
                    }
                });
            }, CONCURRENT_LIMIT_RECHECK_MS);
        };
        // The dialog (with its Retry button) must appear even when the availability response carries
        // partial metadata — a latched limit without a visible dialog leaves no recovery path short
        // of a reload.
        const notifyConcurrentUserLimit = (availability) => {
            isConcurrentUserLimitRef.current = true;
            onConcurrentUserLimitRef.current?.({
                backendReachable: availability.backendReachable ?? true,
                currentUsers: availability.currentUsers ?? 0,
                maxUsers: availability.maxUsers ?? 0,
                message: availability.message ?? CONCURRENT_LIMIT_FALLBACK_MESSAGE,
            });
            scheduleLimitRecheck();
        };
        const clearSseReconnectTimer = () => {
            if (sseReconnectTimeoutRef.current) {
                clearTimeout(sseReconnectTimeoutRef.current);
                sseReconnectTimeoutRef.current = null;
            }
        };
        // EventSource retries transient drops on its own, but a hard failure (connection refused,
        // non-200 while the server restarts) closes it permanently — the stream must be re-created
        // by us, reusing the WebSocket backoff curve.
        const scheduleSseReconnect = () => {
            if (disposed ||
                sseReconnectTimeoutRef.current ||
                modeRef.current !== "sse") {
                return;
            }
            const attempts = sseReconnectAttemptsRef.current;
            const delay = Math.min(1000 * Math.pow(2, Math.min(attempts, 5)), 30000);
            reconnectCountRef.current += 1;
            markSocketEvent("reconnect-scheduled", null, {
                reconnectCount: reconnectCountRef.current,
            });
            sseReconnectTimeoutRef.current = setTimeout(() => {
                sseReconnectTimeoutRef.current = null;
                sseReconnectAttemptsRef.current = attempts + 1;
                connectSse();
            }, delay);
        };
        const clearWsReprobeTimer = () => {
            if (wsReprobeTimerRef.current) {
                clearTimeout(wsReprobeTimerRef.current);
                wsReprobeTimerRef.current = null;
            }
        };
        // SSE mode is sticky by design (a missing IIS WebSocket feature never heals), but the same
        // fallback also triggers on transient failures like an app-pool recycle — probe occasionally
        // so those tabs find their way back to the preferred transport.
        const startWsReprobeTimer = () => {
            if (disposed || wsReprobeTimerRef.current || !wsEnabledRef.current) {
                return;
            }
            wsReprobeTimerRef.current = setTimeout(() => {
                wsReprobeTimerRef.current = null;
                if (disposed || modeRef.current !== "sse") {
                    return;
                }
                if (!isConcurrentUserLimitRef.current) {
                    probeWebSocket();
                }
                startWsReprobeTimer();
            }, WS_REPROBE_INTERVAL_MS);
        };
        // Opens a single throwaway WebSocket. Reaching OPEN proves the transport works again, so the
        // editor switches back; anything else leaves SSE untouched. The probe carries none of the
        // hook's listeners, so it must never be adopted as the real socket.
        const probeWebSocket = () => {
            if (disposed ||
                modeRef.current !== "sse" ||
                !wsEnabledRef.current ||
                wsProbeCleanupRef.current) {
                return;
            }
            let probe;
            try {
                probe = connectSocket(sessionId);
            }
            catch {
                return;
            }
            let settled = false;
            const settle = (opened) => {
                if (settled) {
                    return;
                }
                settled = true;
                wsProbeCleanupRef.current = null;
                clearTimeout(probeTimer);
                try {
                    probe.close();
                }
                catch {
                    /* ignore */
                }
                if (opened && !disposed && modeRef.current === "sse") {
                    switchBackToWebSocket();
                }
            };
            const probeTimer = setTimeout(() => settle(false), WS_PROBE_TIMEOUT_MS);
            wsProbeCleanupRef.current = () => settle(false);
            probe.addEventListener("open", () => settle(true));
            probe.addEventListener("close", () => settle(false));
            probe.addEventListener("error", () => settle(false));
        };
        const switchBackToWebSocket = () => {
            if (disposed) {
                return;
            }
            clearWsReprobeTimer();
            clearSseReconnectTimer();
            sseTransportRef.current?.close();
            sseTransportRef.current = null;
            setActiveTransport(null);
            modeRef.current = null;
            // The probe reached OPEN, so this environment provably supports WebSockets; a later
            // close-before-open must go through the retry path, not straight back to SSE.
            hadOpenedRef.current = true;
            reconnectAttemptsRef.current = 0;
            sseReconnectAttemptsRef.current = 0;
            startConnect();
        };
        const attach = (socket, handler, reusedExistingSocket) => {
            if (disposed) {
                return;
            }
            socket.addEventListener("message", handler);
            socketInstanceRef.current = socket;
            attachedMessageHandlerRef.current = handler;
            markSocketEvent(reusedExistingSocket ? "reused-socket" : "connect-attempt", socket);
        };
        // Hoisted so both the WebSocket and the SSE fallback dispatch incoming messages identically.
        const handleMessage = (event) => {
            if (shouldRecordSocketMessageActivity(event)) {
                markSocketEvent("message", socketInstanceRef.current, {
                    lastMessageAt: new Date().toISOString(),
                });
            }
            onMessageRef.current?.(event);
        };
        const clearFallbackTimer = () => {
            if (fallbackTimerRef.current) {
                clearTimeout(fallbackTimerRef.current);
                fallbackTimerRef.current = null;
            }
        };
        const startFallbackTimer = () => {
            clearFallbackTimer();
            if (!sseEnabledRef.current)
                return; // SSE fallback disabled in settings.
            fallbackTimerRef.current = setTimeout(() => {
                fallbackTimerRef.current = null;
                const ws = socketInstanceRef.current;
                if (!disposed &&
                    modeRef.current !== "sse" &&
                    !isConcurrentUserLimitRef.current &&
                    (!ws || ws.readyState !== WebSocket.OPEN)) {
                    fallbackToSse();
                }
            }, 5000);
        };
        // Establishes (or re-establishes) the SSE fallback transport.
        const connectSse = () => {
            if (disposed)
                return;
            modeRef.current = "sse";
            clearFallbackTimer();
            clearSseReconnectTimer();
            if (reconnectTimeoutRef.current) {
                clearTimeout(reconnectTimeoutRef.current);
                reconnectTimeoutRef.current = null;
            }
            const existingWs = globalThis.editorSocket;
            try {
                existingWs?.close();
            }
            catch {
                /* ignore */
            }
            globalThis.editorSocket = undefined;
            socketInstanceRef.current = null;
            sseTransportRef.current?.close();
            const sse = createSseTransport(sessionId, {
                onOpen: () => {
                    if (disposed)
                        return;
                    isConcurrentUserLimitRef.current = false;
                    clearLimitRecheckTimer();
                    sseReconnectAttemptsRef.current = 0;
                    markSocketEvent("open", null, {
                        lastOpenAt: new Date().toISOString(),
                        lastCloseCode: null,
                        lastCloseReason: null,
                        lastErrorMessage: null,
                    });
                    onOpenRef.current?.();
                    sendClientInfoRef.current?.();
                    requestQuotaRef.current?.();
                },
                onMessage: (event) => {
                    if (!disposed)
                        handleMessage(event);
                },
                onError: (event) => {
                    if (disposed)
                        return;
                    markSocketEvent("error", null, {
                        lastErrorAt: new Date().toISOString(),
                    });
                    onErrorRef.current?.(event);
                    // EventSource auto-reconnects on transient drops but stays CLOSED after a hard failure
                    // (connection refused, non-200 while the server restarts). Capture the state before the
                    // availability round-trip so a late response cannot mask it.
                    const closedHard = sseTransportRef.current?.eventSource.readyState ===
                        EventSource.CLOSED;
                    // Disambiguate a concurrent-user-limit rejection (surface it) from a plain failure
                    // (let EventSource retry transient drops; re-create the stream on hard closes).
                    checkConnectionAvailability(sessionId)
                        .then((availability) => {
                        if (disposed)
                            return;
                        if (!availability.allowed) {
                            notifyConcurrentUserLimit(availability);
                            return;
                        }
                        if (closedHard) {
                            scheduleSseReconnect();
                        }
                    })
                        .catch(() => {
                        // Availability unknown — a permanently closed stream still must be replaced.
                        if (!disposed && closedHard) {
                            scheduleSseReconnect();
                        }
                    });
                },
                onSessionRevoked: () => {
                    if (disposed)
                        return;
                    sseTransportRef.current?.close();
                    sseTransportRef.current = null;
                    setActiveTransport(null);
                    onSessionRevokedRef.current?.();
                },
            });
            sseTransportRef.current = sse;
            setActiveTransport(sse);
            startWsReprobeTimer();
        };
        function fallbackToSse() {
            if (disposed || modeRef.current === "sse")
                return;
            if (!sseEnabledRef.current) {
                // SSE fallback disabled in settings — keep retrying the WebSocket with backoff instead
                // of leaving the tab without any transport.
                scheduleReconnect(socketInstanceRef.current);
                return;
            }
            connectSse();
        }
        ensureEditorRealtimeHandle();
        setReconnectHandler((reason) => {
            if (disposed)
                return;
            if (modeRef.current === "sse") {
                // Re-establish the stream for recovery callers, and treat the explicit request as an
                // opportunity to leave SSE mode if WebSockets have come back in the meantime.
                connectSse();
                probeWebSocket();
                return;
            }
            const ws = globalThis.editorSocket;
            if (ws &&
                (ws.readyState === WebSocket.OPEN ||
                    ws.readyState === WebSocket.CONNECTING)) {
                try {
                    ws.close(4000, reason);
                }
                catch {
                    /* ignore */
                }
                return;
            }
            // No live socket to close (already CLOSED, or never created because a connect attempt
            // latched) — no close event can drive recovery, so re-enter the connect path directly.
            if (reconnectTimeoutRef.current) {
                clearTimeout(reconnectTimeoutRef.current);
                reconnectTimeoutRef.current = null;
            }
            reconnectAttemptsRef.current = 0;
            startConnect();
        });
        const connect = async () => {
            if (disposed || modeRef.current === "sse")
                return;
            // WebSocket transport disabled in settings — skip straight to SSE (if it's enabled).
            if (!wsEnabledRef.current) {
                if (sseEnabledRef.current)
                    connectSse();
                return;
            }
            // Check connection availability before attempting WebSocket connection
            try {
                const availability = await checkConnectionAvailability(sessionId);
                if (disposed)
                    return;
                if (!availability.allowed) {
                    // Concurrent user limit — don't attempt or retry the connection; the limit recheck
                    // timer (and the dialog's Retry button) drive recovery from here.
                    notifyConcurrentUserLimit(availability);
                    return;
                }
                // Reset the flag if connection is allowed
                isConcurrentUserLimitRef.current = false;
            }
            catch (error) {
                console.error("Error checking connection availability:", error);
                // Continue with connection attempt if check fails
            }
            if (disposed)
                return;
            let socket = globalThis.editorSocket;
            const existingSocketSessionId = socket?.__parheliaSessionId;
            const needsNewSocket = !socket ||
                existingSocketSessionId !== sessionId ||
                socket.readyState === WebSocket.CLOSING ||
                socket.readyState === WebSocket.CLOSED;
            if (needsNewSocket) {
                if (socket &&
                    existingSocketSessionId &&
                    existingSocketSessionId !== sessionId &&
                    socket.readyState !== WebSocket.CLOSED &&
                    socket.readyState !== WebSocket.CLOSING) {
                    socket.close();
                }
                socket = connectSocket(sessionId);
                markSocketEvent("connect-attempt", socket);
                socket.__parheliaSessionId = sessionId;
                socket.addEventListener("open", () => {
                    if (disposed) {
                        return;
                    }
                    reconnectAttemptsRef.current = 0;
                    isConcurrentUserLimitRef.current = false;
                    clearLimitRecheckTimer();
                    hadOpenedRef.current = true;
                    modeRef.current = "websocket";
                    clearFallbackTimer();
                    markSocketEvent("open", socket, {
                        lastOpenAt: new Date().toISOString(),
                        lastCloseCode: null,
                        lastCloseReason: null,
                        lastErrorMessage: null,
                    });
                    onOpenRef.current?.();
                    sendClientInfoRef.current?.();
                    requestQuotaRef.current?.();
                });
                socket.addEventListener("close", (event) => {
                    if (disposed) {
                        return;
                    }
                    const isSessionRevoked = event.code === 1008 || event.reason === "session-revoked";
                    markSocketEvent("close", socket, {
                        lastCloseAt: new Date().toISOString(),
                        lastCloseCode: event.code,
                        lastCloseReason: event.reason || null,
                    });
                    // Don't retry if we hit concurrent user limit
                    if (isConcurrentUserLimitRef.current) {
                        return;
                    }
                    // For abnormal closes, re-check availability before reconnecting so
                    // concurrent-user-limit rejections do not get treated as transient failures.
                    const shouldCheckAvailability = event.code !== 1000;
                    if (isSessionRevoked) {
                        if (reconnectTimeoutRef.current) {
                            clearTimeout(reconnectTimeoutRef.current);
                            reconnectTimeoutRef.current = null;
                        }
                        onSessionRevokedRef.current?.();
                        return;
                    }
                    // A WebSocket that closes before it ever opened indicates the transport is unavailable
                    // in this environment (IIS feature missing, Upgrade stripped). Fall back to SSE for the
                    // page lifetime instead of retrying the WebSocket — but only after ruling out a
                    // concurrent-user-limit rejection.
                    if (!hadOpenedRef.current && modeRef.current !== "sse") {
                        checkConnectionAvailability(sessionId)
                            .then((availability) => {
                            if (disposed)
                                return;
                            if (!availability.allowed) {
                                notifyConcurrentUserLimit(availability);
                                return;
                            }
                            fallbackToSse();
                        })
                            .catch(() => fallbackToSse());
                        return;
                    }
                    if (shouldCheckAvailability) {
                        checkConnectionAvailability(sessionId)
                            .then((availability) => {
                            if (disposed)
                                return;
                            if (!availability.allowed) {
                                // Don't retry — the limit recheck timer takes over.
                                notifyConcurrentUserLimit(availability);
                                return;
                            }
                            // Not a concurrent user limit issue, proceed with retry.
                            scheduleReconnect(socket);
                        })
                            .catch(() => {
                            // If the availability check fails, fall back to the normal retry path.
                            scheduleReconnect(socket);
                        });
                        return;
                    }
                });
                socket.addEventListener("error", (error) => {
                    if (disposed) {
                        return;
                    }
                    const errorMessage = error instanceof ErrorEvent && error.message ? error.message : null;
                    markSocketEvent("error", socket, {
                        lastErrorAt: new Date().toISOString(),
                        lastErrorMessage: errorMessage,
                    });
                    onErrorRef.current?.(error);
                });
                globalThis.editorSocket = socket;
                // Expose this socket as the active realtime transport and arm the SSE fallback timer:
                // if it does not reach OPEN shortly, switch to SSE for the page lifetime.
                setActiveTransport(createWebSocketAdapter(socket));
                startFallbackTimer();
            }
            if (socket) {
                attach(socket, handleMessage, !needsNewSocket);
                if (!needsNewSocket) {
                    // Reused an already-open socket — expose it as the active transport.
                    modeRef.current = "websocket";
                    setActiveTransport(createWebSocketAdapter(socket));
                }
                if (socket.readyState === WebSocket.OPEN) {
                    hadOpenedRef.current = true;
                    clearFallbackTimer();
                    updateSocketDiagnostics({}, socket);
                    sendClientInfoRef.current?.();
                    requestQuotaRef.current?.();
                }
            }
        };
        // Call async connect function
        startConnect();
        return () => {
            disposed = true;
            if (reconnectTimeoutRef.current) {
                clearTimeout(reconnectTimeoutRef.current);
                reconnectTimeoutRef.current = null;
            }
            clearFallbackTimer();
            clearLimitRecheckTimer();
            clearSseReconnectTimer();
            clearWsReprobeTimer();
            // Settles (and closes) any in-flight WebSocket probe together with its timeout.
            wsProbeCleanupRef.current?.();
            setReconnectHandler(null);
            if (sseTransportRef.current) {
                sseTransportRef.current.close();
                sseTransportRef.current = null;
            }
            if (getEditorRealtime().mode() !== "websocket") {
                // Only clear the active transport if it isn't a live WebSocket we want to keep across
                // re-renders (the WS path intentionally persists globalThis.editorSocket).
                setActiveTransport(null);
            }
            modeRef.current = null;
            if (socketInstanceRef.current && attachedMessageHandlerRef.current) {
                socketInstanceRef.current.removeEventListener("message", attachedMessageHandlerRef.current);
                attachedMessageHandlerRef.current = null;
                socketInstanceRef.current = null;
            }
        };
    }, [sessionId, connectSocket, transportSettingsReady]);
    return { socketRef: socketInstanceRef };
}
//# sourceMappingURL=useEditorWebSocket.js.map