import { eyeText } from "../earlyText";
import { ParameterInfo } from "../ParameterControl";
import { parameterContext } from "../parameterContext";
import { observeVisibility } from "../visibility";
import "./pupil.css";
import { ChevronDown, ChevronUp, Settings2 } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { createIrisScene, type IrisScene } from "./irisScene";
import {
	createPupilMessageReader,
	drawPupilGraph,
	type PupilSample,
	validPupil,
} from "./neonPupil";

export default function PupilHardwareSync({
	lang = "en",
}: {
	lang?: "en" | "es";
}) {
	const t = eyeText(lang);
	const mountRef = useRef<HTMLDivElement>(null);
	const sceneRef = useRef<IrisScene | null>(null);
	const [deviceIp, setDeviceIp] = useState("neon.local");
	const [status, setStatus] = useState<
		"disconnected" | "connecting" | "streaming" | "error"
	>("disconnected");
	const [errorMsg, setErrorMsg] = useState("");
	const [pupilMm, setPupilMm] = useState<PupilSample>({
		left: null,
		right: null,
	});
	const [isSettingsOpen, setIsSettingsOpen] = useState(true);

	// Disposal deliberately has no React state updates: safe on unmount/cancel.
	const disposeConnectionRef = useRef<(() => void) | null>(null);
	const canvasRef = useRef<HTMLCanvasElement>(null);
	const historyRef = useRef<PupilSample[]>([]);

	useEffect(() => {
		if (!mountRef.current) return;
		const reset = mountRef.current
			.closest(".interactive-viewer")
			?.querySelector<HTMLButtonElement>(".viewer-reset");
		if (reset) {
			const label =
				lang === "es"
					? "Restablecer cámara; mantener conexión"
					: "Reset camera; keep streaming";
			reset.setAttribute("aria-label", label);
			reset.title = label;
		}
		sceneRef.current = createIrisScene(mountRef.current, {
			pupilRadius: 3.0 * (0.5 / 12.0),
			pigmentation: 1,
			showMuscles: false,
		});

		return () => {
			if (sceneRef.current) {
				sceneRef.current.dispose();
				sceneRef.current = null;
			}
			disposeConnectionRef.current?.();
		};
	}, []);

	const toggleConnection = async () => {
		if (status === "streaming" || status === "connecting") {
			disposeConnectionRef.current?.();
			setStatus("disconnected");
			return;
		}

		disposeConnectionRef.current?.();
		setStatus("connecting");
		setErrorMsg("");
		setPupilMm({ left: null, right: null });
		historyRef.current = [];
		const canvas = canvasRef.current;
		const context = canvas?.getContext("2d");
		if (canvas && context)
			drawPupilGraph(context, [], canvas.width, canvas.height);

		const abort = new AbortController();
		const sockets = new Set<WebSocket>();
		const readers = new Set<ReturnType<typeof createPupilMessageReader>>();
		let frame: HTMLIFrameElement | null = null;
		let disposed = false;
		let lastSampleAt = performance.now();
		let receivedFirstSample = false;
		let monitorStarted = false;
		let animationFrame: number | null = null;
		let latest: PupilSample | null = null;
		let lastHudAt = -Infinity;
		let renderingVisible = false;
		let schedulePresentation = () => {};
		const stopVisibility = mountRef.current
			? observeVisibility(
					mountRef.current.closest(".pupil-demo") ?? mountRef.current,
					(active) => {
						renderingVisible = active;
						if (active) schedulePresentation();
						else if (animationFrame !== null) {
							cancelAnimationFrame(animationFrame);
							animationFrame = null;
						}
					},
				)
			: () => {};
		let restoreStorage: (() => void) | null = null;
		const watchdog = window.setInterval(() => {
			// The browser permission prompt can remain open until the user responds.
			if (!monitorStarted) return;
			if (
				performance.now() - lastSampleAt >
				(receivedFirstSample ? 5000 : 15000)
			) {
				fail(
					receivedFirstSample
						? "Pupil stream stopped. Reconnect to try again."
						: "No pupil stream received. Check the device and enable Compute Eye State.",
				);
			}
		}, 1000);
		const dispose = () => {
			if (disposed) return;
			disposed = true;
			abort.abort();
			stopVisibility();
			window.clearInterval(watchdog);
			if (animationFrame !== null) cancelAnimationFrame(animationFrame);
			for (const reader of readers) reader.dispose();
			readers.clear();
			for (const socket of sockets) socket.close();
			sockets.clear();
			frame?.remove(); // destroys the Monitor document's listeners and timers
			restoreStorage?.();
			if (disposeConnectionRef.current === dispose)
				disposeConnectionRef.current = null;
		};
		disposeConnectionRef.current = dispose;
		const fail = (message: string) => {
			if (disposed) return;
			dispose();
			setErrorMsg(message);
			setStatus("error");
		};
		try {
			const safeIp = deviceIp.trim().toLowerCase();
			const ipv4 =
				/^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(safeIp) &&
				safeIp.split(".").every((part) => Number(part) <= 255);
			if (!ipv4 && safeIp !== "localhost" && safeIp !== "neon.local") {
				throw new Error(
					"Please provide a valid device IPv4 address, localhost or neon.local.",
				);
			}

			// Keep the Companion Monitor bundle and its WebSocket integration.
			// A hidden iframe gives its DOM, listeners and timers a disposable lifetime.
			// It is same-origin for callbacks, not a security boundary for the bundle.
			frame = document.createElement("iframe");
			frame.title = "Neon Companion Monitor";
			frame.style.display = "none";
			document.body.appendChild(frame);
			const frameDocument = frame.contentDocument!;
			const frameWindow = frame.contentWindow! as Window & {
				_neonObserve?: (socket: WebSocket, url: string) => void;
				_neonSocketUrl?: (url: string | URL) => string;
			};
			const deviceHttp = `http://${safeIp}:8080/`;
			frameWindow._neonSocketUrl = (raw) => {
				const text = String(raw);
				if (/^(?:wss?|https?):\/\//i.test(text)) {
					const url = new URL(text);
					if (url.protocol === "http:") url.protocol = "ws:";
					if (url.protocol === "https:") url.protocol = "wss:";
					return url.href;
				}
				// e.g. "about://host:8080/api/..." or "/api/...": keep path + query.
				const path = text.replace(/^[a-z][a-z0-9+.-]*:(?:\/\/[^/?#]*)?/i, "");
				const url = new URL(
					path.startsWith("/") ? path : `/${path}`,
					deviceHttp,
				);
				url.protocol = "ws:";
				return url.href;
			};
			const base = frameDocument.createElement("base");
			base.href = deviceHttp;
			frameDocument.head.appendChild(base);
			const dummyRoot = frameDocument.createElement("div");
			dummyRoot.id = "neon-dummy-root";
			frameDocument.body.appendChild(dummyRoot);
			// The Monitor app emits unrelated errors/rejections during normal startup
			// (media playback, UI requests). Do not tear down on them: the no-data
			// watchdog reports genuine failures. Log them to help debugging.
			frameWindow.addEventListener("error", (event) =>
				console.warn("[Neon Monitor] error:", event.error ?? event.message),
			);
			frameWindow.addEventListener("unhandledrejection", (event) =>
				console.warn("[Neon Monitor] unhandled rejection:", event.reason),
			);

			schedulePresentation = () => {
				// Measurements may arrive at 200 Hz; presentation follows browser frames.
				if (!disposed && renderingVisible && latest && animationFrame === null)
					animationFrame = requestAnimationFrame(() => {
						animationFrame = null;
						if (disposed || !renderingVisible || !latest) return;
						const values = [latest.left, latest.right].filter(validPupil);
						const mean =
							values.reduce((sum, value) => sum + value, 0) / values.length;
						if (sceneRef.current) {
							sceneRef.current.params.pupilRadius = mean * (0.5 / 12.0);
							sceneRef.current.invalidate();
						}
						const now = performance.now();
						if (now - lastHudAt >= 100) {
							setPupilMm(latest);
							lastHudAt = now;
						}
						const graph = canvasRef.current;
						const ctx = graph?.getContext("2d");
						if (graph && ctx)
							drawPupilGraph(
								ctx,
								historyRef.current,
								graph.width,
								graph.height,
							);
					});
			};
			const onSample = (sample: PupilSample) => {
				if (disposed) return;
				const left = validPupil(sample.left) ? sample.left : null;
				const right = validPupil(sample.right) ? sample.right : null;
				if (left === null && right === null) return;
				lastSampleAt = performance.now();
				if (!receivedFirstSample) {
					receivedFirstSample = true;
					setStatus("streaming");
				}
				latest = { left, right }; // preserve missing eyes as gaps, not invented measurements
				historyRef.current.push(latest);
				if (historyRef.current.length > 1000) historyRef.current.shift();
				schedulePresentation();
			};
			frameWindow._neonObserve = (socket, url) => {
				sockets.add(socket);
				socket.addEventListener("close", () => sockets.delete(socket));
				// Only sockets constructed by the Monitor bundle reach this hook.
				// Do not compare hostnames: the Monitor resolves `neon.local` to the
				// device IP (from /api/status) before opening its stream sockets.
				if (new URL(url).pathname.includes("status")) return;
				const reader = createPupilMessageReader(onSample, () =>
					fail("Could not read the pupil stream. Reconnect to try again."),
				);
				readers.add(reader);
				socket.addEventListener("message", (event) => reader.push(event.data));
				socket.addEventListener("close", () => {
					reader.dispose();
					readers.delete(reader);
				});
				socket.addEventListener("error", () =>
					fail("Neon stream connection failed."),
				);
			};

			// A user-initiated local fetch triggers Chromium's LNA prompt where
			// supported. Other browsers ignore this hint; it grants no permission.
			// https://developer.chrome.com/blog/local-network-access
			const fetchOpts: RequestInit & { targetAddressSpace?: "local" } = {
				signal: abort.signal,
				cache: "no-store",
				...(safeIp !== "localhost" && !safeIp.startsWith("127.")
					? { targetAddressSpace: "local" as const }
					: {}),
			};
			const fetchMonitor = async (url: string) => {
				try {
					return await fetch(url, fetchOpts);
				} catch (error) {
					if (abort.signal.aborted) throw error;
					let lnaDenied = false;
					for (const name of ["local-network-access", "local-network"]) {
						try {
							const state = await navigator.permissions.query({
								name: name as PermissionName,
							});
							if (state.state === "denied") lnaDenied = true;
							break;
						} catch {
							// Permission name not supported by this browser; try the next.
						}
					}
					throw new Error(
						lnaDenied
							? lang === "es"
								? "El navegador ha denegado el acceso a la red local para este sitio. Permítelo en los ajustes del sitio (junto a la dirección), recarga y vuelve a conectar."
								: "Your browser denied local-network access for this site. Allow it in the site settings (beside the address), reload and connect again."
							: lang === "es"
								? "No se pudo acceder a Neon. Comprueba la dirección, la Wi-Fi y los permisos del navegador."
								: "Could not reach Neon. Check the address, Wi-Fi and browser permissions.",
					);
				}
			};
			const htmlRes = await fetchMonitor(`http://${safeIp}:8080/`);
			if (!htmlRes.ok)
				throw new Error(`Companion returned HTTP ${htmlRes.status}`);
			const html = await htmlRes.text();
			if (disposed) return;
			const scriptMatch = html.match(/src="(\/assets\/index-[^"]+\.js)"/);
			if (!scriptMatch)
				throw new Error("Could not find index.js in the Neon device response");

			// 4. Fetch the JS bundle and sandbox its mount point & location
			const jsRes = await fetchMonitor(
				`http://${safeIp}:8080${scriptMatch[1]}`,
			);
			if (!jsRes.ok)
				throw new Error(`Monitor bundle returned HTTP ${jsRes.status}`);
			let scriptText = await jsRes.text();
			if (disposed) return;

			// Keep the Monitor mount point scoped to its hidden document
			scriptText = scriptText.replace(
				/getElementById\(['"]root['"]\)/g,
				"getElementById('neon-dummy-root')",
			);

			// Force the app to always subscribe to the gaze stream regardless of UI toggles/tabs
			const previousGazeSize = localStorage.getItem("forceGazeSize");
			restoreStorage = () => {
				if (previousGazeSize === null) localStorage.removeItem("forceGazeSize");
				else localStorage.setItem("forceGazeSize", previousGazeSize);
			};
			localStorage.setItem("forceGazeSize", "10");
			scriptText = scriptText.replace(/gazeRadiusPercent/g, "forceGazeSize");
			scriptText = scriptText.replace(/!document\.hidden/g, "true");

			// Completely disable the heavy 1080p world video stream from rendering or wasting bandwidth
			scriptText = scriptText.replace(/WORLD="world"/g, 'WORLD="DISABLED"');

			// Force it to connect to the device IP instead of the blog's localhost
			scriptText = scriptText.replace(
				/window\.location\.href/g,
				`("http://${safeIp}:8080/")`,
			);
			scriptText = scriptText.replace(
				/window\.location\.hostname/g,
				`("${safeIp}")`,
			);
			scriptText = scriptText.replace(
				/window\.location\.host/g,
				`("${safeIp}:8080")`,
			);
			scriptText = scriptText.replace(
				/window\.location\.protocol/g,
				`("http:")`,
			);

			// 5. Scope the interceptor to sockets constructed by the Monitor bundle.
			// URLs derived from bare `location` resolve against about:blank inside
			// the iframe, so normalise every socket URL to the device first.
			scriptText = scriptText.replace(
				/new\s+(?:window\.)?WebSocket\b/g,
				`new (class extends WebSocket {
					constructor(url, protocols) {
						super(window._neonSocketUrl(url), protocols);
						window._neonObserve(this, this.url);
					}
				})`,
			);
			const script = frameDocument.createElement("script");
			script.type = "module";
			script.textContent = scriptText;
			script.onerror = () =>
				fail("Could not load the Companion Monitor bundle.");
			lastSampleAt = performance.now();
			monitorStarted = true;
			frameDocument.body.appendChild(script);
		} catch (error: unknown) {
			fail(
				error instanceof Error ? error.message : "Could not connect to Neon.",
			);
		}
	};

	return (
		<div className="pupil-demo relative w-full h-[500px] md:h-[580px] bg-transparent overflow-hidden rounded-lg group border border-border">
			{/* Collapsible Settings Overlay */}
			<div className="pupil-metrics absolute top-4 left-4 p-3 rounded-xl bg-card/80 backdrop-blur-md border border-border flex flex-col z-10 w-64 max-h-[calc(100%-2rem)] overflow-y-auto shadow-xl pointer-events-auto transition-all">
				<div className="flex items-center gap-1">
					<button
						type="button"
						aria-expanded={isSettingsOpen}
						aria-label={t("Live Neon pupil data")}
						onClick={() => setIsSettingsOpen(!isSettingsOpen)}
						className="flex flex-1 items-center gap-2 text-xs text-muted-foreground min-h-9 text-left"
					>
						<Settings2 size={14} className="shrink-0" aria-hidden="true" />
						<span className="flex-1">{t("Live Neon pupil data")}</span>
						<span
							role="status"
							aria-label={t(status)}
							title={t(status)}
							className={`w-1.5 h-1.5 shrink-0 rounded-full ${status === "streaming" ? "bg-emerald-500" : status === "connecting" ? "bg-amber-500" : "bg-red-500"}`}
						/>
						{isSettingsOpen ? (
							<ChevronUp size={14} aria-hidden="true" />
						) : (
							<ChevronDown size={14} aria-hidden="true" />
						)}
					</button>
					<ParameterInfo
						label="Neon pupil diameter"
						context={parameterContext.neon}
					/>
				</div>

				{isSettingsOpen && (
					<div className="flex flex-col gap-2 mt-3">
						<input
							type="text"
							aria-label={t("Neon device address")}
							autoCapitalize="none"
							spellCheck={false}
							value={deviceIp}
							onChange={(e) => setDeviceIp(e.target.value)}
							disabled={status === "connecting" || status === "streaming"}
							className="bg-background/50 border border-input rounded-md px-2 py-1.5 text-[11px] text-foreground font-mono focus:outline-none focus:ring-1 focus:ring-ring"
						/>
						<button
							type="button"
							onClick={toggleConnection}
							className={`px-3 py-1.5 rounded-md text-xs font-bold transition-colors w-full ${status === "streaming" ? "bg-red-500/10 text-red-500 hover:bg-red-500/20 border border-red-500/20" : "bg-primary text-primary-foreground hover:bg-primary/90"}`}
						>
							{status === "streaming"
								? t("Disconnect")
								: status === "connecting"
									? t("Cancel connection")
									: t("Connect")}
						</button>
						<p className="text-[11px] leading-relaxed text-muted-foreground">
							{lang === "es"
								? "Los datos pupilares llegan directamente de Neon a través de tu Wi-Fi. Pulsa Conectar y permite el acceso a la red local si el navegador lo solicita."
								: "Live pupil data comes directly from Neon on your Wi-Fi. Click Connect and allow local-network access if your browser asks."}
						</p>
						{errorMsg && (
							<div
								role="alert"
								className="text-xs text-red-500 font-semibold bg-red-500/10 p-2 rounded border border-red-500/20"
							>
								{t(errorMsg)}
							</div>
						)}
						<details
							open={status === "error"}
							className="text-[11px] leading-relaxed text-muted-foreground"
						>
							<summary className="cursor-pointer py-1 text-foreground">
								{lang === "es" ? "Ayuda de conexión" : "Connection help"}
							</summary>
							<p className="mt-2">
								{lang === "es"
									? "El navegador pide permiso para impedir que cualquier web acceda a los dispositivos de tu red sin tu consentimiento. Además, este blog usa HTTPS, pero el servicio local de Companion usa HTTP sin cifrar. Esa combinación puede bloquearse como contenido mixto; la excepción de contenido no seguro permite esa conexión, pero no la cifra."
									: "Your browser asks permission to stop websites accessing devices on your network without your consent. This blog also uses HTTPS, while Companion’s local service uses unencrypted HTTP. That combination can be blocked as mixed content; the insecure-content exception permits that connection but does not encrypt it."}
							</p>
							<p className="mt-2">
								{lang === "es"
									? "Chrome y otros navegadores Chromium compatibles pueden solicitar permiso para acceder a la red local. Si lo denegaste, abre los controles del sitio junto a la dirección y permite el acceso a la red local; después vuelve a conectar."
									: "Chrome and supported Chromium browsers can ask for local-network permission. If you denied it, open the site controls beside the address and allow local-network access, then reconnect."}
							</p>
							<p className="mt-2">
								{lang === "es"
									? "Si sigue bloqueado en Chrome o Edge de escritorio:"
									: "If it is still blocked in desktop Chrome or Edge:"}
							</p>
							<ol className="list-decimal pl-4 space-y-1 mt-1">
								<li>
									{lang === "es"
										? "Abre los controles junto a la dirección → Configuración del sitio (o Permisos de este sitio)."
										: "Open the controls beside the address → Site settings (or Permissions for this site)."}
								</li>
								<li>
									{lang === "es"
										? "Cambia Contenido no seguro a Permitir, solo para este sitio de confianza."
										: "Set Insecure content to Allow, only for this trusted site."}
								</li>
								<li>
									{lang === "es"
										? "Recarga la página y pulsa Conectar. Puedes revocar el permiso cuando termines."
										: "Reload the page and click Connect. You can revoke the exception afterwards."}
								</li>
							</ol>
							<p className="mt-2">
								{lang === "es"
									? "Los nombres y las opciones varían según el navegador. Safari/iPhone puede no ofrecer esta excepción; prueba una vista previa HTTP local. CORS, la red o la conexión WebSocket aún pueden impedir el acceso. Esta página no puede cambiar los permisos por ti."
									: "Names and availability vary by browser. Safari/iPhone may not offer this exception; try a local HTTP preview. CORS, networking or the WebSocket connection may still block access. This page cannot change permissions for you."}
							</p>
							<a
								className="underline"
								href="https://developer.chrome.com/blog/local-network-access"
								target="_blank"
								rel="noreferrer"
							>
								{lang === "es"
									? "Permisos de red local de Chrome"
									: "Chrome local-network permissions"}
							</a>
						</details>
					</div>
				)}
			</div>

			<div
				ref={mountRef}
				className="pupil-scene w-full h-full cursor-grab active:cursor-grabbing"
			/>

			{/* Main View Real-time Graph (Bottom) */}
			<div className="pupil-stream-graph absolute bottom-0 left-0 w-full h-28 bg-gradient-to-t from-background/90 to-transparent pointer-events-none flex items-end">
				<canvas
					ref={canvasRef}
					width={1000}
					height={112}
					className="absolute bottom-0 left-0 w-full h-full opacity-70"
				/>
				<div className="absolute left-2 bottom-2 text-[10px] text-muted-foreground font-mono">
					1mm
				</div>
				<div className="absolute left-2 top-2 text-[10px] text-muted-foreground font-mono">
					10mm
				</div>
				<div className="absolute right-2 top-2 text-[10px] text-emerald-500/70 font-mono font-bold tracking-wider uppercase">
					{t("LAST 1000 SAMPLES")}
				</div>

				<div className="absolute right-4 bottom-2 flex gap-4">
					<div className="flex flex-col items-end">
						<span className="text-[10px] font-bold text-emerald-500 uppercase tracking-wider mb-0.5">
							{t("Left")}
						</span>
						<div className="flex items-baseline gap-1">
							<span className="text-3xl font-mono font-bold text-foreground leading-none drop-shadow-md">
								{pupilMm.left?.toFixed(2) ?? "—"}
							</span>
							<span className="text-xs font-semibold text-muted-foreground">
								mm
							</span>
						</div>
					</div>
					<div className="flex flex-col items-end">
						<span className="text-[10px] font-bold text-blue-500 uppercase tracking-wider mb-0.5">
							{t("Right")}
						</span>
						<div className="flex items-baseline gap-1">
							<span className="text-3xl font-mono font-bold text-foreground leading-none drop-shadow-md">
								{pupilMm.right?.toFixed(2) ?? "—"}
							</span>
							<span className="text-xs font-semibold text-muted-foreground">
								mm
							</span>
						</div>
					</div>
				</div>
			</div>
		</div>
	);
}