/**
 * browser-tools — DOM settle logic
 *
 * Adaptive settling after browser actions. Polls for DOM quiet (mutation
 * counter stable, no pending critical requests, optional focus stability)
 * before returning control.
 */
import { getPendingCriticalRequests } from "./utils.js";
// ---------------------------------------------------------------------------
// Mutation counter (installed in-page via evaluate)
// ---------------------------------------------------------------------------
export async function ensureMutationCounter(p) {
	await p.evaluate(() => {
		const key = "__piMutationCounter";
		const installedKey = "__piMutationCounterInstalled";
		const w = window;
		if (typeof w[key] !== "number") w[key] = 0;
		if (w[installedKey]) return;
		const observer = new MutationObserver(() => {
			const current = typeof w[key] === "number" ? w[key] : 0;
			w[key] = current + 1;
		});
		observer.observe(document.documentElement || document.body, {
			subtree: true,
			childList: true,
			attributes: true,
			characterData: true,
		});
		w[installedKey] = true;
	});
}
export async function readMutationCounter(p) {
	try {
		return await p.evaluate(() => {
			const w = window;
			const value = w.__piMutationCounter;
			return typeof value === "number" ? value : 0;
		});
	} catch {
		return 0;
	}
}
// ---------------------------------------------------------------------------
// Focus descriptor (for focus-stability checks)
// ---------------------------------------------------------------------------
export async function readFocusedDescriptor(target) {
	try {
		return await target.evaluate(() => {
			const el = document.activeElement;
			if (!el || el === document.body || el === document.documentElement)
				return "";
			const id = el.id ? `#${el.id}` : "";
			const role = el.getAttribute("role") || "";
			const name = (
				el.getAttribute("aria-label") ||
				el.getAttribute("name") ||
				""
			).trim();
			return `${el.tagName.toLowerCase()}${id}|${role}|${name}`;
		});
	} catch {
		return "";
	}
}
// ---------------------------------------------------------------------------
// Combined settle-state reader (mutation counter + focus in one evaluate)
// ---------------------------------------------------------------------------
/**
 * Reads the mutation counter and optionally the focused element descriptor
 * in a single `evaluate()` call, saving one round-trip per poll iteration.
 */
async function readSettleState(target, checkFocus) {
	try {
		return await target.evaluate((wantFocus) => {
			const w = window;
			const mutationCount =
				typeof w.__piMutationCounter === "number" ? w.__piMutationCounter : 0;
			if (!wantFocus) return { mutationCount, focusDescriptor: "" };
			const el = document.activeElement;
			if (!el || el === document.body || el === document.documentElement) {
				return { mutationCount, focusDescriptor: "" };
			}
			const id = el.id ? `#${el.id}` : "";
			const role = el.getAttribute("role") || "";
			const name = (
				el.getAttribute("aria-label") ||
				el.getAttribute("name") ||
				""
			).trim();
			return {
				mutationCount,
				focusDescriptor: `${el.tagName.toLowerCase()}${id}|${role}|${name}`,
			};
		}, checkFocus);
	} catch {
		return { mutationCount: 0, focusDescriptor: "" };
	}
}
// ---------------------------------------------------------------------------
// Adaptive settle
// ---------------------------------------------------------------------------
/** Threshold (ms) after which zero mutations triggers a shortened quiet window. */
const ZERO_MUTATION_THRESHOLD_MS = 60;
/** Shortened quiet window when no mutations have been observed. */
const ZERO_MUTATION_QUIET_MS = 30;
export async function settleAfterActionAdaptive(p, opts = {}) {
	const timeoutMs = Math.max(150, opts.timeoutMs ?? 500);
	const pollMs = Math.min(100, Math.max(20, opts.pollMs ?? 40));
	const baseQuietWindowMs = Math.max(60, opts.quietWindowMs ?? 100);
	const checkFocus = opts.checkFocusStability ?? false;
	const startedAt = Date.now();
	let polls = 0;
	let sawUrlChange = false;
	let lastActivityAt = startedAt;
	let previousUrl = p.url();
	let totalMutationsSeen = 0;
	let activeQuietWindowMs = baseQuietWindowMs;
	// Install mutation counter + read initial state in one evaluate sequence.
	// ensureMutationCounter must run first (installs the observer), then we
	// read the baseline via the combined reader.
	await ensureMutationCounter(p).catch((e) => {
		if (process.env.SF_DEBUG)
			console.error("[browser-tools] ensureMutationCounter failed:", e.message);
	});
	const initial = await readSettleState(p, checkFocus);
	let previousMutationCount = initial.mutationCount;
	let previousFocus = initial.focusDescriptor;
	while (Date.now() - startedAt < timeoutMs) {
		await new Promise((resolve) => setTimeout(resolve, pollMs));
		polls += 1;
		const now = Date.now();
		const currentUrl = p.url();
		if (currentUrl !== previousUrl) {
			sawUrlChange = true;
			previousUrl = currentUrl;
			lastActivityAt = now;
		}
		// Single combined evaluate for mutation count + focus descriptor.
		const state = await readSettleState(p, checkFocus);
		if (state.mutationCount > previousMutationCount) {
			totalMutationsSeen += state.mutationCount - previousMutationCount;
			previousMutationCount = state.mutationCount;
			lastActivityAt = now;
		}
		if (checkFocus && state.focusDescriptor !== previousFocus) {
			previousFocus = state.focusDescriptor;
			lastActivityAt = now;
		}
		const pendingCritical = getPendingCriticalRequests(p);
		if (pendingCritical > 0) {
			lastActivityAt = now;
			continue;
		}
		// Zero-mutation short-circuit: after ZERO_MUTATION_THRESHOLD_MS with
		// no mutations observed at all, reduce the quiet window to settle faster.
		if (
			totalMutationsSeen === 0 &&
			now - startedAt >= ZERO_MUTATION_THRESHOLD_MS &&
			activeQuietWindowMs !== ZERO_MUTATION_QUIET_MS
		) {
			activeQuietWindowMs = ZERO_MUTATION_QUIET_MS;
		}
		if (now - lastActivityAt >= activeQuietWindowMs) {
			const usedShortcut =
				activeQuietWindowMs === ZERO_MUTATION_QUIET_MS &&
				totalMutationsSeen === 0;
			return {
				settleMode: "adaptive",
				settleMs: now - startedAt,
				settleReason: usedShortcut
					? "zero_mutation_shortcut"
					: sawUrlChange
						? "url_changed_then_quiet"
						: "dom_quiet",
				settlePolls: polls,
			};
		}
	}
	return {
		settleMode: "adaptive",
		settleMs: Date.now() - startedAt,
		settleReason: "timeout_fallback",
		settlePolls: polls,
	};
}
