/**
 * Interactive automation + output operations for the browser plugin.
 *
 * Every function here drives the one persistent page held by a BrowserSession
 * (cdp-session.ts). They never throw to the MCP layer — each returns a
 * structured ActionResult so the agent always sees a named outcome instead of a
 * 500. Element targeting is by CSS selector run through Runtime.evaluate (raw
 * CDP has no Playwright selector engine); the snapshot tool emits a unique CSS
 * selector per interactive node so the agent clicks exactly what it read.
 *
 *   ensure() live? ──no──▶ outcome "session-lost"  ("call browser-navigate first")
 *        │yes                 (only navigate() re-attaches a dead page)
 *        ▼
 *   Runtime.evaluate / Input.* / Page.* ──▶ structured ActionResult
 */
import { closeTarget, listTargets, } from "./cdp-session.js";
const ok = (data) => ({ ok: true, outcome: "ok", data });
const fail = (outcome, detail) => ({ ok: false, outcome, detail });
const SESSION_LOST = fail("session-lost", "no live page — call browser-navigate first");
/** Resolve the live session or a session-lost result. */
function live(session) {
    const s = session.ensure();
    return s ?? SESSION_LOST;
}
function isResult(v) {
    return v.outcome !== undefined && typeof v.ok === "boolean";
}
/** Run an expression in the page, returning its value or the thrown error text. */
async function evalValue(s, expression, timeoutMs, awaitPromise = false) {
    let res;
    try {
        res = (await s.command("Runtime.evaluate", { expression, returnByValue: true, awaitPromise }, timeoutMs));
    }
    catch (err) {
        return { errorText: err instanceof Error ? err.message : String(err) };
    }
    if (res?.exceptionDetails) {
        const ex = res.exceptionDetails;
        return { errorText: ex.exception?.description ?? ex.text ?? "evaluate threw" };
    }
    return { value: res?.result?.value };
}
/** Wrap a body operating on `el` so the result distinguishes not-found from ok. */
function selectorScript(selector, body) {
    return `(() => { const el = document.querySelector(${JSON.stringify(selector)}); if (!el) return { nf: true }; ${body} return { ok: true }; })()`;
}
/** Run a selector-scoped body; map the {nf} sentinel to selector-not-found. */
async function actOnSelector(s, selector, body, timeoutMs) {
    const r = await evalValue(s, selectorScript(selector, body), timeoutMs);
    if ("errorText" in r)
        return fail("evaluate-failed", r.errorText);
    const v = r.value;
    if (v?.nf)
        return fail("selector-not-found", `no element matches ${selector}`);
    return ok();
}
// ── navigate ────────────────────────────────────────────────────────────────
export async function navigate(session, params, ctx) {
    let s = session.ensure();
    if (!s) {
        const att = await session.attach(ctx.commandTimeoutMs);
        if ("outcome" in att)
            return fail(att.outcome, att.detail);
        s = att.session;
    }
    const loadTimeoutMs = params.loadTimeoutMs ?? ctx.loadTimeoutMs;
    const loadPromise = s.waitForEvent("Page.loadEventFired", loadTimeoutMs);
    try {
        await s.command("Page.navigate", { url: params.url }, ctx.commandTimeoutMs);
    }
    catch (err) {
        return fail("navigate-failed", err instanceof Error ? err.message : String(err));
    }
    try {
        await loadPromise;
    }
    catch (err) {
        return fail("load-timeout", err instanceof Error ? err.message : String(err));
    }
    return ok({ url: params.url });
}
// ── snapshot ──────────────────────────────────────────────────────────────
const MAX_SNAPSHOT_NODES = 400;
/** In-page accessibility snapshot: role + accessible name + a unique CSS
 *  selector for each interactive/landmark element, computed in one evaluate. */
const SNAPSHOT_SCRIPT = `(() => {
  function uniqueSelector(el) {
    if (el.id) { const s = '#' + CSS.escape(el.id); try { if (document.querySelectorAll(s).length === 1) return s; } catch (e) {} }
    const parts = [];
    while (el && el.nodeType === 1 && el !== document.documentElement) {
      let part = el.tagName.toLowerCase();
      if (el.id) { try { const s = '#' + CSS.escape(el.id); if (document.querySelectorAll(s).length === 1) { parts.unshift(s); break; } } catch (e) {} }
      const parent = el.parentElement;
      if (parent) {
        const same = Array.prototype.filter.call(parent.children, c => c.tagName === el.tagName);
        if (same.length > 1) part += ':nth-of-type(' + (same.indexOf(el) + 1) + ')';
      }
      parts.unshift(part);
      el = el.parentElement;
    }
    return parts.join(' > ');
  }
  function roleOf(el) {
    const r = el.getAttribute('role'); if (r) return r;
    const t = el.tagName.toLowerCase();
    if (t === 'a' && el.hasAttribute('href')) return 'link';
    if (t === 'button') return 'button';
    if (t === 'input') { const ty = (el.getAttribute('type') || 'text').toLowerCase();
      if (['button','submit','reset','image'].indexOf(ty) >= 0) return 'button';
      if (ty === 'checkbox') return 'checkbox'; if (ty === 'radio') return 'radio';
      return 'textbox'; }
    if (t === 'select') return 'combobox';
    if (t === 'textarea') return 'textbox';
    if (/^h[1-6]$/.test(t)) return 'heading';
    if (t === 'nav') return 'navigation'; if (t === 'main') return 'main';
    return null;
  }
  function nameOf(el) {
    const n = el.getAttribute('aria-label') || el.getAttribute('alt') || el.getAttribute('placeholder') || el.getAttribute('title') || (el.innerText || el.textContent || '');
    return n.replace(/\\s+/g, ' ').trim().slice(0, 120);
  }
  const out = [];
  const all = document.querySelectorAll('*');
  for (let i = 0; i < all.length && out.length < ${MAX_SNAPSHOT_NODES}; i++) {
    const el = all[i];
    const role = roleOf(el);
    if (!role) continue;
    const rect = el.getBoundingClientRect();
    if (rect.width === 0 && rect.height === 0) continue;
    out.push({ role, name: nameOf(el), selector: uniqueSelector(el) });
  }
  return out;
})()`;
export async function snapshot(session, _params, ctx) {
    const s = live(session);
    if (isResult(s))
        return s;
    const r = await evalValue(s, SNAPSHOT_SCRIPT, ctx.commandTimeoutMs);
    if ("errorText" in r)
        return fail("evaluate-failed", r.errorText);
    const nodes = r.value ?? [];
    const lines = nodes.map((n) => `${n.role} ${JSON.stringify(n.name)} → ${n.selector}`);
    const note = nodes.length >= MAX_SNAPSHOT_NODES ? ` (capped at ${MAX_SNAPSHOT_NODES} nodes)` : "";
    return ok({ count: nodes.length, nodes, text: `${nodes.length} interactive/landmark nodes${note}:\n${lines.join("\n")}` });
}
// ── click / fill / fill-form / hover / select-option ────────────────────────
export async function click(session, params, ctx) {
    const s = live(session);
    if (isResult(s))
        return s;
    return actOnSelector(s, params.selector, "el.scrollIntoView({ block: 'center' }); if (typeof el.click === 'function') el.click(); else el.dispatchEvent(new MouseEvent('click', { bubbles: true }));", ctx.commandTimeoutMs);
}
function fillBody(value) {
    const v = JSON.stringify(value);
    return `if (el.isContentEditable) { el.focus(); el.textContent = ${v}; el.dispatchEvent(new Event('input', { bubbles: true })); }
    else { el.focus(); el.value = ${v}; el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); }`;
}
export async function fill(session, params, ctx) {
    const s = live(session);
    if (isResult(s))
        return s;
    return actOnSelector(s, params.selector, fillBody(params.value), ctx.commandTimeoutMs);
}
export async function fillForm(session, params, ctx) {
    const s = live(session);
    if (isResult(s))
        return s;
    const script = `(() => {
    const fields = ${JSON.stringify(params.fields)};
    const missing = [];
    for (const f of fields) {
      const el = document.querySelector(f.selector);
      if (!el) { missing.push(f.selector); continue; }
      if (el.isContentEditable) { el.focus(); el.textContent = f.value; el.dispatchEvent(new Event('input', { bubbles: true })); }
      else { el.focus(); el.value = f.value; el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); }
    }
    return { missing };
  })()`;
    const r = await evalValue(s, script, ctx.commandTimeoutMs);
    if ("errorText" in r)
        return fail("evaluate-failed", r.errorText);
    const missing = r.value?.missing ?? [];
    if (missing.length)
        return fail("selector-not-found", `no element matches: ${missing.join(", ")}`);
    return ok({ filled: params.fields.length });
}
export async function hover(session, params, ctx) {
    const s = live(session);
    if (isResult(s))
        return s;
    const body = `el.scrollIntoView({ block: 'center' });
    for (const t of ['pointerover','mouseover','pointerenter','mouseenter','pointermove','mousemove']) el.dispatchEvent(new MouseEvent(t, { bubbles: true }));`;
    return actOnSelector(s, params.selector, body, ctx.commandTimeoutMs);
}
export async function selectOption(session, params, ctx) {
    const s = live(session);
    if (isResult(s))
        return s;
    const script = `(() => {
    const el = document.querySelector(${JSON.stringify(params.selector)});
    if (!el) return { nf: true };
    const wantValue = ${params.value !== undefined ? JSON.stringify(params.value) : "null"};
    const wantLabel = ${params.label !== undefined ? JSON.stringify(params.label) : "null"};
    let matched = null;
    for (const opt of Array.from(el.options || [])) {
      if (wantValue !== null && opt.value === wantValue) { matched = opt; break; }
      if (wantLabel !== null && (opt.label === wantLabel || opt.textContent.trim() === wantLabel)) { matched = opt; break; }
    }
    if (!matched) return { noopt: true };
    el.value = matched.value;
    el.dispatchEvent(new Event('input', { bubbles: true }));
    el.dispatchEvent(new Event('change', { bubbles: true }));
    return { ok: true };
  })()`;
    const r = await evalValue(s, script, ctx.commandTimeoutMs);
    if ("errorText" in r)
        return fail("evaluate-failed", r.errorText);
    const v = r.value;
    if (v?.nf)
        return fail("selector-not-found", `no element matches ${params.selector}`);
    if (v?.noopt)
        return fail("option-not-found", `no option matches value=${params.value ?? ""} label=${params.label ?? ""}`);
    return ok();
}
// ── type / press-key ────────────────────────────────────────────────────────
export async function type(session, params, ctx) {
    const s = live(session);
    if (isResult(s))
        return s;
    if (params.selector) {
        const focusRes = await actOnSelector(s, params.selector, "el.focus();", ctx.commandTimeoutMs);
        if (!focusRes.ok)
            return focusRes;
    }
    try {
        await s.command("Input.insertText", { text: params.text }, ctx.commandTimeoutMs);
    }
    catch (err) {
        return fail("error", err instanceof Error ? err.message : String(err));
    }
    return ok();
}
const KEY_MAP = {
    Enter: { key: "Enter", code: "Enter", keyCode: 13, text: "\r" },
    Tab: { key: "Tab", code: "Tab", keyCode: 9 },
    Escape: { key: "Escape", code: "Escape", keyCode: 27 },
    Backspace: { key: "Backspace", code: "Backspace", keyCode: 8 },
    Delete: { key: "Delete", code: "Delete", keyCode: 46 },
    ArrowUp: { key: "ArrowUp", code: "ArrowUp", keyCode: 38 },
    ArrowDown: { key: "ArrowDown", code: "ArrowDown", keyCode: 40 },
    ArrowLeft: { key: "ArrowLeft", code: "ArrowLeft", keyCode: 37 },
    ArrowRight: { key: "ArrowRight", code: "ArrowRight", keyCode: 39 },
    Home: { key: "Home", code: "Home", keyCode: 36 },
    End: { key: "End", code: "End", keyCode: 35 },
    PageUp: { key: "PageUp", code: "PageUp", keyCode: 33 },
    PageDown: { key: "PageDown", code: "PageDown", keyCode: 34 },
    Space: { key: " ", code: "Space", keyCode: 32, text: " " },
};
export async function pressKey(session, params, ctx) {
    const s = live(session);
    if (isResult(s))
        return s;
    let spec = KEY_MAP[params.key];
    if (!spec) {
        if (params.key.length === 1)
            spec = { key: params.key, code: `Key${params.key.toUpperCase()}`, keyCode: params.key.toUpperCase().charCodeAt(0), text: params.key };
        else
            return fail("unsupported-key", `unknown key "${params.key}" (single chars and ${Object.keys(KEY_MAP).join(", ")} are supported)`);
    }
    const base = { key: spec.key, code: spec.code, windowsVirtualKeyCode: spec.keyCode, nativeVirtualKeyCode: spec.keyCode };
    try {
        await s.command("Input.dispatchKeyEvent", { type: spec.text ? "keyDown" : "rawKeyDown", ...base, ...(spec.text ? { text: spec.text } : {}) }, ctx.commandTimeoutMs);
        await s.command("Input.dispatchKeyEvent", { type: "keyUp", ...base }, ctx.commandTimeoutMs);
    }
    catch (err) {
        return fail("error", err instanceof Error ? err.message : String(err));
    }
    return ok();
}
// ── wait-for ────────────────────────────────────────────────────────────────
export async function waitFor(session, params, ctx) {
    const s = live(session);
    if (isResult(s))
        return s;
    if (!params.selector && !params.text)
        return fail("error", "wait-for requires selector or text");
    const deadline = Date.now() + (params.timeoutMs ?? ctx.loadTimeoutMs);
    const probe = params.selector
        ? `!!document.querySelector(${JSON.stringify(params.selector)})`
        : `(document.body && document.body.innerText || '').indexOf(${JSON.stringify(params.text)}) >= 0`;
    // eslint-disable-next-line no-constant-condition
    while (true) {
        if (!session.ensure())
            return SESSION_LOST;
        const r = await evalValue(s, probe, ctx.commandTimeoutMs);
        if ("errorText" in r)
            return fail("evaluate-failed", r.errorText);
        if (r.value === true)
            return ok();
        if (Date.now() >= deadline)
            return fail("wait-timeout", `condition not met within timeout (${params.selector ? `selector ${params.selector}` : `text ${JSON.stringify(params.text)}`})`);
        await new Promise((res) => setTimeout(res, 250));
    }
}
// ── handle-dialog ───────────────────────────────────────────────────────────
export function handleDialog(session, params) {
    session.setDialogPolicy({ accept: params.accept, promptText: params.promptText });
    return ok({ armed: { accept: params.accept, promptText: params.promptText ?? null } });
}
// ── evaluate ────────────────────────────────────────────────────────────────
export async function evaluate(session, params, ctx) {
    const s = live(session);
    if (isResult(s))
        return s;
    const r = await evalValue(s, params.expression, ctx.commandTimeoutMs, true);
    if ("errorText" in r)
        return fail("evaluate-failed", r.errorText);
    return ok({ value: r.value });
}
// ── console-messages ────────────────────────────────────────────────────────
export function consoleMessages(session) {
    const messages = session.drainConsole();
    const dialogWarn = session.takeUnhandledDialogFlag() ? " (a dialog opened with no policy armed and was dismissed)" : "";
    return ok({ messages, text: messages.length ? messages.join("\n") + dialogWarn : `no buffered console messages${dialogWarn}` });
}
// ── tabs ────────────────────────────────────────────────────────────────────
export async function tabs(session, params, ctx) {
    switch (params.action) {
        case "list": {
            const r = await listTargets(ctx.deps, ctx.commandTimeoutMs);
            if ("outcome" in r)
                return fail(r.outcome, r.detail);
            session.ensure(); // drop a dead-page reference so `active` is not phantom
            const active = session.currentTargetId();
            const pages = r.targets.filter((t) => t.type === undefined || t.type === "page");
            return ok({ tabs: pages.map((t) => ({ id: t.id, url: t.url ?? "", title: t.title ?? "", active: t.id === active })) });
        }
        case "new": {
            const att = await session.attach(ctx.commandTimeoutMs);
            if ("outcome" in att)
                return fail(att.outcome, att.detail);
            if (params.url) {
                const nav = await navigate(session, { url: params.url }, ctx);
                if (!nav.ok)
                    return nav;
            }
            return ok({ activeTargetId: session.currentTargetId() });
        }
        case "select": {
            if (!params.id)
                return fail("error", "tabs select requires id");
            const r = await listTargets(ctx.deps, ctx.commandTimeoutMs);
            if ("outcome" in r)
                return fail(r.outcome, r.detail);
            const target = r.targets.find((t) => t.id === params.id);
            if (!target)
                return fail("error", `no tab with id ${params.id}`);
            const att = await session.attach(ctx.commandTimeoutMs, { id: target.id, webSocketDebuggerUrl: target.webSocketDebuggerUrl });
            if ("outcome" in att)
                return fail(att.outcome, att.detail);
            return ok({ activeTargetId: session.currentTargetId() });
        }
        case "close": {
            if (!params.id)
                return fail("error", "tabs close requires id");
            if (params.id === session.currentTargetId())
                await session.close(ctx.commandTimeoutMs);
            else
                await closeTarget(ctx.deps, params.id);
            return ok({ closed: params.id });
        }
        default:
            return fail("error", `unknown tabs action ${String(params.action)}`);
    }
}
// ── pdf-save / screenshot / resize ──────────────────────────────────────────
export async function pdfSave(session, params, ctx) {
    const s = live(session);
    if (isResult(s))
        return s;
    let res;
    try {
        res = (await s.command("Page.printToPDF", {
            printBackground: params.printBackground ?? true,
            landscape: params.landscape ?? false,
            scale: params.scale ?? 1,
            preferCSSPageSize: true,
            transferMode: "ReturnAsBase64",
        }, Math.max(ctx.commandTimeoutMs, 60_000)));
    }
    catch (err) {
        return fail("pdf-failed", err instanceof Error ? err.message : String(err));
    }
    if (!res?.data)
        return fail("pdf-failed", "Page.printToPDF returned no data");
    const buf = Buffer.from(res.data, "base64");
    try {
        await ctx.writeFile(params.path, buf);
    }
    catch (err) {
        return fail("error", `write failed: ${err instanceof Error ? err.message : String(err)}`);
    }
    return ok({ path: params.path, bytes: buf.length });
}
export async function screenshot(session, params, ctx) {
    const s = live(session);
    if (isResult(s))
        return s;
    let clip;
    if (params.selector) {
        const r = await evalValue(s, `(() => { const el = document.querySelector(${JSON.stringify(params.selector)}); if (!el) return { nf: true }; const r = el.getBoundingClientRect(); return { x: r.x + window.scrollX, y: r.y + window.scrollY, width: r.width, height: r.height }; })()`, ctx.commandTimeoutMs);
        if ("errorText" in r)
            return fail("evaluate-failed", r.errorText);
        const v = r.value;
        if (v?.nf)
            return fail("selector-not-found", `no element matches ${params.selector}`);
        if (!v?.width || !v?.height)
            return fail("screenshot-failed", `element ${params.selector} has zero size`);
        clip = { x: v.x ?? 0, y: v.y ?? 0, width: v.width, height: v.height, scale: 1 };
    }
    let res;
    try {
        res = (await s.command("Page.captureScreenshot", {
            format: "png",
            ...(clip ? { clip } : {}),
            ...(params.fullPage ? { captureBeyondViewport: true } : {}),
        }, Math.max(ctx.commandTimeoutMs, 30_000)));
    }
    catch (err) {
        return fail("screenshot-failed", err instanceof Error ? err.message : String(err));
    }
    if (!res?.data)
        return fail("screenshot-failed", "Page.captureScreenshot returned no data");
    const buf = Buffer.from(res.data, "base64");
    try {
        await ctx.writeFile(params.path, buf);
    }
    catch (err) {
        return fail("error", `write failed: ${err instanceof Error ? err.message : String(err)}`);
    }
    return ok({ path: params.path, bytes: buf.length });
}
export async function resize(session, params, ctx) {
    const s = live(session);
    if (isResult(s))
        return s;
    try {
        await s.command("Emulation.setDeviceMetricsOverride", { width: params.width, height: params.height, deviceScaleFactor: 1, mobile: false }, ctx.commandTimeoutMs);
    }
    catch (err) {
        return fail("error", err instanceof Error ? err.message : String(err));
    }
    return ok({ width: params.width, height: params.height });
}
//# sourceMappingURL=cdp-actions.js.map