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";
/**
 * 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 close was actually issued. 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 already CLOSING/CLOSED there is nothing to do — the hook will replace it on its next
 * scheduleReconnect tick.
 */
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)
        return false;
    if (socket.readyState === WebSocket.OPEN ||
        socket.readyState === WebSocket.CONNECTING) {
        try {
            socket.close(4000, reason);
            return true;
        }
        catch {
            return false;
        }
    }
    return false;
}
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);
    // 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 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 for the rest of the page lifetime.
        const connectSse = () => {
            if (disposed)
                return;
            modeRef.current = "sse";
            clearFallbackTimer();
            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;
                    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. Disambiguate a concurrent-user-limit
                    // rejection (surface it) from a transient failure (let EventSource retry).
                    checkConnectionAvailability(sessionId)
                        .then((availability) => {
                        if (disposed || availability.allowed)
                            return;
                        isConcurrentUserLimitRef.current = true;
                        if (availability.currentUsers !== undefined &&
                            availability.maxUsers !== undefined &&
                            availability.message) {
                            onConcurrentUserLimitRef.current?.({
                                currentUsers: availability.currentUsers,
                                maxUsers: availability.maxUsers,
                                message: availability.message,
                            });
                        }
                    })
                        .catch(() => { });
                },
                onSessionRevoked: () => {
                    if (disposed)
                        return;
                    sseTransportRef.current?.close();
                    sseTransportRef.current = null;
                    setActiveTransport(null);
                    onSessionRevokedRef.current?.();
                },
            });
            sseTransportRef.current = sse;
            setActiveTransport(sse);
        };
        function fallbackToSse() {
            if (disposed || modeRef.current === "sse")
                return;
            if (!sseEnabledRef.current)
                return; // SSE fallback disabled in settings.
            connectSse();
        }
        ensureEditorRealtimeHandle();
        setReconnectHandler((reason) => {
            if (disposed)
                return;
            if (modeRef.current === "sse") {
                connectSse();
            }
            else {
                const ws = globalThis.editorSocket;
                try {
                    ws?.close(4000, reason);
                }
                catch {
                    /* ignore */
                }
            }
        });
        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) {
                    // Connection not allowed - likely concurrent user limit
                    isConcurrentUserLimitRef.current = true;
                    if (availability.currentUsers !== undefined &&
                        availability.maxUsers !== undefined &&
                        availability.message) {
                        onConcurrentUserLimitRef.current?.({
                            currentUsers: availability.currentUsers,
                            maxUsers: availability.maxUsers,
                            message: availability.message,
                        });
                    }
                    // Don't attempt connection or retry for concurrent user limit
                    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;
                    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) {
                                isConcurrentUserLimitRef.current = true;
                                if (availability.currentUsers !== undefined &&
                                    availability.maxUsers !== undefined &&
                                    availability.message) {
                                    onConcurrentUserLimitRef.current?.({
                                        currentUsers: availability.currentUsers,
                                        maxUsers: availability.maxUsers,
                                        message: availability.message,
                                    });
                                }
                                return;
                            }
                            fallbackToSse();
                        })
                            .catch(() => fallbackToSse());
                        return;
                    }
                    if (shouldCheckAvailability) {
                        checkConnectionAvailability(sessionId)
                            .then((availability) => {
                            if (!availability.allowed) {
                                isConcurrentUserLimitRef.current = true;
                                if (availability.currentUsers !== undefined &&
                                    availability.maxUsers !== undefined &&
                                    availability.message) {
                                    onConcurrentUserLimitRef.current?.({
                                        currentUsers: availability.currentUsers,
                                        maxUsers: availability.maxUsers,
                                        message: availability.message,
                                    });
                                }
                                return; // Don't retry
                            }
                            // 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
        connect().catch((error) => {
            console.error("Error in WebSocket connection:", error);
            onErrorRef.current?.(new Event("error"));
        });
        return () => {
            disposed = true;
            if (reconnectTimeoutRef.current) {
                clearTimeout(reconnectTimeoutRef.current);
                reconnectTimeoutRef.current = null;
            }
            clearFallbackTimer();
            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