import { Type } from "@sinclair/typebox";
import { StringEnum } from "@singularity-forge/ai";
import { diffCompactStates } from "../core.js";
import { setLastActionAfterState, setLastActionBeforeState } from "../state.js";

// ---------------------------------------------------------------------------
// Intent definitions
// ---------------------------------------------------------------------------
const INTENTS = [
	"submit_form",
	"close_dialog",
	"primary_cta",
	"search_field",
	"next_step",
	"dismiss",
	"auth_action",
	"back_navigation",
];
// ---------------------------------------------------------------------------
// Scoring evaluate script — runs entirely in-browser via page.evaluate()
// ---------------------------------------------------------------------------
/**
 * Builds a self-contained IIFE string that scores candidate elements for a
 * given intent. Returns top 5 candidates sorted by score descending, each
 * with { score, selector, tag, role, name, text, reason }.
 *
 * Uses window.__pi utilities (injected via addInitScript) for element
 * metadata — no inline redeclarations.
 */
function buildIntentScoringScript(intent, scope) {
	const scopeSelector = JSON.stringify(scope ?? null);
	return `(() => {
	var pi = window.__pi;
	if (!pi) return { error: "window.__pi not available — browser helpers not injected" };

	var intentRaw = ${JSON.stringify(intent)};
	var normalized = intentRaw.toLowerCase().replace(/[\\s_\\-]+/g, "");
	var scopeSel = ${scopeSelector};
	var root = scopeSel ? document.querySelector(scopeSel) : document.body;
	if (!root) return { error: "Scope selector not found: " + scopeSel };

	// --- Shared helpers ---
	function textOf(el) {
		return (el.textContent || "").trim().replace(/\\s+/g, " ").slice(0, 120).toLowerCase();
	}

	function clamp01(v) { return Math.max(0, Math.min(1, v)); }

	function makeCandidate(el, score, reason) {
		return {
			score: Math.round(clamp01(score) * 100) / 100,
			selector: pi.cssPath(el),
			tag: el.tagName.toLowerCase(),
			role: pi.inferRole(el) || "",
			name: pi.accessibleName(el) || "",
			text: textOf(el).slice(0, 80),
			reason: reason,
		};
	}

	function qsa(sel) { return Array.from(root.querySelectorAll(sel)); }

	function visibleEnabled(el) {
		return pi.isVisible(el) && pi.isEnabled(el);
	}

	function textMatches(el, patterns) {
		var t = textOf(el);
		var n = (pi.accessibleName(el) || "").toLowerCase();
		var combined = t + " " + n;
		for (var i = 0; i < patterns.length; i++) {
			if (combined.indexOf(patterns[i]) !== -1) return true;
		}
		return false;
	}

	function textMatchStrength(el, patterns) {
		var t = textOf(el);
		var n = (pi.accessibleName(el) || "").toLowerCase();
		var combined = t + " " + n;
		var count = 0;
		for (var i = 0; i < patterns.length; i++) {
			if (combined.indexOf(patterns[i]) !== -1) count++;
		}
		return Math.min(count / Math.max(patterns.length, 1), 1);
	}

	// --- Intent-specific scoring ---
	var candidates = [];

	if (normalized === "submitform") {
		var els = qsa('button[type="submit"], input[type="submit"], button:not([type]), button[type="button"]');
		for (var i = 0; i < els.length; i++) {
			var el = els[i];
			if (!visibleEnabled(el)) continue;
			var d1 = el.type === "submit" || el.getAttribute("type") === "submit" ? 0.35 : 0;
			var d2 = el.closest("form") ? 0.3 : 0;
			var d3 = textMatches(el, ["submit", "send", "save", "create", "add", "post", "confirm", "ok", "done", "register", "sign up", "log in"]) ? 0.2 : 0;
			var d4 = 0.15;
			var score = d1 + d2 + d3 + d4;
			var reasons = [];
			if (d1 > 0) reasons.push("submit-type");
			if (d2 > 0) reasons.push("inside-form");
			if (d3 > 0) reasons.push("text-suggests-submit");
			reasons.push("visible+enabled");
			candidates.push(makeCandidate(el, score, reasons.join(", ")));
		}
	}

	else if (normalized === "closedialog") {
		var containers = qsa('[role="dialog"], dialog, [aria-modal="true"], [role="alertdialog"]');
		for (var ci = 0; ci < containers.length; ci++) {
			var btns = containers[ci].querySelectorAll("button, a, [role='button']");
			for (var bi = 0; bi < btns.length; bi++) {
				var el = btns[bi];
				if (!visibleEnabled(el)) continue;
				var d1 = textMatches(el, ["close", "cancel", "dismiss", "×", "✕", "x", "got it", "ok", "done"]) ? 0.35 : 0;
				var ariaLbl = (el.getAttribute("aria-label") || "").toLowerCase();
				var d2 = (ariaLbl.indexOf("close") !== -1 || ariaLbl.indexOf("dismiss") !== -1) ? 0.25 : 0;
				var d3 = 0.2;
				var rect = el.getBoundingClientRect();
				var parentRect = containers[ci].getBoundingClientRect();
				var isTopRight = rect.top - parentRect.top < 60 && parentRect.right - rect.right < 60;
				var d4 = isTopRight ? 0.2 : 0;
				var score = d1 + d2 + d3 + d4;
				var reasons = [];
				if (d1 > 0) reasons.push("text-matches-close");
				if (d2 > 0) reasons.push("aria-label-close");
				reasons.push("inside-dialog");
				if (d4 > 0) reasons.push("top-right-position");
				candidates.push(makeCandidate(el, score, reasons.join(", ")));
			}
		}
	}

	else if (normalized === "primarycta") {
		var els = qsa("button, a, [role='button'], input[type='submit'], input[type='button']");
		for (var i = 0; i < els.length; i++) {
			var el = els[i];
			if (!visibleEnabled(el)) continue;
			var rect = el.getBoundingClientRect();
			var area = rect.width * rect.height;
			var d1 = clamp01(area / 12000);
			var role = pi.inferRole(el);
			var d2 = role === "button" ? 0.25 : (role === "link" ? 0.1 : 0.15);
			var isNegative = textMatches(el, ["cancel", "dismiss", "close", "skip", "no thanks", "no, thanks", "maybe later"]);
			var d3 = isNegative ? 0 : 0.2;
			var inMain = !!el.closest("main, [role='main'], article, section, .hero, .content");
			var d4 = inMain ? 0.15 : 0;
			var score = d1 + d2 + d3 + d4;
			var reasons = [];
			reasons.push("size:" + Math.round(area));
			if (d2 >= 0.25) reasons.push("button-role");
			if (d3 > 0) reasons.push("non-dismissive");
			if (d4 > 0) reasons.push("in-main-content");
			candidates.push(makeCandidate(el, score, reasons.join(", ")));
		}
	}

	else if (normalized === "searchfield") {
		var els = qsa("input, textarea, [role='searchbox'], [role='combobox'], [contenteditable='true']");
		for (var i = 0; i < els.length; i++) {
			var el = els[i];
			if (!pi.isVisible(el)) continue;
			var type = (el.getAttribute("type") || "text").toLowerCase();
			if (["hidden", "submit", "button", "reset", "image", "checkbox", "radio", "file"].indexOf(type) !== -1 && el.tagName.toLowerCase() === "input") continue;
			var d1 = type === "search" || pi.inferRole(el) === "searchbox" ? 0.4 : 0;
			var ph = (el.getAttribute("placeholder") || "").toLowerCase();
			var nm = (el.getAttribute("name") || "").toLowerCase();
			var ariaLbl = (el.getAttribute("aria-label") || "").toLowerCase();
			var combined = ph + " " + nm + " " + ariaLbl;
			var d2 = combined.indexOf("search") !== -1 || combined.indexOf("query") !== -1 || combined.indexOf("find") !== -1 ? 0.3 : 0;
			var d3 = pi.isEnabled(el) ? 0.15 : 0;
			var inHeader = !!el.closest("header, nav, [role='banner'], [role='navigation'], [role='search']");
			var d4 = inHeader ? 0.15 : 0;
			var score = d1 + d2 + d3 + d4;
			if (score < 0.1) continue;
			var reasons = [];
			if (d1 > 0) reasons.push("search-type/role");
			if (d2 > 0) reasons.push("name/placeholder-match");
			if (d3 > 0) reasons.push("enabled");
			if (d4 > 0) reasons.push("in-header/nav");
			candidates.push(makeCandidate(el, score, reasons.join(", ")));
		}
	}

	else if (normalized === "nextstep") {
		var els = qsa("button, a, [role='button'], input[type='submit'], input[type='button']");
		var patterns = ["next", "continue", "proceed", "forward", "go", "step"];
		for (var i = 0; i < els.length; i++) {
			var el = els[i];
			if (!visibleEnabled(el)) continue;
			var d1 = textMatchStrength(el, patterns) * 0.4;
			if (d1 === 0) continue;
			var role = pi.inferRole(el);
			var d2 = role === "button" ? 0.25 : 0.1;
			var d3 = 0.2;
			var isDisabled = !pi.isEnabled(el);
			var d4 = isDisabled ? 0 : 0.15;
			var score = d1 + d2 + d3 + d4;
			var reasons = [];
			reasons.push("text-match");
			if (d2 >= 0.25) reasons.push("button-role");
			reasons.push("visible");
			if (d4 > 0) reasons.push("enabled");
			candidates.push(makeCandidate(el, score, reasons.join(", ")));
		}
	}

	else if (normalized === "dismiss") {
		var els = qsa("button, a, [role='button'], [role='link']");
		var patterns = ["close", "cancel", "dismiss", "skip", "no thanks", "no, thanks", "maybe later", "not now", "×", "✕"];
		for (var i = 0; i < els.length; i++) {
			var el = els[i];
			if (!visibleEnabled(el)) continue;
			var d1 = textMatchStrength(el, patterns) * 0.35;
			if (d1 === 0) continue;
			var inOverlay = !!el.closest('[role="dialog"], dialog, [aria-modal="true"], [role="alertdialog"], .modal, .overlay, .popup, .popover, .toast, .banner');
			var d2 = inOverlay ? 0.3 : 0.05;
			var rect = el.getBoundingClientRect();
			var isEdge = rect.top < 80 || rect.right > window.innerWidth - 80;
			var d3 = isEdge ? 0.15 : 0;
			var d4 = 0.15;
			var score = d1 + d2 + d3 + d4;
			var reasons = [];
			reasons.push("text-match");
			if (d2 >= 0.3) reasons.push("inside-overlay");
			if (d3 > 0) reasons.push("edge-position");
			reasons.push("visible+enabled");
			candidates.push(makeCandidate(el, score, reasons.join(", ")));
		}
	}

	else if (normalized === "authaction") {
		var els = qsa("button, a, [role='button'], [role='link'], input[type='submit']");
		var patterns = ["log in", "login", "sign in", "signin", "sign up", "signup", "register", "create account", "join", "get started"];
		for (var i = 0; i < els.length; i++) {
			var el = els[i];
			if (!visibleEnabled(el)) continue;
			var d1 = textMatchStrength(el, patterns) * 0.4;
			if (d1 === 0) continue;
			var role = pi.inferRole(el);
			var d2 = (role === "button" || role === "link") ? 0.25 : 0.1;
			var rect = el.getBoundingClientRect();
			var inHeader = !!el.closest("header, nav, [role='banner'], [role='navigation']");
			var isProminent = inHeader || rect.top < 200;
			var d3 = isProminent ? 0.2 : 0.05;
			var d4 = 0.15;
			var score = d1 + d2 + d3 + d4;
			var reasons = [];
			reasons.push("text-match");
			if (d2 >= 0.25) reasons.push("button-or-link");
			if (d3 >= 0.2) reasons.push("prominent-position");
			reasons.push("visible+enabled");
			candidates.push(makeCandidate(el, score, reasons.join(", ")));
		}
	}

	else if (normalized === "backnavigation") {
		var els = qsa("button, a, [role='button'], [role='link']");
		var patterns = ["back", "previous", "prev", "return", "go back"];
		for (var i = 0; i < els.length; i++) {
			var el = els[i];
			if (!visibleEnabled(el)) continue;
			var d1 = textMatchStrength(el, patterns) * 0.35;
			if (d1 === 0) continue;
			var innerHtml = el.innerHTML.toLowerCase();
			var hasArrow = innerHtml.indexOf("←") !== -1 || innerHtml.indexOf("&larr") !== -1 || innerHtml.indexOf("arrow") !== -1 || innerHtml.indexOf("chevron-left") !== -1 || innerHtml.indexOf("back") !== -1;
			var d2 = hasArrow ? 0.25 : 0;
			var inNav = !!el.closest("header, nav, [role='banner'], [role='navigation'], .breadcrumb, .toolbar");
			var d3 = inNav ? 0.25 : 0.05;
			var d4 = 0.15;
			var score = d1 + d2 + d3 + d4;
			var reasons = [];
			reasons.push("text-match");
			if (d2 > 0) reasons.push("has-back-arrow/icon");
			if (d3 >= 0.25) reasons.push("in-nav/header");
			reasons.push("visible+enabled");
			candidates.push(makeCandidate(el, score, reasons.join(", ")));
		}
	}

	else {
		return { error: "Unknown intent: " + intentRaw + ". Valid: submit_form, close_dialog, primary_cta, search_field, next_step, dismiss, auth_action, back_navigation" };
	}

	// Sort by score descending, cap at 5
	candidates.sort(function(a, b) { return b.score - a.score; });
	candidates = candidates.slice(0, 5);

	return { intent: intentRaw, normalized: normalized, count: candidates.length, candidates: candidates };
})()`;
}
// ---------------------------------------------------------------------------
// Registration
// ---------------------------------------------------------------------------
export function registerIntentTools(pi, deps) {
	// -----------------------------------------------------------------------
	// browser_find_best
	// -----------------------------------------------------------------------
	pi.registerTool({
		name: "browser_find_best",
		label: "Find Best",
		description:
			'Find the best-matching element for a semantic intent. Returns up to 5 scored candidates (0-1) ranked by structural position, role, text signals, and visibility. Use this to discover which element the agent should interact with for a given goal — e.g. intent="submit_form" finds submit buttons, intent="close_dialog" finds close/dismiss buttons inside dialogs. Each candidate includes a CSS selector usable with browser_click.',
		parameters: Type.Object({
			intent: StringEnum(INTENTS, {
				description:
					"Semantic intent: submit_form, close_dialog, primary_cta, search_field, next_step, dismiss, auth_action, back_navigation",
			}),
			scope: Type.Optional(
				Type.String({
					description:
						"CSS selector to narrow the search area. If omitted, searches the full page.",
				}),
			),
		}),
		async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
			let actionId = null;
			let beforeState = null;
			try {
				const { page: p } = await deps.ensureBrowser();
				const target = deps.getActiveTarget();
				beforeState = await deps.captureCompactPageState(p, {
					selectors: params.scope ? [params.scope] : [],
					includeBodyText: false,
					target,
				});
				actionId = deps.beginTrackedAction(
					"browser_find_best",
					params,
					beforeState.url,
				).id;
				const script = buildIntentScoringScript(params.intent, params.scope);
				const result = await target.evaluate(script);
				if (result.error) {
					deps.finishTrackedAction(actionId, {
						status: "error",
						error: result.error,
						beforeState,
					});
					return {
						content: [{ type: "text", text: result.error }],
						details: {},
						isError: true,
					};
				}
				const afterState = await deps.captureCompactPageState(p, {
					selectors: params.scope ? [params.scope] : [],
					includeBodyText: false,
					target,
				});
				setLastActionBeforeState(beforeState);
				setLastActionAfterState(afterState);
				deps.finishTrackedAction(actionId, {
					status: "success",
					afterUrl: afterState.url,
					beforeState,
					afterState,
				});
				// Format output
				const lines = [];
				lines.push(`Intent: ${params.intent} → ${result.count} candidate(s)`);
				if (params.scope) lines.push(`Scope: ${params.scope}`);
				lines.push("");
				if (result.candidates.length === 0) {
					lines.push(
						"No candidates found for this intent on the current page.",
					);
				} else {
					for (let i = 0; i < result.candidates.length; i++) {
						const c = result.candidates[i];
						lines.push(`${i + 1}. **${c.score}** \`${c.selector}\``);
						lines.push(
							`   ${c.tag}${c.role ? ` [${c.role}]` : ""} — "${c.name || c.text}"`,
						);
						lines.push(`   Reason: ${c.reason}`);
					}
				}
				return {
					content: [{ type: "text", text: lines.join("\n") }],
					details: { intentResult: result },
				};
			} catch (err) {
				const screenshot = await deps.captureErrorScreenshot(
					(() => {
						try {
							return deps.getActivePage();
						} catch {
							return null;
						}
					})(),
				);
				const errMsg = deps.firstErrorLine(err);
				if (actionId !== null) {
					deps.finishTrackedAction(actionId, {
						status: "error",
						error: errMsg,
						beforeState: beforeState ?? undefined,
					});
				}
				const content = [
					{ type: "text", text: `browser_find_best failed: ${errMsg}` },
				];
				if (screenshot) {
					content.push({
						type: "image",
						data: screenshot.data,
						mimeType: screenshot.mimeType,
					});
				}
				return { content, details: {}, isError: true };
			}
		},
	});
	// -----------------------------------------------------------------------
	// browser_act
	// -----------------------------------------------------------------------
	pi.registerTool({
		name: "browser_act",
		label: "Browser Act",
		description:
			'Execute a semantic action in one call. Resolves the top candidate for the given intent (same scoring as browser_find_best), performs the action (click for buttons/links, focus for search fields), settles the page, and returns a before/after diff. Use when you know what you want to accomplish semantically — e.g. intent="submit_form" finds and clicks the submit button, intent="close_dialog" dismisses the dialog.',
		parameters: Type.Object({
			intent: StringEnum(INTENTS, {
				description:
					"Semantic intent: submit_form, close_dialog, primary_cta, search_field, next_step, dismiss, auth_action, back_navigation",
			}),
			scope: Type.Optional(
				Type.String({
					description:
						"CSS selector to narrow the search area. If omitted, searches the full page.",
				}),
			),
		}),
		async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
			let actionId = null;
			let beforeState = null;
			try {
				const { page: p } = await deps.ensureBrowser();
				const target = deps.getActiveTarget();
				beforeState = await deps.captureCompactPageState(p, {
					selectors: params.scope ? [params.scope] : [],
					includeBodyText: true,
					target,
				});
				actionId = deps.beginTrackedAction(
					"browser_act",
					params,
					beforeState.url,
				).id;
				// Score candidates
				const script = buildIntentScoringScript(params.intent, params.scope);
				const result = await target.evaluate(script);
				if (result.error) {
					deps.finishTrackedAction(actionId, {
						status: "error",
						error: result.error,
						beforeState,
					});
					return {
						content: [
							{
								type: "text",
								text: `browser_act failed: ${result.error}`,
							},
						],
						details: {},
						isError: true,
					};
				}
				if (result.candidates.length === 0) {
					deps.finishTrackedAction(actionId, {
						status: "error",
						error: `No candidates found for intent "${params.intent}"`,
						beforeState,
					});
					return {
						content: [
							{
								type: "text",
								text: `browser_act: No candidates found for intent "${params.intent}" on the current page. The page may not have the expected elements (e.g. no dialog for close_dialog, no form for submit_form).`,
							},
						],
						details: { intentResult: result },
						isError: true,
					};
				}
				// Take top candidate and execute action
				const top = result.candidates[0];
				const normalizedIntent = params.intent
					.toLowerCase()
					.replace(/[\s_-]+/g, "");
				if (normalizedIntent === "searchfield") {
					// Focus instead of click for search fields
					try {
						await target.locator(top.selector).first().focus({ timeout: 5000 });
					} catch {
						// Fallback: click to focus
						await target.locator(top.selector).first().click({ timeout: 5000 });
					}
				} else {
					// Click via Playwright locator (D021)
					try {
						await target.locator(top.selector).first().click({ timeout: 5000 });
					} catch {
						// getByRole fallback from interaction.ts pattern
						const nameMatch = top.selector.match(
							/\[(?:aria-label|name|placeholder)="([^"]+)"\]/i,
						);
						const roleName = nameMatch?.[1];
						let clicked = false;
						for (const role of ["button", "link", "combobox", "textbox"]) {
							try {
								const loc = roleName
									? target.getByRole(role, { name: new RegExp(roleName, "i") })
									: target.getByRole(role, {
											name: new RegExp(
												top.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
												"i",
											),
										});
								await loc.first().click({ timeout: 3000 });
								clicked = true;
								break;
							} catch {
								/* try next role */
							}
						}
						if (!clicked) {
							throw new Error(
								`Could not click top candidate "${top.selector}" for intent "${params.intent}"`,
							);
						}
					}
				}
				// Settle after action
				await deps.settleAfterActionAdaptive(p);
				// Capture after state and diff
				const afterState = await deps.captureCompactPageState(p, {
					selectors: params.scope ? [params.scope] : [],
					includeBodyText: true,
					target,
				});
				const diff = diffCompactStates(beforeState, afterState);
				const summary = deps.formatCompactStateSummary(afterState);
				const jsErrors = deps.getRecentErrors(p.url());
				setLastActionBeforeState(beforeState);
				setLastActionAfterState(afterState);
				deps.finishTrackedAction(actionId, {
					status: "success",
					afterUrl: afterState.url,
					diffSummary: diff.summary,
					beforeState,
					afterState,
				});
				// Format output
				const lines = [];
				lines.push(`Intent: ${params.intent}`);
				lines.push(
					`Action: ${normalizedIntent === "searchfield" ? "focused" : "clicked"} top candidate (score: ${top.score})`,
				);
				lines.push(`Target: \`${top.selector}\` — "${top.name || top.text}"`);
				lines.push(`Reason: ${top.reason}`);
				lines.push("");
				lines.push(`Diff:\n${deps.formatDiffText(diff)}`);
				if (jsErrors.trim()) {
					lines.push(`\nJS Errors:\n${jsErrors}`);
				}
				lines.push(`\nPage summary:\n${summary}`);
				return {
					content: [{ type: "text", text: lines.join("\n") }],
					details: { intentResult: result, topCandidate: top, diff },
				};
			} catch (err) {
				const screenshot = await deps.captureErrorScreenshot(
					(() => {
						try {
							return deps.getActivePage();
						} catch {
							return null;
						}
					})(),
				);
				const errMsg = deps.firstErrorLine(err);
				if (actionId !== null) {
					deps.finishTrackedAction(actionId, {
						status: "error",
						error: errMsg,
						beforeState: beforeState ?? undefined,
					});
				}
				const content = [
					{ type: "text", text: `browser_act failed: ${errMsg}` },
				];
				if (screenshot) {
					content.push({
						type: "image",
						data: screenshot.data,
						mimeType: screenshot.mimeType,
					});
				}
				return { content, details: {}, isError: true };
			}
		},
	});
}
