import { resolvePageAndSite } from "../../services/contentService";
import { BridgeClient } from "../../bridge/BridgeClient";
import { alignLoopbackHostToEditor, alignXmCloudManagedEditingHostToEditor, applyParheliaRenderContext, getEditorQueryParam, } from "../../page-viewer/renderUrl";
import { captureScreenshotFromBridgeSource, } from "./capturePageScreenshot";
import { createCaptureRequestId, createHiddenCaptureIframe, readTitleFromHtml, } from "./captureShared";
// Offscreen render defaults to a desktop-width surface so background captures reflect a normal page
// layout; agents can override via the viewportWidth/viewportHeight tool parameters (e.g. a mobile width).
const DEFAULT_VIEWPORT_WIDTH = 1280;
const DEFAULT_VIEWPORT_HEIGHT = 900;
const MIN_VIEWPORT = 240;
const MAX_VIEWPORT_WIDTH = 3840;
const MAX_VIEWPORT_HEIGHT = 4320;
const BRIDGE_READY_TIMEOUT_MS = 20_000;
const DOM_STABILITY_QUIET_PERIOD_MS = 1_000;
const DOM_STABILITY_TIMEOUT_MS = 6_000;
function clampViewportValue(value, fallback, max) {
    if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
        return fallback;
    }
    return Math.min(max, Math.max(MIN_VIEWPORT, Math.round(value)));
}
/**
 * Resolves the offscreen render viewport from the capture parameters, clamping to a sane range and
 * falling back to the desktop defaults. Pure and unit-testable.
 */
export function resolveOffscreenViewport(params) {
    return {
        width: clampViewportValue(params?.viewportWidth, DEFAULT_VIEWPORT_WIDTH, MAX_VIEWPORT_WIDTH),
        height: clampViewportValue(params?.viewportHeight, DEFAULT_VIEWPORT_HEIGHT, MAX_VIEWPORT_HEIGHT),
    };
}
function normalizeItemId(value) {
    return value?.trim().toLowerCase() || "";
}
/**
 * Decides whether a capture request should render its target page in a hidden iframe. True only for a
 * background capture whose target page differs from the page this session currently has open (when a
 * helper session already has the target page open, the cheaper live-page capture is used instead).
 */
export function shouldRenderTargetOffscreen(params) {
    const { backgroundCapture, targetItemId, openItemId } = params;
    if (backgroundCapture !== true || !targetItemId)
        return false;
    if (!openItemId)
        return true;
    return normalizeItemId(openItemId) !== normalizeItemId(targetItemId);
}
/**
 * Builds the URL a hidden iframe loads to render the target page in preview mode with the Parhelia
 * editing bridge enabled. Pure aside from `window.location` host alignment, so it is unit-testable.
 */
export function buildOffscreenRenderUrl(params) {
    const { previewUrl, descriptor, site, helperSessionId } = params;
    const renderUrl = new URL(previewUrl, window.location.origin);
    alignLoopbackHostToEditor(renderUrl);
    alignXmCloudManagedEditingHostToEditor(renderUrl);
    applyParheliaRenderContext(renderUrl, descriptor, site || getEditorQueryParam("site"));
    // Force the editing bridge to be injected even though this is a preview render, and bind it to the
    // helper's live session so the host (AddPlaceholderMarker / personalization session check) accepts it.
    renderUrl.searchParams.set("parhelia", "1");
    if (helperSessionId) {
        renderUrl.searchParams.set("parhelia_session", helperSessionId);
    }
    renderUrl.searchParams.set("edit_rev", createCaptureRequestId("edit-rev"));
    return renderUrl.toString();
}
async function resolveOffscreenRender(target, helperSessionId) {
    if (!target?.itemId) {
        return {
            error: "The capture request is missing a target item, so the page cannot be rendered offscreen.",
        };
    }
    // Host/base-URL alignment and access checks belong to the editor's request context, so the preview
    // URL is resolved here (not server-side) via the canonical resolve stack. This also enforces the
    // helper/owner's read access to the item.
    const resolved = await resolvePageAndSite(target.itemId, target.language ?? "en", target.version ?? 0);
    if (!resolved?.previewUrl) {
        return {
            error: "Could not resolve a preview URL for the target page (it may not exist or you may not have access).",
        };
    }
    const site = resolved.site?.name ?? target.site;
    const descriptor = resolved.pageItem;
    return {
        url: buildOffscreenRenderUrl({
            previewUrl: resolved.previewUrl,
            descriptor,
            site,
            helperSessionId,
        }),
        site,
        descriptor,
    };
}
function createDomStabilityWaiter(quietPeriodMs = DOM_STABILITY_QUIET_PERIOD_MS, timeoutMs = DOM_STABILITY_TIMEOUT_MS) {
    let updateCount = 0;
    let lastUpdateAt = 0;
    let quietTimer;
    let timeoutTimer;
    let resolveWait;
    let waitPromise;
    const clearTimers = () => {
        if (quietTimer !== undefined) {
            window.clearTimeout(quietTimer);
            quietTimer = undefined;
        }
        if (timeoutTimer !== undefined) {
            window.clearTimeout(timeoutTimer);
            timeoutTimer = undefined;
        }
    };
    const finish = () => {
        if (!resolveWait)
            return;
        const resolve = resolveWait;
        resolveWait = undefined;
        clearTimers();
        resolve();
    };
    const scheduleQuietPeriod = () => {
        if (!resolveWait || updateCount === 0)
            return;
        if (quietTimer !== undefined) {
            window.clearTimeout(quietTimer);
        }
        const elapsedSinceLastUpdate = Math.max(0, Date.now() - lastUpdateAt);
        quietTimer = window.setTimeout(finish, Math.max(0, quietPeriodMs - elapsedSinceLastUpdate));
    };
    return {
        notifyUpdate() {
            updateCount += 1;
            lastUpdateAt = Date.now();
            scheduleQuietPeriod();
        },
        wait() {
            if (!waitPromise) {
                waitPromise = new Promise((resolve) => {
                    resolveWait = resolve;
                    timeoutTimer = window.setTimeout(finish, timeoutMs);
                    scheduleQuietPeriod();
                });
            }
            return waitPromise;
        },
        dispose() {
            clearTimers();
            resolveWait = undefined;
        },
    };
}
export const __offscreenPageCaptureTesting__ = {
    createDomStabilityWaiter,
    DOM_STABILITY_QUIET_PERIOD_MS,
    DOM_STABILITY_TIMEOUT_MS,
};
/**
 * Creates a hidden iframe, drives a transient BridgeClient against it, waits for the page to render
 * and settle, runs `fn`, then always tears down the bridge + iframe.
 */
async function withOffscreenPageBridge(url, helperSessionId, requestedViewport, fn) {
    if (!document.body) {
        throw new Error("The Parhelia UI has not finished loading yet.");
    }
    const iframe = createHiddenCaptureIframe(requestedViewport.width, requestedViewport.height);
    let latestGeometry;
    let latestDom;
    let viewport = {
        width: requestedViewport.width,
        height: requestedViewport.height,
    };
    const domStabilityWaiter = createDomStabilityWaiter();
    let readyResolve;
    let readyReject;
    const bridge = new BridgeClient({
        iframe,
        editorOrigin: window.location.origin,
        allowedHostOrigins: [safeOrigin(url)],
        readyTimeoutMs: BRIDGE_READY_TIMEOUT_MS,
        onReadyChange: (ready) => {
            if (ready)
                readyResolve?.();
        },
        onError: (error) => {
            readyReject?.(error);
        },
        onEvent: (event) => {
            if (event.name === "ready") {
                const payload = event.payload;
                if (payload.viewport)
                    viewport = payload.viewport;
            }
            else if (event.name === "geometryUpdated") {
                latestGeometry = event.payload
                    .geometry;
            }
            else if (event.name === "domUpdated") {
                latestDom = event.payload;
                if (latestDom.viewport)
                    viewport = latestDom.viewport;
                domStabilityWaiter.notifyUpdate();
            }
        },
    });
    iframe.src = url;
    document.body.appendChild(iframe);
    try {
        bridge.connect();
        await new Promise((resolve, reject) => {
            readyResolve = resolve;
            readyReject = reject;
            if (bridge.isReady())
                resolve();
        });
        // Mirror the live editor: send `init` so the host starts emitting structure/geometry/dom
        // immediately instead of waiting out its init-fallback timer.
        bridge.sendCommand("init", {
            editorOrigin: window.location.origin,
            sessionId: helperSessionId,
            mode: "preview",
            selectedComponentIds: [],
            version: bridge.getDiagnostics().editorVersion,
            acknowledgedCapabilities: bridge.getAcknowledgedCapabilities(),
        });
        // The bridge debounces DOM snapshots after mutations. Wait until no new snapshot arrives for a
        // full quiet period so client-rendered components are included, while retaining a hard deadline
        // for pages that continuously update or never emit a DOM snapshot.
        await domStabilityWaiter.wait();
        return await fn({
            bridge,
            iframe,
            getGeometry: () => latestGeometry,
            getDom: () => latestDom,
            getViewport: () => viewport,
        });
    }
    finally {
        domStabilityWaiter.dispose();
        try {
            bridge.disconnect();
        }
        catch {
            // ignore teardown errors
        }
        iframe.remove();
    }
}
function safeOrigin(url) {
    try {
        return new URL(url, window.location.origin).origin;
    }
    catch {
        return window.location.origin;
    }
}
async function requestDomFromBridge(state) {
    const result = await state.bridge.requestCommand("captureDom", { requestId: createCaptureRequestId("capture-dom") }, "captureDomResult", { timeoutMs: 10_000 });
    if (result.error) {
        throw new Error(result.error);
    }
    const html = result.html ?? state.getDom()?.html ?? "";
    if (!html.trim()) {
        throw new Error("The rendered page DOM was empty.");
    }
    return html;
}
/**
 * Renders the target page in a hidden iframe and returns its DOM. Used for background/taskboard agents
 * that have no attached browser and no page open.
 */
export async function capturePageDomOffscreen(target, helperSessionId, viewport) {
    const resolved = await resolveOffscreenRender(target, helperSessionId);
    if ("error" in resolved) {
        return { success: false, scope: "page", error: resolved.error };
    }
    try {
        const html = await withOffscreenPageBridge(resolved.url, helperSessionId, viewport, (state) => requestDomFromBridge(state));
        return {
            success: true,
            scope: "page",
            html,
            length: html.length,
            documentTitle: readTitleFromHtml(html),
        };
    }
    catch (error) {
        return {
            success: false,
            scope: "page",
            error: error instanceof Error
                ? error.message
                : "Failed to render the target page offscreen for DOM capture.",
        };
    }
}
function normalizeBackgroundScreenshotScope(scope) {
    // editor-ui/viewport/selection are meaningless for an offscreen render (no editor surface, no user
    // selection), so default those to a full-page shot. Component scopes resolve via bridge geometry.
    switch (scope) {
        case "component":
        case "component-range":
        case "page":
            return scope;
        default:
            return "page";
    }
}
/**
 * Renders the target page in a hidden iframe and returns a screenshot. Reuses the shared bridge
 * screenshot pipeline (captureScreenshotFromBridgeSource) against the transient offscreen bridge.
 */
export async function capturePageScreenshotOffscreen(params, target, helperSessionId, viewport) {
    if (params?.scope === "editor-ui") {
        return {
            success: false,
            scope: "editor-ui",
            error: "The Parhelia editor UI can only be captured from the live editor session.",
        };
    }
    const scope = normalizeBackgroundScreenshotScope(params?.scope);
    const resolved = await resolveOffscreenRender(target, helperSessionId);
    if ("error" in resolved) {
        return { success: false, scope, error: resolved.error };
    }
    try {
        const result = await withOffscreenPageBridge(resolved.url, helperSessionId, viewport, async (state) => {
            const source = {
                bridgeReady: true,
                bridgeGeometry: state.getGeometry(),
                bridgeDom: state.getDom(),
                viewport: state.getViewport(),
                editorIframe: state.iframe,
                requestBridgeCaptureDom: (payload) => state.bridge.requestCommand("captureDom", payload, "captureDomResult", {
                    timeoutMs: 10_000,
                }),
            };
            return captureScreenshotFromBridgeSource(source, { ...params, scope }, [], scope);
        });
        return (result ?? {
            success: false,
            scope,
            error: "The offscreen page bridge was not ready to capture a screenshot.",
        });
    }
    catch (error) {
        return {
            success: false,
            scope,
            error: error instanceof Error
                ? error.message
                : "Failed to render the target page offscreen for screenshot capture.",
        };
    }
}
//# sourceMappingURL=offscreenPageCapture.js.map