/**
 * browser-tools — Node-side utility functions
 *
 * All functions that were helpers in index.ts but run in Node (not browser).
 * They import state accessors from ./state.ts — never raw module-level variables.
 */
import { copyFile, mkdir, stat, writeFile } from "node:fs/promises";
import path from "node:path";
import {
	DEFAULT_MAX_BYTES,
	DEFAULT_MAX_LINES,
	truncateHead,
} from "@singularity-forge/coding-agent";
import {
	beginAction,
	findAction,
	finishAction,
	registryListPages,
	toActionParamsSummary,
} from "./core.js";
import {
	ARTIFACT_ROOT,
	actionTimeline,
	getActiveFrame,
	getActiveTraceSession,
	getConsoleLogs,
	getDialogLogs,
	getHarState,
	getNetworkLogs,
	getPendingCriticalRequestsByPage,
	getSessionArtifactDir,
	getSessionStartedAt,
	pageRegistry,
	setSessionArtifactDir,
	setSessionStartedAt,
} from "./state.js";
// ---------------------------------------------------------------------------
// Text truncation
// ---------------------------------------------------------------------------
export function truncateText(text) {
	const result = truncateHead(text, {
		maxLines: DEFAULT_MAX_LINES,
		maxBytes: DEFAULT_MAX_BYTES,
	});
	if (result.truncated) {
		return (
			result.content +
			`\n\n[Output truncated: ${result.outputLines}/${result.totalLines} lines shown]`
		);
	}
	return result.content;
}
// ---------------------------------------------------------------------------
// Artifact helpers
// ---------------------------------------------------------------------------
export function formatArtifactTimestamp(timestamp) {
	return new Date(timestamp).toISOString().replace(/[:.]/g, "-");
}
export async function ensureDir(dirPath) {
	await mkdir(dirPath, { recursive: true });
	return dirPath;
}
export async function writeArtifactFile(filePath, content) {
	await ensureDir(path.dirname(filePath));
	await writeFile(filePath, content);
	const fileStat = await stat(filePath);
	return { path: filePath, bytes: fileStat.size };
}
export async function copyArtifactFile(sourcePath, destinationPath) {
	await ensureDir(path.dirname(destinationPath));
	await copyFile(sourcePath, destinationPath);
	const fileStat = await stat(destinationPath);
	return { path: destinationPath, bytes: fileStat.size };
}
export function ensureSessionStartedAt() {
	let t = getSessionStartedAt();
	if (!t) {
		t = Date.now();
		setSessionStartedAt(t);
	}
	return t;
}
export async function ensureSessionArtifactDir() {
	const existing = getSessionArtifactDir();
	if (existing) {
		await ensureDir(existing);
		return existing;
	}
	const startedAt = ensureSessionStartedAt();
	const dir = path.join(
		ARTIFACT_ROOT,
		`${formatArtifactTimestamp(startedAt)}-session`,
	);
	setSessionArtifactDir(dir);
	await ensureDir(dir);
	return dir;
}
export function buildSessionArtifactPath(filename) {
	const dir = getSessionArtifactDir();
	if (!dir) {
		throw new Error("browser session artifact directory is not initialized");
	}
	return path.join(dir, filename);
}
export function getActivePageMetadata() {
	const registry = pageRegistry;
	const activeEntry =
		registry.activePageId !== null
			? (registry.pages.find((entry) => entry.id === registry.activePageId) ??
				null)
			: null;
	return {
		id: activeEntry?.id ?? null,
		title: activeEntry?.title ?? "",
		url: activeEntry?.url ?? "",
	};
}
export function getActiveFrameMetadata() {
	const frame = getActiveFrame();
	if (!frame) {
		return { name: null, url: null };
	}
	return {
		name: frame.name() || null,
		url: frame.url() || null,
	};
}
export function getSessionArtifactMetadata() {
	return {
		artifactRoot: ARTIFACT_ROOT,
		sessionStartedAt: getSessionStartedAt(),
		sessionArtifactDir: getSessionArtifactDir(),
		activeTraceSession: getActiveTraceSession(),
		harState: { ...getHarState() },
		activePage: getActivePageMetadata(),
		activeFrame: getActiveFrameMetadata(),
	};
}
export function sanitizeArtifactName(value, fallback) {
	const sanitized = value
		.trim()
		.replace(/[^a-zA-Z0-9._-]+/g, "-")
		.replace(/^-+|-+$/g, "");
	return sanitized || fallback;
}
// ---------------------------------------------------------------------------
// Page helpers
// ---------------------------------------------------------------------------
/**
 * getLivePagesSnapshot requires ensureBrowser (circular) — it will be
 * wired in via ToolDeps. This is a factory that takes ensureBrowser.
 */
export function createGetLivePagesSnapshot(ensureBrowser) {
	return async function getLivePagesSnapshot() {
		await ensureBrowser();
		for (const entry of pageRegistry.pages) {
			try {
				entry.title = await entry.page.title();
				entry.url = entry.page.url();
			} catch {
				// Page may have been closed between snapshots.
			}
		}
		return registryListPages(pageRegistry);
	};
}
export async function resolveAccessibilityScope(selector) {
	if (selector?.trim()) {
		return {
			selector: selector.trim(),
			scope: `selector:${selector.trim()}`,
			source: "explicit_selector",
		};
	}
	const frame = getActiveFrame();
	// We need getActiveTarget for dialog check, but that requires page access.
	// For non-frame scoping, the caller must handle dialog detection separately
	// if needed. Here we handle the frame case and fall through to full_page.
	if (frame) {
		return {
			selector: "body",
			scope: frame.name() ? `active frame:${frame.name()}` : "active frame",
			source: "active_frame",
		};
	}
	return { selector: "body", scope: "full page", source: "full_page" };
}
/**
 * captureAccessibilityMarkdown — needs access to the active target.
 * Accepts the target (Page | Frame) so it doesn't need to pull from state.
 */
export async function captureAccessibilityMarkdown(target, selector) {
	const scopeInfo = await resolveAccessibilityScope(selector);
	const locator = target.locator(scopeInfo.selector ?? "body").first();
	const snapshot = await locator.ariaSnapshot();
	return { snapshot, scope: scopeInfo.scope, source: scopeInfo.source };
}
// ---------------------------------------------------------------------------
// Critical request tracking
// ---------------------------------------------------------------------------
export function isCriticalResourceType(resourceType) {
	return (
		resourceType === "document" ||
		resourceType === "fetch" ||
		resourceType === "xhr"
	);
}
export function updatePendingCriticalRequests(p, delta) {
	const map = getPendingCriticalRequestsByPage();
	const current = map.get(p) ?? 0;
	map.set(p, Math.max(0, current + delta));
}
export function getPendingCriticalRequests(p) {
	return getPendingCriticalRequestsByPage().get(p) ?? 0;
}
// ---------------------------------------------------------------------------
// Verification helpers
// ---------------------------------------------------------------------------
export function verificationFromChecks(checks, retryHint) {
	const passedChecks = checks
		.filter((check) => check.passed)
		.map((check) => check.name);
	const verified = passedChecks.length > 0;
	return {
		verified,
		checks,
		verificationSummary: verified
			? `PASS (${passedChecks.join(", ")})`
			: "SOFT-FAIL (no observable state change)",
		retryHint: verified ? undefined : retryHint,
	};
}
export function verificationLine(verification) {
	return `Verification: ${verification.verificationSummary}`;
}
// ---------------------------------------------------------------------------
// Assertion helpers
// ---------------------------------------------------------------------------
export async function collectAssertionState(
	p,
	checks,
	captureCompactPageState,
	target,
) {
	const selectors = checks
		.map((check) => check.selector)
		.filter((value) => !!value);
	const compactState = await captureCompactPageState(p, {
		selectors,
		includeBodyText: true,
		target,
	});
	const sinceActionId = checks.reduce((max, check) => {
		if (check.sinceActionId === undefined) return max;
		if (max === undefined) return check.sinceActionId;
		return Math.max(max, check.sinceActionId);
	}, undefined);
	return {
		url: compactState.url,
		title: compactState.title,
		bodyText: compactState.bodyText,
		focus: compactState.focus,
		selectorStates: compactState.selectorStates,
		consoleEntries: getConsoleEntriesSince(sinceActionId),
		networkEntries: getNetworkEntriesSince(sinceActionId),
		allConsoleEntries: getConsoleLogs(),
		allNetworkEntries: getNetworkLogs(),
		actionTimeline,
	};
}
export function formatAssertionText(result) {
	const lines = [result.summary];
	for (const check of result.checks.slice(0, 8)) {
		lines.push(
			`- ${check.passed ? "PASS" : "FAIL"} ${check.name}: expected ${JSON.stringify(check.expected)}, got ${JSON.stringify(check.actual)}`,
		);
	}
	lines.push(`Hint: ${result.agentHint}`);
	return lines.join("\n");
}
export function formatDiffText(diff) {
	const lines = [diff.summary];
	for (const change of diff.changes.slice(0, 8)) {
		lines.push(
			`- ${change.type}: ${JSON.stringify(change.before ?? null)} → ${JSON.stringify(change.after ?? null)}`,
		);
	}
	return lines.join("\n");
}
// ---------------------------------------------------------------------------
// URL / dialog helpers
// ---------------------------------------------------------------------------
export function getUrlHash(url) {
	try {
		return new URL(url).hash || "";
	} catch {
		return "";
	}
}
export async function countOpenDialogs(target) {
	try {
		return await target.evaluate(
			() =>
				document.querySelectorAll('[role="dialog"]:not([hidden]),dialog[open]')
					.length,
		);
	} catch {
		return 0;
	}
}
// ---------------------------------------------------------------------------
// Click / input helpers
// ---------------------------------------------------------------------------
export async function captureClickTargetState(target, selector) {
	try {
		return await target.evaluate((sel) => {
			const el = document.querySelector(sel);
			if (!el) {
				return {
					exists: false,
					ariaExpanded: null,
					ariaPressed: null,
					ariaSelected: null,
					open: null,
				};
			}
			return {
				exists: true,
				ariaExpanded: el.getAttribute("aria-expanded"),
				ariaPressed: el.getAttribute("aria-pressed"),
				ariaSelected: el.getAttribute("aria-selected"),
				open:
					el instanceof HTMLDialogElement
						? el.open
						: el.getAttribute("open") !== null,
			};
		}, selector);
	} catch {
		return {
			exists: false,
			ariaExpanded: null,
			ariaPressed: null,
			ariaSelected: null,
			open: null,
		};
	}
}
export async function readInputLikeValue(target, selector) {
	try {
		return await target.evaluate((sel) => {
			const resolveTarget = () => {
				if (sel) return document.querySelector(sel);
				const active = document.activeElement;
				if (
					!active ||
					active === document.body ||
					active === document.documentElement
				)
					return null;
				return active;
			};
			const target = resolveTarget();
			if (!target) return null;
			if (
				target instanceof HTMLInputElement ||
				target instanceof HTMLTextAreaElement
			) {
				return target.value;
			}
			if (target instanceof HTMLSelectElement) {
				return target.value;
			}
			if (target.isContentEditable) {
				return (target.textContent ?? "").trim();
			}
			return target.getAttribute("value");
		}, selector);
	} catch {
		return null;
	}
}
export function firstErrorLine(err) {
	const message =
		typeof err === "object" && err && "message" in err
			? String(err.message ?? "")
			: String(err ?? "unknown error");
	return message.split("\n")[0] || "unknown error";
}
// ---------------------------------------------------------------------------
// Action tracking
// ---------------------------------------------------------------------------
export function beginTrackedAction(tool, params, beforeUrl) {
	return beginAction(actionTimeline, {
		tool,
		paramsSummary: toActionParamsSummary(params),
		beforeUrl,
	});
}
export function finishTrackedAction(actionId, updates) {
	return finishAction(actionTimeline, actionId, updates);
}
export function getSinceTimestamp(sinceActionId) {
	if (!sinceActionId) return 0;
	const action = findAction(actionTimeline, sinceActionId);
	if (!action) return 0;
	return action.startedAt ?? 0;
}
export function getConsoleEntriesSince(sinceActionId) {
	const since = getSinceTimestamp(sinceActionId);
	return getConsoleLogs().filter((entry) => entry.timestamp >= since);
}
export function getNetworkEntriesSince(sinceActionId) {
	const since = getSinceTimestamp(sinceActionId);
	return getNetworkLogs().filter((entry) => entry.timestamp >= since);
}
// ---------------------------------------------------------------------------
// Error summary
// ---------------------------------------------------------------------------
export function getRecentErrors(pageUrl) {
	const parts = [];
	const now = Date.now();
	const since = now - 12_000;
	const toOrigin = (url) => {
		try {
			return new URL(url).origin;
		} catch {
			return null;
		}
	};
	const pageOrigin = toOrigin(pageUrl);
	const sameOrigin = (url) => !pageOrigin || toOrigin(url) === pageOrigin;
	const summarize = (items, max) => {
		const counts = new Map();
		const order = [];
		for (const item of items) {
			if (!counts.has(item)) order.push(item);
			counts.set(item, (counts.get(item) ?? 0) + 1);
		}
		return order.slice(0, max).map((item) => {
			const count = counts.get(item) ?? 1;
			return count > 1 ? `${item} (x${count})` : item;
		});
	};
	const consoleLogs = getConsoleLogs();
	const jsWarnings = consoleLogs
		.filter(
			(e) =>
				(e.type === "error" || e.type === "pageerror") &&
				e.timestamp >= since &&
				sameOrigin(e.url),
		)
		.map((e) => e.text.slice(0, 120));
	if (jsWarnings.length > 0) {
		parts.push("JS: " + summarize(jsWarnings, 2).join(" | "));
	}
	const actionableStatus = new Set([401, 403, 404, 408, 409, 422, 429]);
	const actionableTypes = new Set(["document", "fetch", "xhr", "script"]);
	const networkLogs = getNetworkLogs();
	const netWarnings = networkLogs
		.filter((e) => e.timestamp >= since && sameOrigin(e.url))
		.filter((e) => {
			if (e.failed) return actionableTypes.has(e.resourceType);
			if (e.status === null) return false;
			if (e.status >= 500) return true;
			return (
				actionableStatus.has(e.status) && actionableTypes.has(e.resourceType)
			);
		})
		.map((e) => {
			if (e.failed) return `${e.method} ${e.resourceType} FAILED`;
			return `${e.method} ${e.resourceType} ${e.status}`;
		});
	if (netWarnings.length > 0) {
		parts.push("Network: " + summarize(netWarnings, 2).join(" | "));
	}
	const dialogLogs = getDialogLogs();
	const dialogWarnings = dialogLogs
		.filter((e) => e.timestamp >= since && sameOrigin(e.url))
		.map((e) => `${e.type}: ${e.message.slice(0, 80)}`);
	if (dialogWarnings.length > 0) {
		parts.push("Dialogs: " + summarize(dialogWarnings, 1).join(" | "));
	}
	if (parts.length === 0) return "";
	return `\n\nWarnings: ${parts.join("; ")}\nUse browser_get_console_logs/browser_get_network_logs for full diagnostics.`;
}
// ---------------------------------------------------------------------------
// Ref helpers (parsing / formatting — no browser evaluate)
// ---------------------------------------------------------------------------
export function parseRef(input) {
	const trimmed = input.trim().toLowerCase();
	const token = trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
	const versioned = token.match(/^v(\d+):(e\d+)$/);
	if (versioned) {
		const version = parseInt(versioned[1], 10);
		const key = versioned[2];
		return { key, version, display: `@v${version}:${key}` };
	}
	return { key: token, version: null, display: `@${token}` };
}
export function formatVersionedRef(version, key) {
	return `@v${version}:${key}`;
}
export function staleRefGuidance(refDisplay, reason) {
	return `Ref ${refDisplay} could not be resolved (${reason}). The ref is likely stale after DOM/navigation changes. Call browser_snapshot_refs again to refresh refs.`;
}
// ---------------------------------------------------------------------------
// Compact state summary formatting
// ---------------------------------------------------------------------------
export function formatCompactStateSummary(state) {
	const lines = [];
	lines.push(`Title: ${state.title}`);
	lines.push(`URL: ${state.url}`);
	lines.push(
		`Elements: ${state.counts.landmarks} landmarks, ${state.counts.buttons} buttons, ${state.counts.links} links, ${state.counts.inputs} inputs`,
	);
	if (state.headings.length > 0) {
		lines.push(
			"Headings: " +
				state.headings
					.map((text, index) => `H${index + 1} "${text}"`)
					.join(", "),
		);
	}
	if (state.focus) {
		lines.push(`Focused: ${state.focus}`);
	}
	if (state.dialog.title) {
		lines.push(`Active dialog: "${state.dialog.title}"`);
	}
	lines.push(
		"Use browser_find for targeted discovery, browser_assert for verification, or browser_get_accessibility_tree for full detail.",
	);
	return lines.join("\n");
}
