"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { create } from "@bufbuild/protobuf";
import { InitiateOAuthConnectInputSchema, CompleteOAuthConnectInputSchema, ConnectInputSchema, } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/io_pb";
import { useStigmer } from "../hooks";
import { resolveDeclaredSystemEnvVars } from "../environment/systemEnvVars";
import { toError } from "../internal/toError";
/**
 * Message type posted by {@link OAuthCallbackHandler} to the opener window.
 *
 * @internal
 */
export const OAUTH_CALLBACK_MESSAGE_TYPE = "stigmer:oauth:callback";
/**
 * BroadcastChannel name used as a fallback when `window.opener` is severed
 * by `Cross-Origin-Opener-Policy` headers on the OAuth provider.
 *
 * @internal
 */
export const OAUTH_BROADCAST_CHANNEL = "stigmer:oauth:broadcast";
const POPUP_WIDTH = 600;
const POPUP_HEIGHT = 700;
const POPUP_CALLBACK_TIMEOUT_MS = 120_000;
/**
 * Action hook that orchestrates the full OAuth popup flow for MCP servers.
 *
 * Handles the complete lifecycle:
 * 1. Calls `initiateOAuthConnect` to get the authorization URL
 * 2. Opens a popup window to the OAuth consent screen
 * 3. Listens for the callback via `window.postMessage`
 * 4. Calls `completeOAuthConnect` to exchange the code for tokens
 * 5. Chains to `connect` for tool discovery
 *
 * The popup is opened **synchronously** before the `initiateOAuthConnect`
 * RPC to avoid browser popup blockers. A blank page is shown briefly
 * while the RPC resolves, then the popup navigates to the auth URL.
 *
 * @example
 * ```tsx
 * const oauth = useMcpServerOAuthConnect();
 * const { refetch } = useMcpServer(org, slug);
 *
 * async function handleSignIn() {
 *   try {
 *     await oauth.startOAuth(mcpServer.metadata.id, org);
 *     refetch();
 *   } catch {
 *     // error is available via oauth.error
 *   }
 * }
 *
 * <button onClick={handleSignIn} disabled={oauth.isInProgress}>
 *   {oauth.isInProgress ? "Signing in..." : "Sign in with OAuth"}
 * </button>
 * ```
 */
export function useMcpServerOAuthConnect() {
    const stigmer = useStigmer();
    const [phase, setPhase] = useState("idle");
    const [error, setError] = useState(null);
    const popupRef = useRef(null);
    const cleanupRef = useRef(null);
    const cancelledRef = useRef(false);
    useEffect(() => {
        return () => {
            cleanupRef.current?.();
        };
    }, []);
    const clearError = useCallback(() => {
        if (cleanupRef.current || popupRef.current) {
            cancelledRef.current = true;
            cleanupRef.current?.();
            closePopup(popupRef.current);
            popupRef.current = null;
            cleanupRef.current = null;
        }
        setPhase("idle");
        setError(null);
    }, []);
    const startOAuth = useCallback(async (mcpServerId, org, declaredEnvKeys) => {
        setPhase("initiating");
        setError(null);
        cancelledRef.current = false;
        cleanupRef.current?.();
        const left = window.screenX + (window.outerWidth - POPUP_WIDTH) / 2;
        const top = window.screenY + (window.outerHeight - POPUP_HEIGHT) / 2;
        const popup = window.open("about:blank", "stigmer_oauth", `width=${POPUP_WIDTH},height=${POPUP_HEIGHT},left=${left},top=${top},popup=yes`);
        if (!popup) {
            const blocked = new Error("Your browser blocked the authentication popup. " +
                "Please allow popups for this site and try again.");
            setError(blocked);
            setPhase("idle");
            throw blocked;
        }
        popupRef.current = popup;
        try {
            const initOutput = await stigmer.mcpServer.initiateOAuthConnect(create(InitiateOAuthConnectInputSchema, { mcpServerId, org }));
            popup.location.href = initOutput.authorizationUrl;
            setPhase("awaiting-callback");
            const { code, state } = await waitForOAuthCallback(popup, initOutput.state, (dispose) => {
                cleanupRef.current = dispose;
            });
            setPhase("completing");
            await stigmer.mcpServer.completeOAuthConnect(create(CompleteOAuthConnectInputSchema, {
                mcpServerId,
                authorizationCode: code,
                state,
            }));
            setPhase("connecting");
            const systemEnv = declaredEnvKeys
                ? await resolveDeclaredSystemEnvVars(stigmer, declaredEnvKeys)
                : {};
            const runtimeEnvMap = {};
            for (const [key, envInput] of Object.entries(systemEnv)) {
                runtimeEnvMap[key] = {
                    value: envInput.value,
                    isSecret: envInput.isSecret ?? false,
                };
            }
            const input = create(ConnectInputSchema, {
                mcpServerId,
                org,
                ...(Object.keys(runtimeEnvMap).length > 0
                    ? { runtimeEnv: runtimeEnvMap }
                    : {}),
            });
            const server = await stigmer.mcpServer.connect(input);
            setPhase("done");
            return server;
        }
        catch (err) {
            const wrapped = toError(err);
            if (!cancelledRef.current) {
                setError(wrapped);
                setPhase("idle");
                closePopup(popup);
            }
            cancelledRef.current = false;
            throw wrapped;
        }
        finally {
            popupRef.current = null;
            cleanupRef.current = null;
        }
    }, [stigmer]);
    return {
        startOAuth,
        isInProgress: phase !== "idle" && phase !== "done",
        phase,
        error,
        clearError,
    };
}
// ---------------------------------------------------------------------------
// Popup callback listener
// ---------------------------------------------------------------------------
/**
 * Grace period (ms) after `popup.closed` is first detected before treating
 * it as a user-initiated close.
 *
 * Only used when BroadcastChannel is unavailable. When BC is available,
 * `popup.closed` polling is skipped entirely because COOP providers
 * (e.g. Sentry, GitHub) sever the opener reference on cross-origin
 * navigation, making `popup.closed` permanently `true` while the popup
 * is still active. The overall {@link POPUP_CALLBACK_TIMEOUT_MS} serves
 * as the safety net for abandoned flows instead.
 */
const POPUP_CLOSED_GRACE_MS = 5_000;
function waitForOAuthCallback(popup, expectedState, onDispose) {
    return new Promise((resolve, reject) => {
        let settled = false;
        let timeoutId;
        let pollId;
        let bc = null;
        let hasBroadcastChannel = false;
        function cleanup() {
            if (timeoutId)
                clearTimeout(timeoutId);
            if (pollId)
                clearInterval(pollId);
            window.removeEventListener("message", onMessage);
            try {
                bc?.close();
            }
            catch { /* ignore */ }
        }
        function settle(outcome) {
            if (settled)
                return;
            settled = true;
            cleanup();
            if (outcome instanceof Error) {
                reject(outcome);
            }
            else {
                resolve(outcome);
            }
        }
        onDispose(() => {
            settle(new Error("OAuth flow was cancelled."));
            closePopup(popup);
        });
        function validateAndSettle(data) {
            if (data?.type !== OAUTH_CALLBACK_MESSAGE_TYPE)
                return;
            if (data.state !== expectedState) {
                settle(new Error("OAuth state mismatch — the callback did not match the " +
                    "initiated flow. Please try again."));
                return;
            }
            if (!data.code) {
                settle(new Error("No authorization code received from the OAuth provider."));
                return;
            }
            settle({ code: data.code, state: data.state });
        }
        function onMessage(event) {
            if (event.origin !== window.location.origin)
                return;
            validateAndSettle(event.data);
        }
        window.addEventListener("message", onMessage);
        // BroadcastChannel — works even when COOP severs window.opener.
        try {
            bc = new BroadcastChannel(OAUTH_BROADCAST_CHANNEL);
            hasBroadcastChannel = true;
            bc.onmessage = (event) => {
                validateAndSettle(event.data);
            };
        }
        catch {
            // BroadcastChannel unsupported — rely on postMessage only.
        }
        timeoutId = setTimeout(() => {
            settle(new Error("OAuth authentication timed out. Ensure your callback page " +
                "renders <OAuthCallbackHandler /> from @stigmer/react at " +
                "the URL configured as your OAuth redirect URI."));
            closePopup(popup);
        }, POPUP_CALLBACK_TIMEOUT_MS);
        // When BroadcastChannel is available, skip popup.closed polling.
        // COOP providers (Sentry, GitHub, etc.) sever the opener reference
        // on cross-origin navigation, making popup.closed permanently true
        // while the popup is still active. BroadcastChannel reliably
        // delivers the callback regardless of COOP; the overall timeout
        // above catches abandoned flows.
        //
        // When BroadcastChannel is NOT available (legacy browsers), fall
        // back to popup.closed polling with a short grace period — it is
        // the only signal we have in that degraded path.
        let popupClosedAt = null;
        pollId = setInterval(() => {
            if (hasBroadcastChannel)
                return;
            if (popup.closed) {
                if (popupClosedAt === null) {
                    popupClosedAt = Date.now();
                }
                else if (Date.now() - popupClosedAt > POPUP_CLOSED_GRACE_MS) {
                    settle(new Error("The authentication window was closed before completing sign-in."));
                }
            }
            else {
                popupClosedAt = null;
            }
        }, 500);
    });
}
function closePopup(popup) {
    try {
        popup?.close();
    }
    catch {
        // Cross-origin popup may throw on close — safe to ignore.
    }
}
//# sourceMappingURL=useMcpServerOAuthConnect.js.map