import { runExpectation, } from "./expect.js";
import { extractVisibleText, queryByText, findClickableNearText, extractLinks, extractTweetCards, } from "./dom-helpers.js";
/**
 * Old interaction action names that should be treated as act kinds
 * for backward compatibility (existing skill scripts, etc.).
 */
const LEGACY_ACT_KINDS = new Set([
    "click",
    "hover",
    "scroll",
    "type",
    "press",
    "evaluate",
    "select",
    "wait",
]);
/**
 * Centralized failure detection. Any prefix added by an action method that
 * means "this didn't work" should appear here. Kept in one place so we
 * don't drift as actions add new error shapes.
 */
export function isFailureText(text) {
    if (!text)
        return true;
    const t = text.trimStart();
    return /^(Error[:\s]|Wait timeout:|Wait error:|Unknown browser action:|Unknown act kind:|\[verification failed\])/i.test(t);
}
function allowEvaluateFallbacks(actions) {
    return actions.allowEvaluateFallbacks();
}
function evaluateFallbackDisabled(feature) {
    return `Error: evaluate fallback disabled by browser.allowEvaluateFallbacks=false: ${feature}`;
}
/**
 * Same as `dispatchBrowserAction` but returns the structured result. Use
 * this from internal callers that need to know whether an action succeeded
 * without re-running the failure-prefix regex themselves.
 */
export async function dispatchBrowserActionDetailed(actions, action, args) {
    const text = await dispatchBrowserAction(actions, action, args);
    const expectationFailed = /\[verification failed\]/i.test(text);
    // An action that ran cleanly but failed its post-verify is NOT ok — the
    // user-visible task didn't happen. Both signals fold into a single ok bit
    // for downstream callers (script-runner, skill generation).
    const ok = !isFailureText(text) && !expectationFailed;
    return {
        ok,
        text,
        action,
        ...(expectationFailed ? { expectationFailed } : {}),
    };
}
/**
 * Shared browser action dispatcher.
 * Routes top-level actions and act interaction kinds to BrowserActions methods.
 * Used by agent.ts, task-runner.ts, and script-runner.ts.
 */
export async function dispatchBrowserAction(actions, action, args) {
    // Backward compat: if action is an old interaction name, treat as act kind
    if (LEGACY_ACT_KINDS.has(action) && !args.kind) {
        return dispatchActKind(actions, action, args);
    }
    // Backward compat: old close_tab/focus_tab names
    if (action === "close_tab")
        action = "close";
    if (action === "focus_tab")
        action = "focus";
    switch (action) {
        // --- Top-level actions ---
        case "navigate":
            return actions.navigate(args.url);
        case "snapshot": {
            const opts = {};
            if (args.mode)
                opts.mode = args.mode;
            if (args.selector != null)
                opts.selector = args.selector;
            if (args.compact != null)
                opts.compact = args.compact;
            if (args.depth != null)
                opts.depth = args.depth;
            if (args.maxChars != null)
                opts.maxChars = args.maxChars;
            if (args.snapshotFormat != null)
                opts.snapshotFormat = args.snapshotFormat;
            if (args.refs != null)
                opts.refs = args.refs;
            return actions.snapshot(Object.keys(opts).length > 0 ? opts : undefined);
        }
        case "screenshot": {
            // Same ref/selector split as act-kind dispatchers — a CSS selector
            // landing in `ref` should route through `element` instead of failing
            // ref lookup.
            const rawRef = args.ref;
            const refIsSnapshot = rawRef ? isSnapshotRef(rawRef) : false;
            const ss = await actions.screenshot({
                fullPage: args.fullPage,
                ref: refIsSnapshot ? rawRef : undefined,
                element: args.element ??
                    (rawRef && !refIsSnapshot ? rawRef : undefined),
                type: args.type,
                save: args.save,
                saveAs: args.saveAs,
            });
            return ss.text;
        }
        case "tabs":
            return actions.listTabs();
        case "open":
            return actions.openTab(args.url);
        case "focus":
            return actions.focusTab(args.index);
        case "close": {
            const index = args.index;
            if (index != null) {
                return actions.closeTab(index);
            }
            // No index: close active page
            return actions.closeActivePage();
        }
        case "console":
            return actions.console(args.level);
        case "dialog":
            return actions.dialog(args.accept, args.promptText, args.timeoutMs);
        case "upload": {
            // Same ref/selector split as the act-kind dispatchers. Skill replay
            // commonly persists a CSS selector (e.g. `input[type="file"]`) in
            // the ref slot for stability — without this split actions.upload
            // would route it through the aria-ref map and fail with "ref not
            // found".
            //
            // Priority when BOTH a snapshot ref and an explicit selector are
            // passed: the explicit selector wins. This honors the documented
            // contract ("explicit selector / inputSelector always win") and
            // protects against the case where a stale eN ref is paired with a
            // freshly-known stable selector — taking the ref path first would
            // fail with stale-ref while the selector would have worked.
            const rawRef = args.ref ??
                args.inputRef;
            const explicitSelector = args.selector ??
                args.inputSelector;
            const refIsSnapshot = rawRef ? isSnapshotRef(rawRef) : false;
            const finalRef = explicitSelector
                ? undefined
                : refIsSnapshot
                    ? rawRef
                    : undefined;
            const finalSelector = explicitSelector ?? (rawRef && !refIsSnapshot ? rawRef : undefined);
            return actions.upload(args.paths, finalRef, args.timeoutMs, finalSelector);
        }
        // --- act (interaction) ---
        // expect handling lives inside dispatchActKind itself, so legacy
        // top-level shapes ({ action: "click", ..., expect }) routed through
        // LEGACY_ACT_KINDS at the top of this function get verification too.
        case "act": {
            // Support nested request object (OpenClaw compat)
            const request = args.request;
            const merged = request && typeof request === "object"
                ? { ...args, ...request }
                : args;
            const kind = merged.kind;
            if (!kind)
                return "Error: act action requires a 'kind' parameter.";
            return dispatchActKind(actions, kind, merged);
        }
        // --- batch: run a sequence of browser actions in one tool call. ---
        // Stops on first failure. Useful when DOM state changes between
        // steps would otherwise stale refs across separate tool calls.
        //
        // Failure detection routes through `dispatchBrowserActionDetailed.ok`
        // — that bit folds in BOTH text-prefix failures (Error:/Wait timeout/
        // Unknown act kind) AND `[verification failed]` post-action expect
        // mismatches. A local regex would miss the second class, defeating
        // the purpose of running batch + expect together.
        //
        // The aggregate result is a single string. To make the OUTER
        // `dispatchBrowserActionDetailed` see batch failures (so the agent's
        // tool-call log records ok=false and ScriptRunner throws), we prefix
        // the joined output with `Error: batch failed at step N` whenever any
        // step failed. Without that prefix, isFailureText would only inspect
        // the first line ("[step 0/...]") and conclude success.
        case "batch": {
            const steps = args.steps;
            if (!Array.isArray(steps) || steps.length === 0) {
                return "Error: batch requires a non-empty `steps` array of {action, params?}.";
            }
            const stopOnFailure = args.stopOnFailure !== false;
            const lines = [];
            let firstFailure = null;
            let stoppedEarly = false;
            for (let i = 0; i < steps.length; i++) {
                const s = steps[i];
                if (!s || typeof s.action !== "string") {
                    lines.push(`[step ${i}] Error: missing action.`);
                    if (!firstFailure) {
                        firstFailure = { index: i, reason: "missing action" };
                    }
                    if (stopOnFailure) {
                        stoppedEarly = true;
                        break;
                    }
                    continue;
                }
                const r = await dispatchBrowserActionDetailed(actions, s.action, s.params ?? {});
                lines.push(`[step ${i}/${steps.length - 1} ${s.action}] ${r.text}`);
                if (!r.ok) {
                    if (!firstFailure) {
                        firstFailure = {
                            index: i,
                            reason: r.expectationFailed
                                ? "verification failed"
                                : "action failed",
                        };
                    }
                    if (stopOnFailure) {
                        stoppedEarly = true;
                        try {
                            const snap = await actions.snapshot({
                                mode: "interactive",
                                maxChars: 4000,
                            });
                            lines.push(`\n[batch failure snapshot]\n${snap}`);
                        }
                        catch {
                            // best-effort
                        }
                        break;
                    }
                }
            }
            const body = lines.join("\n\n");
            if (firstFailure) {
                // Prefix with a leading "Error:" so isFailureText catches it at the
                // outer Detailed wrapper. Without this, `r.ok` for the batch tool
                // call would be true even though one of its steps failed.
                const stoppedNote = stoppedEarly
                    ? ` (stopped early)`
                    : ` (continued through ${steps.length} steps)`;
                return (`Error: batch failed at step ${firstFailure.index}: ${firstFailure.reason}${stoppedNote}\n` +
                    body);
            }
            return body;
        }
        // --- verify: run expect block alone, without any preceding action. ---
        // Lets a caller assert "we should be in state X now" as a free-standing
        // step, e.g. after a multi-tool flow where the caller wants to confirm
        // before declaring success.
        case "verify": {
            const expect = args.expect ?? undefined;
            if (!expect)
                return "Error: verify requires an 'expect' object.";
            const page = await actions.getPage();
            const r = await runExpectation(page, expect);
            return r.ok
                ? `[verified] ${r.summary}`
                : `[verification failed] ${r.summary}`;
        }
        default:
            return `Unknown browser action: ${action}`;
    }
}
/**
 * Heuristic check that an act-kind dispatch returned a success string.
 * Once P2-3 introduces structured `BrowserActionResult`, this will be
 * replaced with a real `.ok` field — for now we string-match the prefixes
 * the existing methods emit. Keeping all prefixes in one place so the
 * coupling is at least centralized.
 */
function actionLooksSuccessful(result) {
    if (!result)
        return false;
    const trimmed = result.trimStart();
    if (trimmed.startsWith("Error"))
        return false;
    if (trimmed.startsWith("Wait timeout"))
        return false;
    if (trimmed.startsWith("Wait error"))
        return false;
    if (trimmed.startsWith("Unknown act kind"))
        return false;
    return true;
}
async function appendExpectationDetailed(actions, actionResult, expect, opts = {}) {
    if (!actionLooksSuccessful(actionResult)) {
        if (!opts.verifyFailedAction) {
            // Preserve original failure verbatim — running expect on top of most
            // failed actions would just produce noise.
            return { ok: false, text: actionResult };
        }
        const page = await actions.getPage();
        const r = await runExpectation(page, expect);
        if (r.ok) {
            return {
                ok: true,
                text: `[verified after failed action] ${r.summary}\n${actionResult}`,
                summary: r.summary,
            };
        }
        return {
            ok: false,
            text: `${actionResult}\n[verification failed] ${r.summary}`,
            summary: r.summary,
            expectationFailed: true,
        };
    }
    const page = await actions.getPage();
    const r = await runExpectation(page, expect);
    if (r.ok) {
        return {
            ok: true,
            text: `${actionResult}\n[verified] ${r.summary}`,
            summary: r.summary,
        };
    }
    // The action ran but the page didn't end up where the LLM expected.
    // We tell it explicitly so it doesn't move on assuming success.
    return {
        ok: false,
        text: `${actionResult}\n[verification failed] ${r.summary}`,
        summary: r.summary,
        expectationFailed: true,
    };
}
function shouldRetryClickWithDomFallback(kind, args, checked) {
    if (kind !== "click")
        return false;
    if (checked.ok)
        return false;
    if (!checked.expectationFailed && !isClickEventMissFailure(checked.text)) {
        return false;
    }
    if (args.fallbackDispatch !== "dom")
        return false;
    if (args.dispatch != null && args.dispatch !== "mouse")
        return false;
    if (args.doubleClick === true)
        return false;
    if (args.modifiers != null)
        return false;
    if (args.button != null && args.button !== "left")
        return false;
    return true;
}
function isClickEventMissFailure(text) {
    return /click event did not reach target/i.test(text);
}
/**
 * True only for actual snapshot refs (e1, e2, ...) and role-refs ("role|name").
 * Anything else looks like a CSS selector and should route through the
 * `selector` parameter — this happens in practice when skill replay
 * persists data-testid in the `ref` field for stability.
 */
function isSnapshotRef(s) {
    if (/^e\d+$/.test(s))
        return true;
    // Role-ref: "role|name" — first half must be a plain role keyword,
    // and there must be no CSS-selector specials in the role part.
    if (/^[a-z]+\|/.test(s))
        return true;
    return false;
}
/**
 * Look at args and split `ref` into either a snapshot ref or a CSS selector
 * based on its shape. Skill replay (and direct LLM calls that copy stable
 * selectors into the ref slot) commonly emit `[data-testid="..."]` as ref;
 * forwarding that to actions.click(ref) would route through the aria-ref
 * map, find nothing, and report "ref not found". Detecting the shape here
 * fixes the entire act-path replay surface in one place.
 *
 * Returns a tuple [refForActions, selectorForActions]. If the user passed
 * an explicit `selector` already, we never overwrite it.
 */
function splitRefOrSelector(args) {
    const rawRef = args.ref;
    const rawSelector = args.selector;
    if (rawRef && !isSnapshotRef(rawRef)) {
        return { ref: undefined, selector: rawSelector ?? rawRef };
    }
    return { ref: rawRef, selector: rawSelector };
}
/**
 * Same trick for drag's startRef/endRef pair.
 */
function splitDragRefs(args) {
    const sR = args.startRef;
    const eR = args.endRef;
    const sS = args.startSelector;
    const eS = args.endSelector;
    return {
        startRef: sR && isSnapshotRef(sR) ? sR : undefined,
        endRef: eR && isSnapshotRef(eR) ? eR : undefined,
        startSelector: sS ?? (sR && !isSnapshotRef(sR) ? sR : undefined),
        endSelector: eS ?? (eR && !isSnapshotRef(eR) ? eR : undefined),
    };
}
/**
 * Dispatch an interaction kind to the appropriate BrowserActions method,
 * then run any post-action `expect` block. By keeping the expectation
 * step here (rather than in `case "act"` only), every entry point that
 * routes to dispatchActKind gets verification for free — this covers
 * LEGACY_ACT_KINDS top-level shapes like `{ action: "click", ref:
 * "...", expect: {...} }` that older skills still emit.
 */
async function dispatchActKind(actions, kind, args) {
    const actionResult = await runActKindCore(actions, kind, args);
    const expect = args.expect;
    if (!expect)
        return actionResult;
    const checked = await appendExpectationDetailed(actions, actionResult, expect, {
        verifyFailedAction: kind === "click" && isClickEventMissFailure(actionResult),
    });
    if (shouldRetryClickWithDomFallback(kind, args, checked)) {
        if (!allowEvaluateFallbacks(actions)) {
            return [
                checked.text,
                `[evaluate fallback disabled] fallbackDispatch="dom" is disabled by browser.allowEvaluateFallbacks=false`,
            ].join("\n");
        }
        const fallbackActionResult = await runActKindCore(actions, kind, {
            ...args,
            dispatch: "dom",
        });
        const fallbackChecked = await appendExpectationDetailed(actions, fallbackActionResult, expect);
        if (fallbackChecked.ok) {
            return [
                checked.expectationFailed
                    ? `[mouse verification failed; retried with dom dispatch] ${checked.summary}`
                    : "[mouse event-miss; retried with dom dispatch]",
                actionResult,
                fallbackChecked.text,
            ].join("\n");
        }
        return [
            checked.text,
            "[dom fallback attempted]",
            fallbackChecked.text,
        ].join("\n");
    }
    return checked.text;
}
/**
 * The pure switch-on-kind: routes each interaction kind to the
 * corresponding BrowserActions method. No expect handling here — that's
 * the wrapper's job.
 */
async function runActKindCore(actions, kind, args) {
    switch (kind) {
        case "click": {
            const opts = {};
            if (args.doubleClick != null)
                opts.doubleClick = args.doubleClick;
            if (args.button != null)
                opts.button = args.button;
            if (args.modifiers != null)
                opts.modifiers = args.modifiers;
            if (args.dispatch != null) {
                if (args.dispatch !== "mouse" && args.dispatch !== "dom") {
                    return `Error: click dispatch must be "mouse" or "dom", got ${JSON.stringify(args.dispatch)}`;
                }
                if (args.dispatch === "dom" && !allowEvaluateFallbacks(actions)) {
                    return evaluateFallbackDisabled('click dispatch="dom"');
                }
                opts.dispatch = args.dispatch;
            }
            const { ref, selector } = splitRefOrSelector(args);
            return actions.click(ref, Object.keys(opts).length > 0 ? opts : undefined, selector);
        }
        case "type": {
            const opts = {};
            if (args.clear != null)
                opts.clear = args.clear;
            if (args.submit != null)
                opts.submit = args.submit;
            if (args.method != null)
                opts.method = args.method;
            if (args.fallback != null)
                opts.fallback = args.fallback;
            const { ref, selector } = splitRefOrSelector(args);
            return actions.type(ref, args.text, Object.keys(opts).length > 0 ? opts : undefined, selector);
        }
        case "press":
            return actions.pressKey(args.key, args.delayMs);
        case "hover": {
            const { ref, selector } = splitRefOrSelector(args);
            return actions.hover(ref, selector);
        }
        case "scroll": {
            const opts = {};
            if (args.direction != null)
                opts.direction = args.direction;
            if (args.amount != null)
                opts.amount = args.amount;
            if (args.deltaX != null)
                opts.deltaX = args.deltaX;
            if (args.deltaY != null)
                opts.deltaY = args.deltaY;
            const { ref, selector } = splitRefOrSelector(args);
            return actions.scroll(ref, Object.keys(opts).length > 0 ? opts : undefined, selector);
        }
        case "drag": {
            const split = splitDragRefs(args);
            return actions.drag(split.startRef, split.endRef, split.startSelector, split.endSelector);
        }
        case "select": {
            // Backward compat: accept single `value` or array `values`
            const values = args.values ??
                (args.value ? [args.value] : []);
            const { ref, selector } = splitRefOrSelector(args);
            return actions.selectOption(ref, values, selector);
        }
        case "fill":
            return actions.fill(args.fields);
        case "resize":
            return actions.resize(args.width, args.height);
        case "wait": {
            const waitOpts = {};
            if (args.selector != null)
                waitOpts.selector = args.selector;
            if (args.text != null)
                waitOpts.text = args.text;
            if (args.textGone != null)
                waitOpts.textGone = args.textGone;
            if (args.url != null)
                waitOpts.url = args.url;
            if (args.loadState != null)
                waitOpts.loadState = args.loadState;
            if (args.fn != null)
                waitOpts.fn = args.fn;
            if (args.timeMs != null)
                waitOpts.timeMs = args.timeMs;
            if (args.timeoutMs != null)
                waitOpts.timeoutMs = args.timeoutMs;
            // Backward compat: accept old `timeout` name
            if (args.timeout != null && args.timeoutMs == null)
                waitOpts.timeoutMs = args.timeout;
            // P1-2 additions
            if (args.selectorVisible != null)
                waitOpts.selectorVisible = args.selectorVisible;
            if (args.selectorHidden != null)
                waitOpts.selectorHidden = args.selectorHidden;
            if (args.selectorEnabled != null)
                waitOpts.selectorEnabled = args.selectorEnabled;
            if (args.selectorDisabled != null)
                waitOpts.selectorDisabled = args.selectorDisabled;
            if (args.urlMatches != null)
                waitOpts.urlMatches = args.urlMatches;
            if (args.domQuietMs != null)
                waitOpts.domQuietMs = args.domQuietMs;
            if (args.locatorStableMs != null)
                waitOpts.locatorStableMs = args.locatorStableMs;
            if (args.textCountAtLeast != null)
                waitOpts.textCountAtLeast =
                    args.textCountAtLeast;
            return actions.wait(waitOpts);
        }
        case "evaluate":
            if (!allowEvaluateFallbacks(actions)) {
                return evaluateFallbackDisabled("act kind evaluate");
            }
            // Accept both `fn` (OpenClaw) and `script` (legacy dipclaw)
            return actions.evaluate(args.fn ?? args.script);
        // --- High-level DOM helpers (P1-4). Each replaces a recurring evaluate
        // pattern that was costing 1-2 round-trips to a syntax error. ---
        case "extractVisibleText": {
            const page = await actions.getPage();
            return extractVisibleText(page, {
                selector: args.selector,
                maxChars: args.maxChars,
            });
        }
        case "queryByText": {
            const page = await actions.getPage();
            return queryByText(page, {
                text: args.text,
                fuzzy: args.fuzzy,
                limit: args.limit,
            });
        }
        case "findClickableNearText": {
            const page = await actions.getPage();
            return findClickableNearText(page, {
                text: args.text,
                maxAncestors: args.maxAncestors,
                limit: args.limit,
            });
        }
        case "extractLinks": {
            const page = await actions.getPage();
            return extractLinks(page, {
                selector: args.selector,
                limit: args.limit,
            });
        }
        case "extractTweetCards": {
            const page = await actions.getPage();
            return extractTweetCards(page, {
                limit: args.limit,
            });
        }
        case "close":
            return actions.closeActivePage();
        default:
            return `Unknown act kind: ${kind}`;
    }
}
//# sourceMappingURL=dispatch.js.map