import { useCallback, useEffect, useRef } from "react";
import { DIALOG_TYPES, } from "./agentDialogTypes";
import { useEditContext } from "../../client/editContext";
import { capturePageDom } from "./capturePageDom";
import { capturePageScreenshot } from "./capturePageScreenshot";
import { post } from "../../services/serviceHelper";
function getResponseLogPayload(response) {
    if (!response.resultJson || response.resultJson.length <= 1000) {
        return response;
    }
    return {
        ...response,
        resultJson: `[omitted ${response.resultJson.length} chars]`,
    };
}
function normalizeDialogAgentId(value) {
    return value?.trim().toLowerCase() || "";
}
function hasDialogExpiry(request) {
    return request.dialogType !== "questionnaire";
}
function isAutomaticDialogRequest(request) {
    return (request.dialogType === DIALOG_TYPES.CAPTURE_PAGE_SCREENSHOT ||
        request.dialogType === DIALOG_TYPES.CAPTURE_PAGE_DOM);
}
function getVisibleDialogRegistry() {
    const registry = globalThis.__agentDialogVisibleCallbacks;
    return registry && typeof registry === "object" ? registry : {};
}
function getVisibleDialogEntry(agentId) {
    const rawValue = getVisibleDialogRegistry()[agentId];
    if (!rawValue) {
        return null;
    }
    if (typeof rawValue === "string") {
        return {
            callbackId: rawValue,
            terminalInstanceId: null,
        };
    }
    if (typeof rawValue === "object") {
        return {
            callbackId: typeof rawValue.callbackId === "string" ? rawValue.callbackId : null,
            terminalInstanceId: typeof rawValue.terminalInstanceId === "string"
                ? rawValue.terminalInstanceId
                : null,
        };
    }
    return null;
}
function getMountedDialogAgentIds() {
    const mountedAgentsRaw = globalThis?.__agentDialogMountedAgents;
    return Array.isArray(mountedAgentsRaw)
        ? mountedAgentsRaw.map((id) => normalizeDialogAgentId(id))
        : [];
}
function canDispatchToMountedTerminal(request) {
    if (isAutomaticDialogRequest(request)) {
        return true;
    }
    const targetAgentId = normalizeDialogAgentId(request.agentId);
    if (!targetAgentId) {
        return true;
    }
    return getMountedDialogAgentIds().includes(targetAgentId);
}
/**
 * AgentDialogHandler - Central coordinator for agent dialog requests
 *
 * This component listens for agent dialog requests via WebSocket messages
 * and dispatches them to the appropriate handler (e.g., AgentTerminal for inline dialogs).
 * When a response is received, it sends it back to the agent.
 */
export function AgentDialogHandler() {
    const editContext = useEditContext();
    // Queue for pending requests with retry tracking
    const pendingRequestsRef = useRef([]);
    const isProcessingRef = useRef(false);
    const currentRequestRef = useRef(null);
    const dialogAcceptedRef = useRef(false);
    const retryTimeoutRef = useRef(null);
    // Stable refs for callbacks used by the terminal:ready listener,
    // so the listener can be registered once and never torn down.
    const dispatchDialogRef = useRef(() => { });
    const processNextRequestRef = useRef(() => { });
    const clearRetryTimeoutRef = useRef(() => { });
    // Max retries for initial dispatch (short-term)
    // After these retries, the request stays pending until the terminal is ready
    const MAX_INITIAL_RETRIES = 5;
    const RETRY_DELAYS = [500, 1000, 1500, 2000, 2500]; // ms
    // Requests that were never shown should not linger forever in the queue.
    // Once a dialog was accepted by a terminal, keep it alive up to the long-term timeout
    // so switching tasks/terminals does not silently cancel the user's pending questionnaire.
    const STALE_UNACCEPTED_THRESHOLD_MS = 60_000;
    // Long-term timeout: cancel requests older than 10 minutes
    const LONG_TERM_TIMEOUT_MS = 10 * 60 * 1000;
    /**
     * Send a dialog response back to the agent via HTTP
     */
    const sendResponse = useCallback((response) => {
        if (!editContext) {
            console.error("[AgentDialogHandler] Cannot submit dialog response: editContext is undefined");
            return;
        }
        console.log("[AgentDialogHandler] Sending response:", getResponseLogPayload(response));
        void (async () => {
            const result = await post("/parhelia/agent/submitDialogResponse", response, editContext.sessionId);
            if (result.type !== "success") {
                console.error("[AgentDialogHandler] Failed to submit dialog response:", result.summary, result.details);
                editContext.showToast(result.summary || "Failed to submit dialog response");
                return;
            }
            if (result.data?.success === false) {
                console.warn("[AgentDialogHandler] Dialog response was not accepted by the server:", response.callbackId);
            }
        })();
    }, [editContext]);
    /**
     * Clean up retry timeout
     */
    const clearRetryTimeout = useCallback(() => {
        if (retryTimeoutRef.current) {
            clearTimeout(retryTimeoutRef.current);
            retryTimeoutRef.current = null;
        }
    }, []);
    const completeAutomaticDialog = useCallback(async (pending) => {
        const { request } = pending;
        clearRetryTimeout();
        dialogAcceptedRef.current = true;
        pending.hasBeenAccepted = true;
        try {
            const result = request.dialogType === DIALOG_TYPES.CAPTURE_PAGE_DOM
                ? await capturePageDom(request, request.parameters ?? {}, editContext)
                : await capturePageScreenshot(request, request.parameters ?? {}, editContext);
            if (!result.success && result.error) {
                editContext?.showToast?.(result.error);
            }
            const response = {
                callbackId: request.callbackId,
                cancelled: false,
                resultJson: JSON.stringify(result),
            };
            sendResponse(response);
        }
        catch (error) {
            const message = error instanceof Error
                ? error.message
                : "Failed to complete the automatic capture request.";
            const scope = request.dialogType === DIALOG_TYPES.CAPTURE_PAGE_DOM
                ? "page"
                : "viewport";
            const response = {
                callbackId: request.callbackId,
                cancelled: false,
                resultJson: JSON.stringify({
                    success: false,
                    scope,
                    error: message,
                }),
            };
            sendResponse(response);
        }
        finally {
            currentRequestRef.current = null;
            isProcessingRef.current = false;
            dialogAcceptedRef.current = false;
            setTimeout(() => processNextRequestRef.current(), 100);
        }
    }, [clearRetryTimeout, editContext, sendResponse]);
    /**
     * Dispatch the dialog to AgentTerminal
     */
    const dispatchDialog = useCallback((pending) => {
        const { request, retryCount } = pending;
        dialogAcceptedRef.current = false;
        if (isAutomaticDialogRequest(request)) {
            void completeAutomaticDialog(pending);
            return;
        }
        const mountedAgentListeners = getMountedDialogAgentIds();
        const targetAgentId = normalizeDialogAgentId(request.agentId);
        const hasTargetListener = !!targetAgentId && mountedAgentListeners.includes(targetAgentId);
        if (targetAgentId && !hasTargetListener) {
            const alreadyQueued = pendingRequestsRef.current.some((queued) => queued.request.callbackId === pending.request.callbackId);
            if (!alreadyQueued) {
                pendingRequestsRef.current.unshift(pending);
            }
            currentRequestRef.current = null;
            isProcessingRef.current = false;
            console.log("[AgentDialogHandler] Target terminal is not mounted yet, keeping dialog pending:", targetAgentId);
            const hasAnotherDispatchableRequest = pendingRequestsRef.current.some((queued) => queued.request.callbackId !== pending.request.callbackId &&
                canDispatchToMountedTerminal(queued.request));
            if (hasAnotherDispatchableRequest) {
                processNextRequest();
            }
            return;
        }
        console.log(`[AgentDialogHandler] Dispatching dialog request (attempt ${retryCount + 1}):`, request);
        window.dispatchEvent(new CustomEvent("agent:dialog:show", {
            detail: {
                request,
                onComplete: (result) => {
                    clearRetryTimeout();
                    dialogAcceptedRef.current = true;
                    const response = {
                        callbackId: request.callbackId,
                        cancelled: result === null,
                        resultJson: result !== null ? JSON.stringify(result) : undefined,
                    };
                    sendResponse(response);
                    currentRequestRef.current = null;
                    isProcessingRef.current = false;
                    setTimeout(() => processNextRequest(), 100);
                },
                onCancel: () => {
                    clearRetryTimeout();
                    dialogAcceptedRef.current = true;
                    const response = {
                        callbackId: request.callbackId,
                        cancelled: true,
                    };
                    sendResponse(response);
                    currentRequestRef.current = null;
                    isProcessingRef.current = false;
                    setTimeout(() => processNextRequest(), 100);
                },
            },
        }));
        // Schedule retry if not accepted within timeout
        const delay = RETRY_DELAYS[Math.min(retryCount, RETRY_DELAYS.length - 1)];
        retryTimeoutRef.current = setTimeout(() => {
            if (!dialogAcceptedRef.current && currentRequestRef.current) {
                const pendingRequest = currentRequestRef.current;
                // Check for long-term timeout (10 minutes)
                const elapsed = Date.now() - pendingRequest.createdAt;
                if (elapsed > LONG_TERM_TIMEOUT_MS) {
                    console.warn(`[AgentDialogHandler] Dialog request timed out after ${Math.round(elapsed / 1000)}s, cancelling`);
                    const response = {
                        callbackId: request.callbackId,
                        cancelled: true,
                        error: "Dialog request timed out - no handler responded",
                    };
                    sendResponse(response);
                    currentRequestRef.current = null;
                    isProcessingRef.current = false;
                    setTimeout(() => processNextRequest(), 100);
                    return;
                }
                if (retryCount < MAX_INITIAL_RETRIES) {
                    console.log(`[AgentDialogHandler] Dialog not accepted, retrying (${retryCount + 1}/${MAX_INITIAL_RETRIES})...`);
                    currentRequestRef.current.retryCount++;
                    dispatchDialog(currentRequestRef.current);
                }
                else {
                    const parked = currentRequestRef.current;
                    currentRequestRef.current = null;
                    isProcessingRef.current = false;
                    if (!hasDialogExpiry(request)) {
                        parked.retryCount = 0;
                        pendingRequestsRef.current.push(parked);
                        console.log(`[AgentDialogHandler] Re-queued non-expiring dialog after ${MAX_INITIAL_RETRIES} retries (agentId: ${request.agentId})`);
                        const hasAnotherDispatchableRequest = pendingRequestsRef.current.some((queued) => queued.request.callbackId !== parked.request.callbackId &&
                            canDispatchToMountedTerminal(queued.request));
                        if (hasAnotherDispatchableRequest) {
                            processNextRequest();
                        }
                        return;
                    }
                    else {
                        console.log(`[AgentDialogHandler] Dialog not accepted after ${MAX_INITIAL_RETRIES} retries, parking stale request (agentId: ${request.agentId})`);
                    }
                    processNextRequest();
                }
            }
        }, delay);
    }, [sendResponse, clearRetryTimeout, completeAutomaticDialog]);
    /**
     * Process the next dialog request in the queue
     */
    const processNextRequest = useCallback(() => {
        if (isProcessingRef.current)
            return;
        // Purge stale requests from the queue.
        // - Never-accepted requests expire quickly so dropped dialogs do not leak forever.
        // - Accepted/paused requests stay alive until the long-term timeout so unanswered
        //   questionnaires survive task switches and delayed user responses.
        const now = Date.now();
        pendingRequestsRef.current = pendingRequestsRef.current.filter((p) => {
            const elapsed = now - p.createdAt;
            if (hasDialogExpiry(p.request) && elapsed > LONG_TERM_TIMEOUT_MS) {
                sendResponse({
                    callbackId: p.request.callbackId,
                    cancelled: true,
                    error: "Dialog request timed out - no handler responded",
                });
                return false;
            }
            if (hasDialogExpiry(p.request) &&
                !p.hasBeenAccepted &&
                elapsed > STALE_UNACCEPTED_THRESHOLD_MS) {
                sendResponse({
                    callbackId: p.request.callbackId,
                    cancelled: true,
                    error: "Dialog request expired",
                });
                return false;
            }
            return true;
        });
        if (pendingRequestsRef.current.length === 0)
            return;
        const nextPendingIndex = pendingRequestsRef.current.findIndex((pending) => canDispatchToMountedTerminal(pending.request));
        if (nextPendingIndex === -1) {
            return;
        }
        isProcessingRef.current = true;
        const removed = pendingRequestsRef.current.splice(nextPendingIndex, 1);
        const pending = removed[0];
        if (!pending) {
            isProcessingRef.current = false;
            return;
        }
        currentRequestRef.current = pending;
        dispatchDialog(pending);
    }, [dispatchDialog, sendResponse]);
    // Keep stable refs pointing to latest callbacks for the mount-only terminal:ready listener
    dispatchDialogRef.current = dispatchDialog;
    processNextRequestRef.current = processNextRequest;
    clearRetryTimeoutRef.current = clearRetryTimeout;
    /**
     * Handle incoming dialog request from WebSocket
     */
    const handleDialogRequest = useCallback((request) => {
        console.log("[AgentDialogHandler] Received dialog request:", request);
        // Check if this request is already pending (avoid duplicates on reconnect)
        const existingIndex = pendingRequestsRef.current.findIndex((p) => p.request.callbackId === request.callbackId);
        if (existingIndex >= 0) {
            console.log("[AgentDialogHandler] Request already pending, skipping duplicate:", request.callbackId);
            return;
        }
        // Also check if it's the current request being processed
        if (currentRequestRef.current?.request.callbackId === request.callbackId) {
            console.log("[AgentDialogHandler] Request already being processed, skipping duplicate:", request.callbackId);
            return;
        }
        // Clear stale current request that has been sitting unprocessed too long
        if (currentRequestRef.current &&
            hasDialogExpiry(currentRequestRef.current.request) &&
            !dialogAcceptedRef.current &&
            !currentRequestRef.current.hasBeenAccepted &&
            Date.now() - currentRequestRef.current.createdAt >
                STALE_UNACCEPTED_THRESHOLD_MS) {
            sendResponse({
                callbackId: currentRequestRef.current.request.callbackId,
                cancelled: true,
                error: "Dialog request expired",
            });
            currentRequestRef.current = null;
            isProcessingRef.current = false;
            clearRetryTimeout();
        }
        // If a new request arrives while another dialog is currently accepted,
        // pause the current one instead of cancelling it. This lets users switch
        // between tasks/terminals without implicitly rejecting an in-progress
        // questionnaire or inline dialog.
        if (currentRequestRef.current && dialogAcceptedRef.current) {
            const pausedCurrent = currentRequestRef.current;
            console.log("[AgentDialogHandler] Pausing current dialog on new request arrival:", pausedCurrent.request.callbackId);
            currentRequestRef.current = null;
            isProcessingRef.current = false;
            dialogAcceptedRef.current = false;
            clearRetryTimeout();
            const pausedExists = pendingRequestsRef.current.some((p) => p.request.callbackId === pausedCurrent.request.callbackId);
            if (!pausedExists) {
                pendingRequestsRef.current.unshift(pausedCurrent);
            }
        }
        // If incoming request is for a terminal that is already mounted and current
        // request belongs to another agent, pause/requeue current and dispatch
        // the new request directly so the visible terminal picks it up immediately.
        if (currentRequestRef.current &&
            normalizeDialogAgentId(currentRequestRef.current.request.agentId) !==
                normalizeDialogAgentId(request.agentId) &&
            !!normalizeDialogAgentId(request.agentId) &&
            canDispatchToMountedTerminal(request)) {
            const pausedCurrent = currentRequestRef.current;
            clearRetryTimeout();
            currentRequestRef.current = null;
            isProcessingRef.current = false;
            dialogAcceptedRef.current = false;
            const pausedExists = pendingRequestsRef.current.some((p) => p.request.callbackId === pausedCurrent.request.callbackId);
            if (!pausedExists) {
                pendingRequestsRef.current.push(pausedCurrent);
            }
            const newPending = {
                request,
                retryCount: 0,
                createdAt: Date.now(),
                hasBeenAccepted: false,
            };
            currentRequestRef.current = newPending;
            isProcessingRef.current = true;
            dispatchDialog(newPending);
            return;
        }
        pendingRequestsRef.current.push({
            request,
            retryCount: 0,
            createdAt: Date.now(),
            hasBeenAccepted: false,
        });
        processNextRequest();
    }, [processNextRequest, sendResponse, clearRetryTimeout]);
    /**
     * Listen for dialog requests via custom event
     * (dispatched by useSocketMessageHandler)
     */
    useEffect(() => {
        const handleEvent = (event) => {
            handleDialogRequest(event.detail);
        };
        window.addEventListener("agent:dialog:request", handleEvent);
        return () => {
            window.removeEventListener("agent:dialog:request", handleEvent);
            // Clean up retry timeout on unmount
            clearRetryTimeout();
        };
    }, [handleDialogRequest, clearRetryTimeout]);
    /**
     * Listen for dialog acceptance confirmation from AgentTerminal
     * This helps stop retries immediately when a dialog is accepted
     */
    useEffect(() => {
        const handleAccepted = (event) => {
            if (currentRequestRef.current?.request.callbackId ===
                event.detail.callbackId) {
                console.log("[AgentDialogHandler] Dialog accepted:", event.detail.callbackId);
                currentRequestRef.current.hasBeenAccepted = true;
                dialogAcceptedRef.current = true;
                clearRetryTimeout();
            }
        };
        window.addEventListener("agent:dialog:accepted", handleAccepted);
        return () => {
            window.removeEventListener("agent:dialog:accepted", handleAccepted);
        };
    }, [clearRetryTimeout]);
    /**
     * Listen for orphaned dialogs -- when a terminal unmounts while displaying a
     * dialog, it fires this event so the handler can re-dispatch to the next terminal.
     * Uses refs for the same reason as the terminal:ready listener above.
     */
    useEffect(() => {
        const handleOrphaned = (event) => {
            const { callbackId } = event.detail;
            if (currentRequestRef.current?.request.callbackId === callbackId &&
                dialogAcceptedRef.current) {
                const targetAgentId = normalizeDialogAgentId(currentRequestRef.current.request.agentId);
                const mountedRaw = globalThis?.__agentDialogMountedAgents;
                const mounted = Array.isArray(mountedRaw)
                    ? mountedRaw.map((id) => normalizeDialogAgentId(id))
                    : [];
                const hasListener = !!targetAgentId && mounted.includes(targetAgentId);
                if (hasListener) {
                    dialogAcceptedRef.current = false;
                    currentRequestRef.current.retryCount = 0;
                    dispatchDialogRef.current(currentRequestRef.current);
                }
                else {
                    clearRetryTimeoutRef.current();
                    isProcessingRef.current = false;
                    dialogAcceptedRef.current = false;
                }
            }
        };
        window.addEventListener("agent:dialog:orphaned", handleOrphaned);
        return () => {
            window.removeEventListener("agent:dialog:orphaned", handleOrphaned);
        };
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);
    /**
     * Listen for agent terminal ready events
     * When a terminal mounts (e.g., user switches to an agent), re-dispatch any pending dialogs
     *
     * Uses stable refs for callbacks so the listener is registered once on mount
     * and never torn down mid-render — avoids a race where the event fires between
     * effect cleanup and re-registration.
     */
    useEffect(() => {
        const handleTerminalReady = (event) => {
            const agentId = normalizeDialogAgentId(event.detail.agentId);
            const readyTerminalInstanceId = typeof event.detail.terminalInstanceId === "string"
                ? event.detail.terminalInstanceId
                : null;
            console.log("[AgentDialogHandler] Terminal ready for agent:", agentId);
            // If another agent terminal becomes ready while current request belongs to
            // a different agent, try to preempt current in favor of a queued request
            // for the ready agent. Keep paused request in queue instead of cancelling.
            if (currentRequestRef.current &&
                normalizeDialogAgentId(currentRequestRef.current.request.agentId) !==
                    agentId) {
                const queuedForReadyAgentIndex = pendingRequestsRef.current.findIndex((p) => normalizeDialogAgentId(p.request.agentId) === agentId);
                if (queuedForReadyAgentIndex !== -1) {
                    const queuedForReadyAgent = pendingRequestsRef.current[queuedForReadyAgentIndex];
                    if (queuedForReadyAgent) {
                        const pausedCurrent = currentRequestRef.current;
                        clearRetryTimeoutRef.current();
                        isProcessingRef.current = false;
                        currentRequestRef.current = null;
                        dialogAcceptedRef.current = false;
                        pendingRequestsRef.current.splice(queuedForReadyAgentIndex, 1);
                        const pausedExists = pendingRequestsRef.current.some((p) => p.request.callbackId === pausedCurrent.request.callbackId);
                        if (!pausedExists) {
                            pendingRequestsRef.current.unshift(pausedCurrent);
                        }
                        currentRequestRef.current = queuedForReadyAgent;
                        isProcessingRef.current = true;
                        dispatchDialogRef.current(queuedForReadyAgent);
                        return;
                    }
                }
            }
            // Check if there's a current pending request for this agent
            if (currentRequestRef.current &&
                normalizeDialogAgentId(currentRequestRef.current.request.agentId) ===
                    agentId) {
                const visibleEntry = getVisibleDialogEntry(agentId);
                const visibleCallbackId = visibleEntry?.callbackId ?? null;
                const visibleTerminalInstanceId = visibleEntry?.terminalInstanceId ?? null;
                const currentCallbackId = currentRequestRef.current.request.callbackId || null;
                if (dialogAcceptedRef.current) {
                    const isSameVisibleTerminal = visibleCallbackId === currentCallbackId &&
                        (!readyTerminalInstanceId ||
                            !visibleTerminalInstanceId ||
                            visibleTerminalInstanceId === readyTerminalInstanceId);
                    if (isSameVisibleTerminal) {
                        return;
                    }
                    dialogAcceptedRef.current = false;
                }
                console.log("[AgentDialogHandler] Re-dispatching pending dialog for agent:", agentId);
                currentRequestRef.current.retryCount = 0;
                isProcessingRef.current = true;
                dispatchDialogRef.current(currentRequestRef.current);
                return;
            }
            // Process pending requests for the ready agent
            if (!isProcessingRef.current &&
                !currentRequestRef.current &&
                pendingRequestsRef.current.length > 0) {
                processNextRequestRef.current();
            }
        };
        window.addEventListener("agent:terminal:ready", handleTerminalReady);
        return () => {
            window.removeEventListener("agent:terminal:ready", handleTerminalReady);
        };
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);
    // This component doesn't render anything - dialogs are shown inline in AgentTerminal
    return null;
}
export default AgentDialogHandler;
//# sourceMappingURL=AgentDialogHandler.js.map