// ABOUTME: Remote web testing extension using Cloudflare Browser Rendering for screenshots, content extraction, and a11y.
// ABOUTME: Registers /web-remote command and web_remote tool backed by a deployed Cloudflare Worker.
// ABOUTME: REMOTE ONLY — cannot access localhost, 127.0.0.1, or local network. Use agent-browser skill for local testing.
/**
 * Web Remote -- Cloudflare Browser Rendering powered REMOTE web testing
 *
 * IMPORTANT: This is a REMOTE service. It CANNOT access localhost, 127.0.0.1,
 * or any local network address. For local testing, use the agent-browser skill instead.
 *
 * Uses a deployed Cloudflare Worker (pi-web-test) with Browser Rendering
 * binding to provide headless browser capabilities:
 *
 *   - Screenshot any URL at custom viewport sizes
 *   - Extract page text/HTML content (with optional CSS selector)
 *   - Run accessibility audits via axe-core
 *   - Capture responsive screenshots at mobile/tablet/desktop breakpoints
 *
 * Screenshots are saved to .pi/web-test-captures/ and paths are returned
 * so the agent can Read them to visually inspect pages.
 *
 * Commands:
 *   /web-remote screenshot <url>          -- capture a screenshot
 *   /web-remote content <url> [selector]  -- extract page content
 *   /web-remote a11y <url>                -- accessibility audit
 *   /web-remote responsive <url>          -- multi-viewport screenshots
 *
 * Tool:
 *   web_remote                            -- programmatic access (agent can call)
 *
 * Prerequisites:
 *   - Cloudflare Worker deployed (auto-deployed on first use)
 *   - wrangler CLI authenticated
 *   - API key in agent/extensions/web-test-worker/.env
 *
 * Usage: pi -e extensions/web-test.ts
 */

import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Type } from "@sinclair/typebox";
import { type AutocompleteItem } from "@mariozechner/pi-tui";
import { Text } from "@mariozechner/pi-tui";
import { execSync } from "child_process";
import { existsSync, mkdirSync, writeFileSync, readFileSync } from "fs";
import { join, dirname } from "path";
import { fileURLToPath } from "url";

// ── Constants ────────────────────────────────────

const CAPTURE_DIR_NAME = "web-test-captures";
const WORKER_NAME = "pi-web-test";

// ── Types ────────────────────────────────────────

type Action = "screenshot" | "content" | "a11y" | "responsive";

interface WorkerConfig {
	workerUrl: string;
	apiKey: string;
}

interface WebTestResult {
	action: Action;
	url: string;
	success: boolean;
	screenshots?: string[];
	data?: any;
	error?: string;
	elapsed: number;
}

// ── Config Loading ───────────────────────────────

function loadWorkerConfig(): WorkerConfig | null {
	const extDir = dirname(fileURLToPath(import.meta.url));
	const envPath = join(extDir, "web-test-worker", ".env");

	if (!existsSync(envPath)) {
		return null;
	}

	const content = readFileSync(envPath, "utf-8");
	const vars: Record<string, string> = {};
	for (const line of content.split("\n")) {
		const trimmed = line.trim();
		if (!trimmed || trimmed.startsWith("#")) continue;
		const eq = trimmed.indexOf("=");
		if (eq > 0) {
			vars[trimmed.slice(0, eq).trim()] = trimmed.slice(eq + 1).trim();
		}
	}

	if (!vars.WORKER_URL || !vars.API_KEY) {
		return null;
	}

	return { workerUrl: vars.WORKER_URL, apiKey: vars.API_KEY };
}

// ── Capture Directory ────────────────────────────

function ensureCaptureDir(cwd: string): string {
	const captureDir = join(cwd, ".pi", CAPTURE_DIR_NAME);
	if (!existsSync(captureDir)) {
		mkdirSync(captureDir, { recursive: true });
	}
	return captureDir;
}

function timestamp(): string {
	const now = new Date();
	const pad = (n: number, len = 2) => String(n).padStart(len, "0");
	return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
}

// ── Worker Deployment ────────────────────────────

function checkWorkerHealth(config: WorkerConfig): boolean {
	try {
		const result = execSync(
			`curl -sf --max-time 5 "${config.workerUrl}/ping"`,
			{ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] },
		);
		const parsed = JSON.parse(result);
		return parsed.status === "ok";
	} catch {
		return false;
	}
}

function deployWorker(): { success: boolean; url?: string; error?: string } {
	const extDir = dirname(fileURLToPath(import.meta.url));
	const workerDir = join(extDir, "web-test-worker");

	if (!existsSync(join(workerDir, "node_modules"))) {
		try {
			execSync("npm install", { cwd: workerDir, stdio: "ignore", timeout: 60000 });
		} catch (e: any) {
			return { success: false, error: `npm install failed: ${e.message}` };
		}
	}

	try {
		const output = execSync("npx wrangler deploy 2>&1", {
			cwd: workerDir,
			encoding: "utf-8",
			timeout: 60000,
		});

		// Extract URL from deploy output
		const urlMatch = output.match(/https:\/\/[\w-]+\.[\w-]+\.workers\.dev/);
		if (urlMatch) {
			return { success: true, url: urlMatch[0] };
		}

		return { success: true, url: undefined };
	} catch (e: any) {
		return { success: false, error: `wrangler deploy failed: ${e.stdout || e.message}` };
	}
}

// ── Worker API Calls ─────────────────────────────

async function callWorker(
	config: WorkerConfig,
	endpoint: string,
	body: Record<string, any>,
): Promise<Response> {
	const resp = await fetch(`${config.workerUrl}${endpoint}`, {
		method: "POST",
		headers: {
			"Content-Type": "application/json",
			"X-Api-Key": config.apiKey,
		},
		body: JSON.stringify(body),
	});
	return resp;
}

// ── Action Handlers ──────────────────────────────

async function doScreenshot(
	config: WorkerConfig,
	url: string,
	cwd: string,
	opts: { width?: number; height?: number; fullPage?: boolean },
): Promise<WebTestResult> {
	const start = Date.now();

	const resp = await callWorker(config, "/screenshot", {
		url,
		width: opts.width ?? 1280,
		height: opts.height ?? 720,
		fullPage: opts.fullPage ?? false,
	});

	if (!resp.ok) {
		const err = await resp.json().catch(() => ({ error: resp.statusText })) as any;
		return { action: "screenshot", url, success: false, error: err.error || resp.statusText, elapsed: Date.now() - start };
	}

	const captureDir = ensureCaptureDir(cwd);
	const ts = timestamp();
	const filename = `screenshot-${ts}.png`;
	const filePath = join(captureDir, filename);

	const buffer = Buffer.from(await resp.arrayBuffer());
	writeFileSync(filePath, buffer);

	const title = decodeURIComponent(resp.headers.get("X-Page-Title") || "untitled");

	return {
		action: "screenshot",
		url,
		success: true,
		screenshots: [filePath],
		data: { title, width: opts.width ?? 1280, height: opts.height ?? 720, sizeBytes: buffer.length },
		elapsed: Date.now() - start,
	};
}

async function doContent(
	config: WorkerConfig,
	url: string,
	opts: { selector?: string },
): Promise<WebTestResult> {
	const start = Date.now();

	const resp = await callWorker(config, "/content", { url, selector: opts.selector });

	if (!resp.ok) {
		const err = await resp.json().catch(() => ({ error: resp.statusText })) as any;
		return { action: "content", url, success: false, error: err.error || resp.statusText, elapsed: Date.now() - start };
	}

	const data = await resp.json();

	return {
		action: "content",
		url,
		success: true,
		data,
		elapsed: Date.now() - start,
	};
}

async function doA11y(
	config: WorkerConfig,
	url: string,
): Promise<WebTestResult> {
	const start = Date.now();

	const resp = await callWorker(config, "/a11y", { url });

	if (!resp.ok) {
		const err = await resp.json().catch(() => ({ error: resp.statusText })) as any;
		return { action: "a11y", url, success: false, error: err.error || resp.statusText, elapsed: Date.now() - start };
	}

	const data = await resp.json();

	return {
		action: "a11y",
		url,
		success: true,
		data,
		elapsed: Date.now() - start,
	};
}

async function doResponsive(
	config: WorkerConfig,
	url: string,
	cwd: string,
	opts: { viewports?: Array<{ name: string; width: number; height: number }> },
): Promise<WebTestResult> {
	const start = Date.now();

	const resp = await callWorker(config, "/responsive", {
		url,
		viewports: opts.viewports,
	});

	if (!resp.ok) {
		const err = await resp.json().catch(() => ({ error: resp.statusText })) as any;
		return { action: "responsive", url, success: false, error: err.error || resp.statusText, elapsed: Date.now() - start };
	}

	const data = await resp.json() as any;

	// Save each screenshot as a separate PNG
	const captureDir = ensureCaptureDir(cwd);
	const ts = timestamp();
	const savedPaths: string[] = [];

	if (data.screenshots && Array.isArray(data.screenshots)) {
		for (const shot of data.screenshots) {
			const filename = `responsive-${shot.name}-${ts}.png`;
			const filePath = join(captureDir, filename);
			const buffer = Buffer.from(shot.base64, "base64");
			writeFileSync(filePath, buffer);
			savedPaths.push(filePath);
		}
	}

	return {
		action: "responsive",
		url,
		success: true,
		screenshots: savedPaths,
		data: {
			title: data.title,
			viewports: data.viewports,
		},
		elapsed: Date.now() - start,
	};
}

// ── Result Formatting ────────────────────────────

function formatResult(result: WebTestResult): string {
	const lines: string[] = [];

	if (!result.success) {
		lines.push(`Error: ${result.error}`);
		lines.push(`URL: ${result.url}`);
		lines.push(`Elapsed: ${Math.round(result.elapsed / 1000)}s`);
		return lines.join("\n");
	}

	lines.push(`Web test complete: ${result.action}`);
	lines.push(`URL: ${result.url}`);
	lines.push(`Elapsed: ${Math.round(result.elapsed / 1000)}s`);
	lines.push("");

	switch (result.action) {
		case "screenshot": {
			const d = result.data;
			lines.push(`Page title: ${d.title}`);
			lines.push(`Viewport: ${d.width}x${d.height}`);
			lines.push(`File size: ${(d.sizeBytes / 1024).toFixed(1)} KB`);
			lines.push("");
			if (result.screenshots?.length) {
				lines.push("Screenshot saved:");
				for (const p of result.screenshots) lines.push(`  ${p}`);
				lines.push("");
				lines.push("Use Read on the path above to view the captured page.");
			}
			break;
		}
		case "content": {
			const d = result.data as any;
			lines.push(`Page title: ${d.title}`);
			lines.push(`Text length: ${d.textLength} chars`);
			lines.push(`HTML length: ${d.htmlLength} chars`);
			lines.push("");
			lines.push("--- Page Text ---");
			// Truncate for display
			const text = d.text as string;
			lines.push(text.length > 2000 ? text.slice(0, 2000) + "\n...[truncated]" : text);
			break;
		}
		case "a11y": {
			const d = result.data as any;
			lines.push(`Page title: ${d.title}`);
			lines.push("");
			lines.push(`Summary:`);
			lines.push(`  Violations: ${d.summary.violations}`);
			lines.push(`  Passes: ${d.summary.passes}`);
			lines.push(`  Incomplete: ${d.summary.incomplete}`);
			lines.push(`  Inapplicable: ${d.summary.inapplicable}`);

			if (d.violations && d.violations.length > 0) {
				lines.push("");
				lines.push("Violations:");
				for (const v of d.violations) {
					lines.push(`  [${v.impact}] ${v.id}: ${v.description}`);
					lines.push(`    Help: ${v.help}`);
					lines.push(`    Affected nodes: ${v.nodes}`);
					lines.push(`    More info: ${v.helpUrl}`);
					lines.push("");
				}
			} else {
				lines.push("");
				lines.push("No accessibility violations found.");
			}
			break;
		}
		case "responsive": {
			const d = result.data as any;
			lines.push(`Page title: ${d.title}`);
			lines.push("");

			if (d.viewports && d.viewports.length > 0) {
				lines.push("Viewports captured:");
				for (const vp of d.viewports) {
					lines.push(`  ${vp.name}: ${vp.width}x${vp.height}`);
				}
			}

			if (result.screenshots?.length) {
				lines.push("");
				lines.push("Screenshots saved:");
				for (const p of result.screenshots) lines.push(`  ${p}`);
				lines.push("");
				lines.push("Use Read on any path above to view the captured page.");
			}
			break;
		}
	}

	return lines.join("\n");
}

// ── Extension ────────────────────────────────────

export default function (pi: ExtensionAPI) {

	let config: WorkerConfig | null = null;

	function getConfig(): WorkerConfig | null {
		if (config) return config;
		config = loadWorkerConfig();
		return config;
	}

	function ensureWorker(): { config: WorkerConfig | null; error?: string } {
		const cfg = getConfig();
		if (!cfg) {
			return {
				config: null,
				error: "Worker not configured. Missing .env file at agent/extensions/web-test-worker/.env with WORKER_URL and API_KEY.",
			};
		}

		// Quick health check
		if (!checkWorkerHealth(cfg)) {
			// Try redeploying
			const result = deployWorker();
			if (!result.success) {
				return { config: null, error: `Worker health check failed and redeploy failed: ${result.error}` };
			}
			if (result.url && result.url !== cfg.workerUrl) {
				cfg.workerUrl = result.url;
			}
		}

		return { config: cfg };
	}

	// ── /web-test command ────────────────────────

	const ACTIONS = ["screenshot", "content", "a11y", "responsive"];

	pi.registerCommand("web-remote", {
		description: "Test REMOTE web pages using Cloudflare Browser Rendering (screenshot, content, a11y, responsive). CANNOT access localhost — use agent-browser for local testing.",
		getArgumentCompletions: (prefix: string): AutocompleteItem[] | null => {
			const items = ACTIONS.map(a => ({
				value: a,
				label: a === "screenshot" ? "screenshot <url> -- capture a PNG screenshot"
					: a === "content" ? "content <url> [selector] -- extract page text/HTML"
					: a === "a11y" ? "a11y <url> -- accessibility audit via axe-core"
					: "responsive <url> -- multi-viewport screenshots",
			}));
			const filtered = items.filter(i => i.value.startsWith(prefix));
			return filtered.length > 0 ? filtered : items;
		},
		handler: async (args, ctx) => {
			const parts = (args ?? "").trim().split(/\s+/);
			const action = parts[0]?.toLowerCase();
			const url = parts[1];

			if (!action || !ACTIONS.includes(action)) {
				ctx.ui.notify(
					"Usage: /web-remote <action> <url>\n" +
					"Actions: screenshot, content, a11y, responsive\n" +
					"NOTE: Remote only — cannot access localhost. Use agent-browser for local testing.",
					"warning",
				);
				return;
			}

			if (!url) {
				ctx.ui.notify(`Usage: /web-remote ${action} <url>`, "warning");
				return;
			}

			const { config: cfg, error } = ensureWorker();
			if (!cfg) {
				ctx.ui.notify(error!, "error");
				return;
			}

			ctx.ui.notify(`Running ${action} on ${url}...`, "info");

			let result: WebTestResult;

			switch (action) {
				case "screenshot":
					result = await doScreenshot(cfg, url, ctx.cwd, {});
					break;
				case "content":
					result = await doContent(cfg, url, { selector: parts[2] });
					break;
				case "a11y":
					result = await doA11y(cfg, url);
					break;
				case "responsive":
					result = await doResponsive(cfg, url, ctx.cwd, {});
					break;
				default:
					return;
			}

			if (result.success) {
				const msg = result.screenshots?.length
					? `${action} complete (${Math.round(result.elapsed / 1000)}s). ${result.screenshots.length} file(s) saved.`
					: `${action} complete (${Math.round(result.elapsed / 1000)}s).`;
				ctx.ui.notify(msg, "success");
			} else {
				ctx.ui.notify(`${action} failed: ${result.error}`, "error");
			}

			return formatResult(result);
		},
	});

	// ── web_remote tool ──────────────────────────

	pi.registerTool({
		name: "web_remote",
		label: "Web Remote",
		description: [
			"Test REMOTE web pages using Cloudflare Browser Rendering.",
			"IMPORTANT: This is a REMOTE service — it CANNOT access localhost, 127.0.0.1,",
			"or any local network address. For localhost testing, use the agent-browser skill",
			"(via Bash: agent-browser open <url>, agent-browser snapshot -i, etc.).",
			"",
			"Captures screenshots, extracts content, runs accessibility audits,",
			"and tests responsive layouts via a remote headless Chromium browser.",
			"",
			"Actions:",
			"  screenshot  -- capture a PNG screenshot (returns file path for Read tool)",
			"  content     -- extract page text and HTML (with optional CSS selector)",
			"  a11y        -- run axe-core accessibility audit",
			"  responsive  -- capture at mobile (375px), tablet (768px), desktop (1440px)",
			"",
			"Screenshot paths can be passed to the Read tool to visually inspect pages.",
		].join("\n"),
		parameters: Type.Object({
			action: Type.String({
				description: "Action to perform: screenshot, content, a11y, responsive",
			}),
			url: Type.String({
				description: "URL to test (must be http: or https:)",
			}),
			width: Type.Optional(Type.Number({ description: "Viewport width in pixels (default: 1280, screenshot only)" })),
			height: Type.Optional(Type.Number({ description: "Viewport height in pixels (default: 720, screenshot only)" })),
			fullPage: Type.Optional(Type.Boolean({ description: "Capture full page scroll (default: false, screenshot only)" })),
			selector: Type.Optional(Type.String({ description: "CSS selector to extract (content action only)" })),
		}),

		async execute(_toolCallId, params, _signal, onUpdate, ctx) {
			const { action, url, width, height, fullPage, selector } =
				params as { action: string; url: string; width?: number; height?: number; fullPage?: boolean; selector?: string };

			// Validate action
			if (!ACTIONS.includes(action)) {
				return {
					content: [{ type: "text" as const, text: `Unknown action: ${action}. Available: ${ACTIONS.join(", ")}` }],
					details: { error: `Unknown action: ${action}` },
				};
			}

			// Validate URL
			try {
				const parsed = new URL(url);
				if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
					return {
						content: [{ type: "text" as const, text: "Only http: and https: URLs are allowed." }],
						details: { error: "Invalid protocol" },
					};
				}
			} catch {
				return {
					content: [{ type: "text" as const, text: `Invalid URL: ${url}` }],
					details: { error: "Invalid URL" },
				};
			}

			const { config: cfg, error } = ensureWorker();
			if (!cfg) {
				return {
					content: [{ type: "text" as const, text: error! }],
					details: { error },
				};
			}

			if (onUpdate) {
				onUpdate({
					content: [{ type: "text" as const, text: `Running ${action} on ${url}...` }],
					details: { action, url, status: "running" },
				});
			}

			let result: WebTestResult;

			switch (action) {
				case "screenshot":
					result = await doScreenshot(cfg, url, ctx.cwd, { width, height, fullPage });
					break;
				case "content":
					result = await doContent(cfg, url, { selector });
					break;
				case "a11y":
					result = await doA11y(cfg, url);
					break;
				case "responsive":
					result = await doResponsive(cfg, url, ctx.cwd, {});
					break;
				default:
					result = { action: action as Action, url, success: false, error: "Unknown action", elapsed: 0 };
			}

			const output = formatResult(result);

			return {
				content: [{ type: "text" as const, text: output }],
				details: {
					action,
					url,
					status: result.success ? "done" : "error",
					screenshots: result.screenshots,
					data: result.data,
					elapsed: result.elapsed,
				},
			};
		},

		renderCall(_params, _theme) {
			const p = _params as { action: string; url: string };
			const DIM = "\x1b[90m";
			const BRIGHT = "\x1b[1;97m";
			const RST = "\x1b[0m";
			return new Text(`${DIM}web-remote:${RST} ${BRIGHT}${p.action}${RST} ${DIM}${p.url}${RST}`, 0, 0);
		},

		renderResult(result, _options, _theme) {
			const details = result.details as any;
			const DIM = "\x1b[90m";
			const GREEN = "\x1b[32m";
			const RED = "\x1b[91m";
			const BRIGHT = "\x1b[1;97m";
			const YELLOW = "\x1b[33m";
			const RST = "\x1b[0m";

			if (details?.status === "error") {
				return new Text(`${RED}failed${RST} ${DIM}${details?.action || ""}${RST}`, 0, 0);
			}

			const elapsed = details?.elapsed ? Math.round(details.elapsed / 1000) : 0;
			const action = details?.action || "";

			switch (action) {
				case "screenshot": {
					const count = details?.screenshots?.length ?? 0;
					return new Text(
						`${GREEN}captured${RST} ${BRIGHT}${count}${RST} ${DIM}screenshot in ${elapsed}s${RST}`,
						0, 0,
					);
				}
				case "content": {
					const len = details?.data?.textLength ?? 0;
					return new Text(
						`${GREEN}extracted${RST} ${BRIGHT}${len}${RST} ${DIM}chars in ${elapsed}s${RST}`,
						0, 0,
					);
				}
				case "a11y": {
					const violations = details?.data?.summary?.violations ?? 0;
					const passes = details?.data?.summary?.passes ?? 0;
					const color = violations > 0 ? YELLOW : GREEN;
					return new Text(
						`${color}${violations} violations${RST} ${DIM}${passes} passes in ${elapsed}s${RST}`,
						0, 0,
					);
				}
				case "responsive": {
					const count = details?.screenshots?.length ?? 0;
					return new Text(
						`${GREEN}captured${RST} ${BRIGHT}${count}${RST} ${DIM}viewports in ${elapsed}s${RST}`,
						0, 0,
					);
				}
				default:
					return new Text(`${GREEN}done${RST} ${DIM}in ${elapsed}s${RST}`, 0, 0);
			}
		},
	});

	// ── Session start ────────────────────────────

	pi.on("session_start", async (_event, ctx) => {
		ensureCaptureDir(ctx.cwd);
	});
}
