/**
 * BrowserObserver — snapshots the current page into a compact, LLM-friendly form.
 *
 * Output contract:
 *   - Every interactive element gets a stable numeric `index` within this snapshot.
 *   - The index is ALSO set on the element via `data-avis-idx="<index>"` so
 *     subsequent actions can re-locate it without re-observing (as long as the
 *     DOM hasn't significantly changed).
 *   - Non-interactive text is preserved as plain "text" lines, interleaved with
 *     interactive lines so the LLM can read the page naturally.
 *
 * Design choice: we keep the in-page script as an inline string so the Node
 * build never needs DOM typings.
 */
// ============================================================================
// In-page evaluator — must be self-contained, no imports.
// ============================================================================
const OBSERVE_SCRIPT = /* js */ `(() => {
	const MAX_TEXT = 120;
	const MAX_NAME = 80;

	function clip(str, n) {
		if (!str) return "";
		str = String(str).replace(/\\s+/g, " ").trim();
		return str.length > n ? str.slice(0, n) + "…" : str;
	}

	function isVisible(el) {
		if (!(el instanceof Element)) return false;
		const rect = el.getBoundingClientRect();
		if (rect.width === 0 || rect.height === 0) return false;
		const style = window.getComputedStyle(el);
		if (style.visibility === "hidden" || style.display === "none" || style.opacity === "0") return false;
		return true;
	}

	function accessibleName(el) {
		const aria = el.getAttribute("aria-label");
		if (aria && aria.trim()) return clip(aria, MAX_NAME);
		const labelledBy = el.getAttribute("aria-labelledby");
		if (labelledBy) {
			const parts = labelledBy.split(/\\s+/).map((id) => {
				const l = document.getElementById(id);
				return l ? l.textContent : "";
			});
			const s = parts.join(" ").trim();
			if (s) return clip(s, MAX_NAME);
		}
		if (el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.tagName === "SELECT") {
			if (el.id) {
				const lbl = document.querySelector('label[for="' + CSS.escape(el.id) + '"]');
				if (lbl && lbl.textContent) return clip(lbl.textContent, MAX_NAME);
			}
			const parentLabel = el.closest("label");
			if (parentLabel && parentLabel.textContent) return clip(parentLabel.textContent, MAX_NAME);
			if (el.placeholder) return clip(el.placeholder, MAX_NAME);
			if (el.name) return clip(el.name, MAX_NAME);
		}
		if (el.tagName === "IMG") {
			if (el.alt) return clip(el.alt, MAX_NAME);
		}
		// Fallback: own text
		const t = el.innerText || el.textContent;
		if (t) return clip(t, MAX_NAME);
		const title = el.getAttribute("title");
		if (title) return clip(title, MAX_NAME);
		return "";
	}

	const INTERACTIVE_TAGS = new Set([
		"A", "BUTTON", "INPUT", "SELECT", "TEXTAREA", "SUMMARY"
	]);
	// LABEL is clickable but usually wraps a real input; we traverse into it rather
	// than emitting it as a leaf, so nested inputs are visible to the agent.
	const INTERACTIVE_ROLES = new Set([
		"button", "link", "checkbox", "radio", "menuitem", "option",
		"tab", "switch", "combobox", "listbox", "searchbox", "textbox", "slider"
	]);

	function isInteractive(el) {
		if (!el || !(el instanceof Element)) return false;
		if (el.hasAttribute("disabled")) return false;
		const tag = el.tagName;
		if (INTERACTIVE_TAGS.has(tag)) return true;
		const role = el.getAttribute("role");
		if (role && INTERACTIVE_ROLES.has(role)) return true;
		if (el.hasAttribute("onclick")) return true;
		if (el.getAttribute("contenteditable") === "true") return true;
		if (el.hasAttribute("tabindex") && el.getAttribute("tabindex") !== "-1") {
			// treat only elements with roles or click handlers as interactive here
			if (role && INTERACTIVE_ROLES.has(role)) return true;
		}
		return false;
	}

	function bboxOf(el) {
		const r = el.getBoundingClientRect();
		return {
			x: Math.round(r.left),
			y: Math.round(r.top),
			w: Math.round(r.width),
			h: Math.round(r.height),
		};
	}

	// Clear prior indexes so snapshots are deterministic.
	document.querySelectorAll("[data-avis-idx]").forEach((n) => n.removeAttribute("data-avis-idx"));

	// Walk DOM in document order.
	const elements = [];
	const lines = [];
	let nextIndex = 0;

	function describeInteractive(el, index) {
		const tag = el.tagName.toLowerCase();
		const role = el.getAttribute("role") || "";
		const name = accessibleName(el);
		const out = {
			index,
			tag,
			role: role || undefined,
			name: name || undefined,
			bbox: bboxOf(el),
		};
		if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
			out.value = clip(el.value || "", MAX_TEXT);
			if (el.type) out.type = el.type;
			if (el.placeholder) out.placeholder = clip(el.placeholder, MAX_NAME);
			if (el.type === "checkbox" || el.type === "radio") out.checked = !!el.checked;
		} else if (el.tagName === "SELECT") {
			out.value = clip(el.value || "", MAX_TEXT);
		} else if (el.tagName === "A") {
			const href = el.getAttribute("href");
			if (href) out.href = href.slice(0, 200);
		}
		if (el.hasAttribute("disabled")) out.disabled = true;
		return out;
	}

	function processNode(node, textAccumulator) {
		if (node.nodeType === 3) {
			// Text node
			const txt = (node.nodeValue || "").replace(/\\s+/g, " ").trim();
			if (txt) textAccumulator.push(txt);
			return;
		}
		if (node.nodeType !== 1) return;
		const el = node;

		// Skip invisible subtrees entirely (huge perf win)
		if (!isVisible(el)) return;

		// Skip script/style/noscript
		const tag = el.tagName;
		if (tag === "SCRIPT" || tag === "STYLE" || tag === "NOSCRIPT" || tag === "TEMPLATE") return;

		if (isInteractive(el)) {
			// Flush pending text
			if (textAccumulator.length > 0) {
				const joined = clip(textAccumulator.join(" "), 400);
				if (joined) lines.push(joined);
				textAccumulator.length = 0;
			}
			const idx = nextIndex++;
			el.setAttribute("data-avis-idx", String(idx));
			const desc = describeInteractive(el, idx);
			elements.push(desc);
			// Build a one-line marker for the textual view
			const parts = ["[" + idx + "]", "<" + desc.tag + ">"];
			if (desc.role) parts.push("role=" + desc.role);
			if (desc.type) parts.push("type=" + desc.type);
			if (desc.value) parts.push("value=\\"" + desc.value + "\\"");
			if (desc.checked) parts.push("checked");
			if (desc.disabled) parts.push("disabled");
			if (desc.href) parts.push("href=" + desc.href);
			let marker = parts.join(" ");
			if (desc.name) marker += " " + desc.name;
			lines.push(marker);
			// Do not recurse into interactive element children (keep output compact).
			return;
		}

		// Not interactive: recurse.
		const children = el.childNodes;
		for (let i = 0; i < children.length; i++) {
			processNode(children[i], textAccumulator);
		}
	}

	const buffer = [];
	processNode(document.body, buffer);
	if (buffer.length > 0) {
		const joined = clip(buffer.join(" "), 400);
		if (joined) lines.push(joined);
	}

	const viewport = { w: window.innerWidth, h: window.innerHeight };
	const pageHeight = Math.max(
		document.documentElement.scrollHeight,
		document.body ? document.body.scrollHeight : 0,
	);
	const scrollY = window.scrollY || 0;
	const scroll = {
		x: window.scrollX || 0,
		y: scrollY,
		pagesAbove: viewport.h > 0 ? scrollY / viewport.h : 0,
		pagesBelow: viewport.h > 0 ? Math.max(0, (pageHeight - scrollY - viewport.h) / viewport.h) : 0,
	};

	// Cap total lines to keep LLM context sane
	const MAX_LINES = 400;
	if (lines.length > MAX_LINES) {
		lines.length = MAX_LINES;
		lines.push("…[truncated — " + elements.length + " interactive elements total]");
	}

	return {
		url: location.href,
		title: document.title,
		viewport,
		scroll,
		elements,
		content: lines.join("\\n"),
	};
})()`;
export async function observePage(page) {
    const snap = (await page.evaluate(OBSERVE_SCRIPT));
    const authSignals = detectAuthSignals(snap);
    if (authSignals.length > 0)
        snap.authSignals = authSignals;
    return snap;
}
function detectAuthSignals(snap) {
    const haystack = [snap.url, snap.title, snap.content].filter(Boolean).join("\n").toLowerCase();
    const signals = [];
    if (/(^|[^a-z])(sign in|log in|login|sign into|log into)([^a-z]|$)/i.test(haystack)) {
        signals.push("login required");
    }
    if (/(two-factor|2fa|mfa|multi-factor|verification code|authenticator app|one-time password|otp)/i.test(haystack)) {
        signals.push("2FA/MFA required");
    }
    if (/(passkey|security key|webauthn|touch id|face id|use your device|approve sign in)/i.test(haystack)) {
        signals.push("passkey or device approval required");
    }
    if (/(captcha|i am human|i'm human|verify you are human|robot check|recaptcha|hcaptcha)/i.test(haystack)) {
        signals.push("CAPTCHA or human verification required");
    }
    return Array.from(new Set(signals));
}
/** Human-readable snapshot for submission to the LLM / display. */
export function formatSnapshot(snap) {
    const { url, title, viewport, scroll, elements, content, authSignals } = snap;
    const header = [
        `URL: ${url}`,
        `Title: ${title || "(untitled)"}`,
        `Viewport: ${viewport.w}x${viewport.h}`,
        `Scroll: ${scroll.y}px (${scroll.pagesAbove.toFixed(1)} pages above, ${scroll.pagesBelow.toFixed(1)} below)`,
        `Interactive elements: ${elements.length}`,
        authSignals && authSignals.length > 0 ? `Auth signals: ${authSignals.join(", ")}` : undefined,
    ]
        .filter(Boolean)
        .join("\n");
    return `${header}\n\n${content}`;
}
//# sourceMappingURL=browser-observer.js.map