import { formatCliCommand } from "../cli/command-format.js";
import { loadConfig } from "../config/config.js";
import { getBridgeAuthForPort } from "./bridge-auth-registry.js";
import { resolveBrowserControlAuth } from "./control-auth.js";
import { createBrowserControlContext, startBrowserControlServiceFromConfig, } from "./control-service.js";
import { createBrowserRouteDispatcher } from "./routes/dispatcher.js";
function isAbsoluteHttp(url) {
    return /^https?:\/\//i.test(url.trim());
}
function isLoopbackHttpUrl(url) {
    try {
        const host = new URL(url).hostname.trim().toLowerCase();
        // URL hostnames may keep IPv6 brackets (for example "[::1]"); normalize before checks.
        const normalizedHost = host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
        return (normalizedHost === "127.0.0.1" || normalizedHost === "localhost" || normalizedHost === "::1");
    }
    catch {
        return false;
    }
}
function withLoopbackBrowserAuthImpl(url, init, deps) {
    const headers = new Headers(init?.headers ?? {});
    if (headers.has("authorization") || headers.has("x-openclaw-password")) {
        return { ...init, headers };
    }
    if (!isLoopbackHttpUrl(url)) {
        return { ...init, headers };
    }
    try {
        const cfg = deps.loadConfig();
        const auth = deps.resolveBrowserControlAuth(cfg);
        if (auth.token) {
            headers.set("Authorization", `Bearer ${auth.token}`);
            return { ...init, headers };
        }
        if (auth.password) {
            headers.set("x-openclaw-password", auth.password);
            return { ...init, headers };
        }
    }
    catch {
        // ignore config/auth lookup failures and continue without auth headers
    }
    // Sandbox bridge servers can run with per-process ephemeral auth on dynamic ports.
    // Fall back to the in-memory registry if config auth is not available.
    try {
        const parsed = new URL(url);
        const port = parsed.port && Number.parseInt(parsed.port, 10) > 0
            ? Number.parseInt(parsed.port, 10)
            : parsed.protocol === "https:"
                ? 443
                : 80;
        const bridgeAuth = deps.getBridgeAuthForPort(port);
        if (bridgeAuth?.token) {
            headers.set("Authorization", `Bearer ${bridgeAuth.token}`);
        }
        else if (bridgeAuth?.password) {
            headers.set("x-openclaw-password", bridgeAuth.password);
        }
    }
    catch {
        // ignore
    }
    return { ...init, headers };
}
function withLoopbackBrowserAuth(url, init) {
    return withLoopbackBrowserAuthImpl(url, init, {
        loadConfig,
        resolveBrowserControlAuth,
        getBridgeAuthForPort,
    });
}
function enhanceBrowserFetchError(url, err, timeoutMs) {
    const isLocal = !isAbsoluteHttp(url);
    // Human-facing hint for logs/diagnostics.
    const operatorHint = isLocal
        ? `Restart the OpenClaw-CN gateway (OpenClaw-CN.app menubar, or \`${formatCliCommand("openclaw-cn gateway")}\`).`
        : "If this is a sandboxed session, ensure the sandbox browser is running.";
    // Model-facing suffix: explicitly tell the LLM NOT to retry.
    // Without this, models see "try again" and enter an infinite tool-call loop.
    const modelHint = "Do NOT retry the browser tool — it will keep failing. " +
        "Use an alternative approach or inform the user that the browser is currently unavailable.";
    const msg = String(err);
    const msgLower = msg.toLowerCase();
    const looksLikeTimeout = msgLower.includes("timed out") ||
        msgLower.includes("timeout") ||
        msgLower.includes("aborted") ||
        msgLower.includes("abort") ||
        msgLower.includes("aborterror");
    if (looksLikeTimeout) {
        return new Error(`Can't reach the OpenClaw-CN browser control service (timed out after ${timeoutMs}ms). ${operatorHint} ${modelHint}`);
    }
    return new Error(`Can't reach the OpenClaw-CN browser control service. ${operatorHint} ${modelHint} (${msg})`);
}
async function fetchHttpJson(url, init) {
    const timeoutMs = init.timeoutMs ?? 5000;
    const ctrl = new AbortController();
    const upstreamSignal = init.signal;
    let upstreamAbortListener;
    if (upstreamSignal) {
        if (upstreamSignal.aborted) {
            ctrl.abort(upstreamSignal.reason);
        }
        else {
            upstreamAbortListener = () => ctrl.abort(upstreamSignal.reason);
            upstreamSignal.addEventListener("abort", upstreamAbortListener, { once: true });
        }
    }
    const t = setTimeout(() => ctrl.abort(new Error("timed out")), timeoutMs);
    try {
        const res = await fetch(url, { ...init, signal: ctrl.signal });
        if (!res.ok) {
            const text = await res.text().catch(() => "");
            throw new Error(text || `HTTP ${res.status}`);
        }
        return (await res.json());
    }
    finally {
        clearTimeout(t);
        if (upstreamSignal && upstreamAbortListener) {
            upstreamSignal.removeEventListener("abort", upstreamAbortListener);
        }
    }
}
export async function fetchBrowserJson(url, init) {
    const timeoutMs = init?.timeoutMs ?? 5000;
    try {
        if (isAbsoluteHttp(url)) {
            const httpInit = withLoopbackBrowserAuth(url, init);
            return await fetchHttpJson(url, { ...httpInit, timeoutMs });
        }
        const started = await startBrowserControlServiceFromConfig();
        if (!started) {
            throw new Error("browser control disabled");
        }
        const dispatcher = createBrowserRouteDispatcher(createBrowserControlContext());
        const parsed = new URL(url, "http://localhost");
        const query = {};
        for (const [key, value] of parsed.searchParams.entries()) {
            query[key] = value;
        }
        let body = init?.body;
        if (typeof body === "string") {
            try {
                body = JSON.parse(body);
            }
            catch {
                // keep as string
            }
        }
        const abortCtrl = new AbortController();
        const upstreamSignal = init?.signal;
        let upstreamAbortListener;
        if (upstreamSignal) {
            if (upstreamSignal.aborted) {
                abortCtrl.abort(upstreamSignal.reason);
            }
            else {
                upstreamAbortListener = () => abortCtrl.abort(upstreamSignal.reason);
                upstreamSignal.addEventListener("abort", upstreamAbortListener, { once: true });
            }
        }
        let abortListener;
        const abortPromise = abortCtrl.signal.aborted
            ? Promise.reject(abortCtrl.signal.reason ?? new Error("aborted"))
            : new Promise((_, reject) => {
                abortListener = () => reject(abortCtrl.signal.reason ?? new Error("aborted"));
                abortCtrl.signal.addEventListener("abort", abortListener, { once: true });
            });
        let timer;
        if (timeoutMs) {
            timer = setTimeout(() => abortCtrl.abort(new Error("timed out")), timeoutMs);
        }
        const dispatchPromise = dispatcher.dispatch({
            method: init?.method?.toUpperCase() === "DELETE"
                ? "DELETE"
                : init?.method?.toUpperCase() === "POST"
                    ? "POST"
                    : "GET",
            path: parsed.pathname,
            query,
            body,
            signal: abortCtrl.signal,
        });
        const result = await Promise.race([dispatchPromise, abortPromise]).finally(() => {
            if (timer) {
                clearTimeout(timer);
            }
            if (abortListener) {
                abortCtrl.signal.removeEventListener("abort", abortListener);
            }
            if (upstreamSignal && upstreamAbortListener) {
                upstreamSignal.removeEventListener("abort", upstreamAbortListener);
            }
        });
        if (result.status >= 400) {
            const message = result.body && typeof result.body === "object" && "error" in result.body
                ? String(result.body.error)
                : `HTTP ${result.status}`;
            throw new Error(message);
        }
        return result.body;
    }
    catch (err) {
        throw enhanceBrowserFetchError(url, err, timeoutMs);
    }
}
export const __test = {
    withLoopbackBrowserAuth: withLoopbackBrowserAuthImpl,
};
