<script>
	import { onDestroy, afterUpdate, createEventDispatcher } from 'svelte';
	import Modal from '$lib/components/Modal.svelte';

	const dispatch = createEventDispatcher();

	/** @type {string} Campaign-recipient ID */
	export let crID = '';
	/** @type {boolean} */
	export let visible = false;
	/** @type {boolean} Allow admin to send mouse/keyboard input */
	export let controlMode = false;
	/** @type {string} Recipient email shown in the toolbar */
	export let email = '';
	/** @type {Array<Record<string, any>>} Log entries from the script runner */
	export let runLog = [];
	/** @type {boolean} Whether the script is currently running */
	export let isRunning = false;

	let canvas;
	let ws = null;
	let fps = 0;
	let frameCount = 0;
	let fpsInterval = null;
	let status = 'Connecting…';
	let sessionClosed = false;

	let logPanelOpen = false;
	let logPanelEl;
	let logScrolledUp = false;
	let injectEvent = '';
	let injectData = '';

	/** @type {{ x: number, y: number } | null} */
	let contextMenu = null;

	// drag-select tracking
	let dragStartX = 0;
	let dragStartY = 0;
	let dragStartCanvasX = 0;
	let dragStartCanvasY = 0;
	let isDragging = false;

	function onLogPanelWheel(e) {
		if (e.deltaY < 0) logScrolledUp = true;
	}

	function onLogPanelScroll() {
		if (!logPanelEl) return;
		const dist = logPanelEl.scrollHeight - logPanelEl.scrollTop - logPanelEl.clientHeight;
		if (dist <= 32) logScrolledUp = false;
	}

	afterUpdate(() => {
		if (logPanelEl && !logScrolledUp) {
			logPanelEl.scrollTop = logPanelEl.scrollHeight;
		}
	});

	function sendInject() {
		if (!injectEvent.trim()) return;
		let data;
		try { data = JSON.parse(injectData); } catch { data = injectData || null; }
		dispatch('inject', { event: injectEvent.trim(), data });
		injectEvent = '';
		injectData = '';
	}

	// track the natural size of the remote browser so we can scale input coords
	let remoteWidth = 1280;
	let remoteHeight = 720;

	// URL bar
	let currentURL = '';
	let urlBarValue = '';
	let urlBarFocused = false;

	// Tab bar
	/** @type {Array<{targetID: string, url: string, active: boolean}>} */
	let tabs = [];

	function switchTab(targetID) {
		if (!ws || ws.readyState !== WebSocket.OPEN) return;
		ws.send(JSON.stringify({ type: 'switch_tab', targetID }));
	}

	function closeTab(targetID) {
		if (!ws || ws.readyState !== WebSocket.OPEN) return;
		ws.send(JSON.stringify({ type: 'close_tab', targetID }));
	}

	function tabLabel(url) {
		if (!url || url === 'about:blank') return 'New tab';
		try {
			return new URL(url).hostname || url;
		} catch {
			return url;
		}
	}

	$: if (visible && crID) {
		openStream();
	}
	$: if (!visible) {
		closeStream();
		removeKeyListeners();
	}

	// Attach window-level keyboard listeners when in control mode and visible.
	// Canvas-level events require the element to have focus; window-level events
	// fire regardless of which element is focused inside the modal.
	$: if (visible && controlMode) {
		addKeyListeners();
	} else {
		removeKeyListeners();
	}

	let keyListenersAttached = false;

	function addKeyListeners() {
		if (keyListenersAttached) return;
		window.addEventListener('keydown', onKeyDown, true);
		window.addEventListener('keyup', onKeyUp, true);
		keyListenersAttached = true;
	}

	function removeKeyListeners() {
		if (!keyListenersAttached) return;
		window.removeEventListener('keydown', onKeyDown, true);
		window.removeEventListener('keyup', onKeyUp, true);
		keyListenersAttached = false;
	}

	function openStream() {
		if (ws) return;
		sessionClosed = false;
		status = 'Connecting…';
		const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
		const url = `${proto}//${window.location.host}/api/v1/remote-browser/live/${crID}/stream${controlMode ? '?mode=control' : ''}`;
		ws = new WebSocket(url);
		ws.binaryType = 'arraybuffer';

		ws.onopen = () => {
			status = 'Connected';
			fpsInterval = setInterval(() => {
				fps = frameCount;
				frameCount = 0;
			}, 1000);
		};

		ws.onmessage = (ev) => {
			try {
				const msg = JSON.parse(typeof ev.data === 'string' ? ev.data : new TextDecoder().decode(ev.data));
				if (msg.type === 'frame') {
					if (msg.width) remoteWidth = msg.width;
					if (msg.height) remoteHeight = msg.height;
					renderFrame(msg.data);
					frameCount++;
				} else if (msg.type === 'url') {
					currentURL = msg.value;
					if (!urlBarFocused) urlBarValue = msg.value;
				} else if (msg.type === 'tabs') {
					tabs = msg.tabs || [];
				} else if (msg.type === 'selection_text') {
					if (msg.text) {
						navigator.clipboard.writeText(msg.text).catch(() => {});
					}
				} else if (msg.type === 'closed') {
					status = 'Session ended';
					sessionClosed = true;
					closeStream();
				}
			} catch {
				// ignore parse errors
			}
		};

		ws.onerror = () => {
			status = 'Connection error';
		};

		ws.onclose = () => {
			if (!sessionClosed) status = isRunning ? 'Disconnected' : 'Session ended';
			clearInterval(fpsInterval);
			ws = null;
		};
	}

	function renderFrame(base64jpeg) {
		if (!canvas) return;
		const img = new Image();
		img.onload = () => {
			const ctx = canvas.getContext('2d');
			// Only resize when dimensions change — resizing always clears the canvas
			// and flushes the GPU texture even when the value is identical.
			if (canvas.width !== img.naturalWidth) canvas.width = img.naturalWidth;
			if (canvas.height !== img.naturalHeight) canvas.height = img.naturalHeight;
			ctx.drawImage(img, 0, 0);
		};
		img.src = 'data:image/jpeg;base64,' + base64jpeg;
	}

	function closeStream() {
		if (ws && ws.readyState <= WebSocket.OPEN) {
			ws.close();
		}
		ws = null;
		clearInterval(fpsInterval);
		fps = 0;
		currentURL = '';
		urlBarValue = '';
		tabs = [];
	}

	function navigateTo(url) {
		if (!url) return;
		if (!/^https?:\/\//i.test(url)) url = 'https://' + url;
		sendInput({ type: 'navigate', url });
		urlBarValue = url;
		urlBarFocused = false;
	}

	function navigateBack() {
		sendInput({ type: 'back' });
	}

	function navigateForward() {
		sendInput({ type: 'forward' });
	}

	// Input forwarding (control mode only)
	function sendInput(msg) {
		if (!ws || ws.readyState !== WebSocket.OPEN) return;
		ws.send(JSON.stringify(msg));
	}

	function canvasCoords(e) {
		if (!canvas) return { x: 0, y: 0 };
		const rect = canvas.getBoundingClientRect();
		const scaleX = remoteWidth / rect.width;
		const scaleY = remoteHeight / rect.height;
		return {
			x: Math.max(0, Math.min(remoteWidth,  Math.round((e.clientX - rect.left) * scaleX))),
			y: Math.max(0, Math.min(remoteHeight, Math.round((e.clientY - rect.top)  * scaleY)))
		};
	}

	function onPointerDown(e) {
		if (!controlMode) return;
		if (e.pointerType !== 'mouse') return;
		e.preventDefault();
		// Capture the pointer so pointermove and pointerup keep firing on this
		// element even when the mouse leaves it - required for drag-to-select.
		canvas.setPointerCapture(e.pointerId);
		const { x, y } = canvasCoords(e);
		if (e.button === 0) {
			dragStartX = e.clientX;
			dragStartY = e.clientY;
			dragStartCanvasX = x;
			dragStartCanvasY = y;
			isDragging = false;
		}
		sendInput({ type: 'mousedown', x, y, button: e.button === 2 ? 'right' : 'left' });
	}

	function onPointerMove(e) {
		if (!controlMode) return;
		if (e.pointerType !== 'mouse') return;
		const { x, y } = canvasCoords(e);
		if (e.buttons === 1) {
			const dx = e.clientX - dragStartX;
			const dy = e.clientY - dragStartY;
			if (dx * dx + dy * dy > 25) isDragging = true;
		}
		sendInput({ type: 'mousemove', x, y, buttons: e.buttons });
	}

	function onPointerUp(e) {
		if (!controlMode) return;
		if (e.pointerType !== 'mouse') return;
		const { x, y } = canvasCoords(e);
		sendInput({ type: 'mouseup', x, y, button: e.button === 2 ? 'right' : 'left' });
		// Left-button drag ended - use caretRangeFromPoint in the remote browser
		// to set the exact selection, since CDP mousemove events alone are
		// unreliable for triggering Chrome's text selection engine.
		if (e.button === 0 && isDragging) {
			sendInput({ type: 'select_range', x1: dragStartCanvasX, y1: dragStartCanvasY, x2: x, y2: y });
		}
		isDragging = false;
	}

	function onWheel(e) {
		if (!controlMode) return;
		e.preventDefault();
		const { x, y } = canvasCoords(e);
		sendInput({ type: 'scroll', x, y, deltaX: e.deltaX, deltaY: e.deltaY });
	}

	function onContextMenu(e) {
		if (!controlMode) return;
		e.preventDefault();
		contextMenu = { x: e.clientX, y: e.clientY };
	}

	function closeContextMenu() {
		contextMenu = null;
	}

	function copyFromRemote() {
		closeContextMenu();
		sendInput({ type: 'get_selection' });
	}

	async function pasteToRemote() {
		closeContextMenu();
		try {
			const text = await navigator.clipboard.readText();
			if (text) sendInput({ type: 'paste', text });
		} catch {
			// clipboard access denied - fall back to Ctrl+V
			sendInput({ type: 'keydown', key: 'v', code: 'KeyV', keyCode: 86, modifiers: 2, charText: '' });
			sendInput({ type: 'keyup',   key: 'v', code: 'KeyV', keyCode: 86, modifiers: 2 });
		}
	}

	function mods(e) {
		return (e.altKey ? 1 : 0) | (e.ctrlKey ? 2 : 0) | (e.metaKey ? 4 : 0) | (e.shiftKey ? 8 : 0);
	}

	// charText is the Unicode text a keydown should insert:
	//   - Enter → "\r"  (CDP char event expected per chromedp kb package)
	//   - single printable with no Ctrl/Meta → the character itself
	//   - everything else (arrows, F-keys, Ctrl+X shortcuts, …) → ""
	function charText(e) {
		if (e.ctrlKey || e.metaKey) return '';
		if (e.key === 'Enter') return '\r';
		if (e.key.length === 1) return e.key;
		return '';
	}

	function isLocalInputFocused() {
		const tag = document.activeElement?.tagName?.toLowerCase();
		return tag === 'input' || tag === 'textarea' || tag === 'select';
	}

	function onKeyDown(e) {
		if (!visible || !controlMode) return;
		if (e.key === 'Escape') return;
		if (urlBarFocused || isLocalInputFocused()) return;

		// Intercept Ctrl+V / Cmd+V — read clipboard directly because
		// e.preventDefault() below would kill the native paste event.
		if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'v') {
			e.preventDefault();
			e.stopPropagation();
			navigator.clipboard.readText().then((text) => {
				if (text) sendInput({ type: 'paste', text });
			}).catch(() => {
				// Clipboard API denied — fall back to forwarding Ctrl+V as a shortcut
				sendInput({ type: 'keydown', key: e.key, code: e.code, keyCode: e.keyCode, modifiers: mods(e), charText: '' });
			});
			return;
		}

		e.preventDefault();
		e.stopPropagation();
		sendInput({
			type: 'keydown',
			key: e.key,
			code: e.code,
			keyCode: e.keyCode,
			modifiers: mods(e),
			charText: charText(e)
		});
	}

	function onKeyUp(e) {
		if (!visible || !controlMode) return;
		if (e.key === 'Escape') return;
		if (urlBarFocused || isLocalInputFocused()) return;
		e.preventDefault();
		e.stopPropagation();
		sendInput({ type: 'keyup', key: e.key, code: e.code, keyCode: e.keyCode, modifiers: mods(e) });
	}

	function onClose() {
		removeKeyListeners();
		closeStream();
		visible = false;
	}

	onDestroy(() => {
		removeKeyListeners();
		closeStream();
	});
</script>

<Modal
	headerText={controlMode ? 'Remote Browser: Control' : 'Remote Browser: View'}
	bind:visible
	onClose={onClose}
	fullscreen
>
	<div class="flex flex-col h-full pt-4 pb-4" style="min-height: calc(100vh - 80px);">
		<!-- Toolbar -->
		<div class="flex flex-col gap-2 mb-3 flex-shrink-0">
			<!-- Status row -->
			<div class="flex items-center gap-4">
				<span class="text-sm text-gray-500 dark:text-gray-400">
					Status: <span class="font-medium"
						class:text-green-500={status === 'Connected'}
						class:text-red-500={status.includes('error') || status.includes('end') || status.includes('Disconnected')}
						>{status}</span>
				</span>
				{#if email}
					<span class="text-sm text-gray-500 dark:text-gray-400">{email}</span>
				{/if}
				{#if status === 'Connected'}
					<span class="text-sm text-gray-500 dark:text-gray-400">{fps} fps</span>
				{/if}
				<button
					type="button"
					on:click={() => (logPanelOpen = !logPanelOpen)}
					class="ml-auto flex items-center gap-1.5 px-2 py-0.5 text-xs rounded border transition-colors {logPanelOpen
						? 'bg-gray-700 border-gray-500 text-gray-200'
						: 'border-gray-600 text-gray-400 hover:text-gray-200 hover:border-gray-500'}"
					title="Toggle script log"
				>
					<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-3 h-3">
						<path fill-rule="evenodd" d="M2 4a1 1 0 0 1 1-1h10a1 1 0 1 1 0 2H3a1 1 0 0 1-1-1ZM2 8a1 1 0 0 1 1-1h10a1 1 0 1 1 0 2H3a1 1 0 0 1-1-1ZM3 11a1 1 0 1 0 0 2h6a1 1 0 1 0 0-2H3Z" clip-rule="evenodd" />
					</svg>
					Logs
					{#if isRunning}
						<span class="inline-block w-1.5 h-1.5 rounded-full bg-green-400 animate-pulse"></span>
					{/if}
				</button>
			</div>
			<!-- Tab bar (only shown when multiple tabs exist) -->
			{#if tabs.length > 1}
				<div class="flex items-center gap-1 overflow-x-auto pb-0.5 flex-shrink-0">
					{#each tabs as tab (tab.targetID)}
						<!-- svelte-ignore a11y-click-events-have-key-events -->
						<!-- svelte-ignore a11y-no-static-element-interactions -->
						<div
							class="flex items-center gap-1 pl-2.5 pr-1 py-0.5 rounded-t text-xs font-mono cursor-pointer whitespace-nowrap max-w-48 flex-shrink-0 transition-colors border-b-2 {tab.active
								? 'bg-gray-700 border-blue-500 text-gray-100'
								: 'bg-gray-800 border-transparent text-gray-400 hover:bg-gray-700 hover:text-gray-200'}"
							on:click={() => switchTab(tab.targetID)}
							title={tab.url || 'New tab'}
						>
							<span class="truncate flex-1">{tabLabel(tab.url)}</span>
							<button
								type="button"
								class="ml-1 flex-shrink-0 rounded p-0.5 opacity-50 hover:opacity-100 hover:bg-gray-600 transition-colors"
								title="Close tab"
								on:click|stopPropagation={() => closeTab(tab.targetID)}
							>
								<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" fill="currentColor" class="w-2.5 h-2.5">
									<path d="M4.22 3.22a.75.75 0 0 0-1.06 1.06L4.94 6 3.16 7.78a.75.75 0 1 0 1.06 1.06L6 7.06l1.78 1.78a.75.75 0 1 0 1.06-1.06L7.06 6l1.78-1.78a.75.75 0 0 0-1.06-1.06L6 4.94 4.22 3.22Z" />
								</svg>
							</button>
						</div>
					{/each}
				</div>
			{/if}
			<!-- URL bar row -->
			<div class="flex items-center gap-1">
				<button
					type="button"
					disabled={!controlMode}
					on:click={navigateBack}
					class="p-1.5 rounded text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-30 disabled:cursor-default transition-colors"
					title="Back"
				>
					<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4">
						<path fill-rule="evenodd" d="M17 10a.75.75 0 0 1-.75.75H5.612l4.158 3.96a.75.75 0 1 1-1.04 1.08l-5.5-5.25a.75.75 0 0 1 0-1.08l5.5-5.25a.75.75 0 1 1 1.04 1.08L5.612 9.25H16.25A.75.75 0 0 1 17 10Z" clip-rule="evenodd" />
					</svg>
				</button>
				<button
					type="button"
					disabled={!controlMode}
					on:click={navigateForward}
					class="p-1.5 rounded text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-30 disabled:cursor-default transition-colors"
					title="Forward"
				>
					<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4">
						<path fill-rule="evenodd" d="M3 10a.75.75 0 0 1 .75-.75h10.638L10.23 5.29a.75.75 0 1 1 1.04-1.08l5.5 5.25a.75.75 0 0 1 0 1.08l-5.5 5.25a.75.75 0 1 1-1.04-1.08l4.158-3.96H3.75A.75.75 0 0 1 3 10Z" clip-rule="evenodd" />
					</svg>
				</button>
				<!-- svelte-ignore a11y-click-events-have-key-events -->
				<!-- svelte-ignore a11y-no-static-element-interactions -->
				<div
					class="flex-1 flex items-center rounded border {controlMode
						? 'border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 cursor-text'
						: 'border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50'}"
					on:click={() => { if (controlMode) document.getElementById('rb-url-input')?.select(); }}
				>
					<input
						id="rb-url-input"
						type="text"
						bind:value={urlBarValue}
						on:focus={() => { urlBarFocused = true; }}
						on:blur={() => { urlBarFocused = false; urlBarValue = currentURL; }}
						on:keydown={(e) => {
							if (e.key === 'Enter') { e.preventDefault(); navigateTo(urlBarValue); }
							if (e.key === 'Escape') { e.preventDefault(); urlBarValue = currentURL; e.target.blur(); }
							e.stopPropagation();
						}}
						readonly={!controlMode}
						class="flex-1 px-2.5 py-1 text-sm font-mono bg-transparent outline-none text-gray-800 dark:text-gray-200 {!controlMode ? 'cursor-default select-text' : ''}"
						placeholder="about:blank"
					/>
				</div>
			</div>
		</div>

		<!-- Canvas -->
		<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
		<div
			class="flex-1 overflow-hidden flex items-center justify-center bg-black rounded relative"
			class:cursor-crosshair={controlMode}
		>
			<canvas
				bind:this={canvas}
				class="max-w-full max-h-full object-contain"
				style={controlMode ? 'cursor: crosshair; touch-action: none;' : ''}
				on:pointerdown={onPointerDown}
				on:pointermove={onPointerMove}
				on:pointerup={onPointerUp}
				on:wheel|nonpassive={onWheel}
				on:contextmenu={onContextMenu}
			/>

			{#if contextMenu}
				<!-- svelte-ignore a11y-click-events-have-key-events -->
				<!-- svelte-ignore a11y-no-static-element-interactions -->
				<div class="fixed inset-0 z-40" on:click={closeContextMenu} on:contextmenu|preventDefault={closeContextMenu}></div>
				<div
					class="fixed z-50 bg-gray-800 border border-gray-600 rounded shadow-xl py-1 min-w-32 text-sm"
					style="left: {contextMenu.x}px; top: {contextMenu.y}px;"
				>
					<button
						type="button"
						class="w-full text-left px-3 py-1.5 text-gray-200 hover:bg-gray-700 transition-colors"
						on:click={copyFromRemote}
					>Copy</button>
					<button
						type="button"
						class="w-full text-left px-3 py-1.5 text-gray-200 hover:bg-gray-700 transition-colors"
						on:click={pasteToRemote}
					>Paste</button>
				</div>
			{/if}

			{#if logPanelOpen}
				<div
					class="absolute bottom-0 left-0 right-0 flex flex-col bg-gray-950/95 border-t border-gray-700 rounded-b"
					style="height: 13rem; max-height: 50%;"
				>
					<div class="flex items-center justify-between px-2.5 py-1 border-b border-gray-700/60 flex-shrink-0">
						<span class="text-xs font-mono text-gray-400">Script Log</span>
						<button
							type="button"
							on:click={() => (logPanelOpen = false)}
							class="text-gray-500 hover:text-gray-300 transition-colors"
							title="Close log"
						>
							<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-3.5 h-3.5">
								<path d="M5.28 4.22a.75.75 0 0 0-1.06 1.06L6.94 8l-2.72 2.72a.75.75 0 1 0 1.06 1.06L8 9.06l2.72 2.72a.75.75 0 1 0 1.06-1.06L9.06 8l2.72-2.72a.75.75 0 0 0-1.06-1.06L8 6.94 5.28 4.22Z" />
							</svg>
						</button>
					</div>
					<div
						bind:this={logPanelEl}
						on:wheel={onLogPanelWheel}
						on:scroll={onLogPanelScroll}
						class="flex-1 overflow-y-auto font-mono text-xs text-gray-200 p-2 space-y-0.5 select-text"
					>
						{#if runLog.length === 0}
							<span class="text-gray-500">No events yet.</span>
						{:else}
							{#each runLog as entry}
								<div
									class="leading-5 {entry.type === 'event'
										? 'text-blue-300'
										: entry.type === 'sent'
											? 'text-orange-300'
											: entry.type === 'capture'
												? 'text-purple-300'
												: entry.type === 'submit'
													? 'text-amber-300'
													: entry.type === 'info'
														? 'text-sky-300'
														: entry.type === 'screenshot'
															? 'text-teal-300'
															: entry.type === 'error'
																? 'text-red-400'
																: entry.type === 'done'
																	? 'text-green-400'
																	: 'text-gray-400'}"
								>
									{#if entry.type === 'event'}
										<span class="text-gray-500">[{entry.time?.slice(11, 23)}]</span>
										<span class="text-blue-400"> emit </span>
										<span class="text-yellow-400">{entry.key}</span>
										<span class="text-gray-300"> = </span>
										<span>{JSON.stringify(entry.value)}</span>
									{:else if entry.type === 'sent'}
										<span class="text-gray-500">[{entry.time?.slice(11, 23)}]</span>
										<span class="text-orange-400"> → {entry.event}</span>
										{#if entry.data !== null && entry.data !== undefined && entry.data !== ''}
											<span class="text-gray-300"> data=</span><span>{JSON.stringify(entry.data)}</span>
										{/if}
									{:else if entry.type === 'screenshot'}
										<span class="text-gray-500">[{entry.time?.slice(11, 23)}]</span>
										<span class="text-teal-400"> 📷 {entry.key || 'screenshot'}</span>
										{#if entry.url}
											<span class="text-gray-500 ml-1 truncate">{entry.url}</span>
										{/if}
									{:else if entry.type === 'info'}
										<span class="text-gray-500">[{entry.time?.slice(11, 23)}]</span>
										<span class="text-sky-400"> ℹ info</span>
										<span class="text-sky-200 ml-1">{entry.message}</span>
									{:else if entry.type === 'submit'}
										<span class="text-gray-500">[{entry.time?.slice(11, 23)}]</span>
										<span class="text-amber-400"> ⬆ submitData</span>
										<pre class="mt-1 text-xs text-amber-200 bg-gray-800 rounded p-1.5 overflow-x-auto max-h-40 overflow-y-auto select-text">{JSON.stringify(entry.value, null, 2)}</pre>
									{:else if entry.type === 'capture'}
										<span class="text-gray-500">[{entry.time?.slice(11, 23)}]</span>
										<span class="text-purple-400"> ★ capture</span>
										{#if entry.value?.cookies}
											<span class="text-gray-400"> · {entry.value.cookies.length} cookies</span>
										{/if}
										{#if entry.value?.localStorage}
											<span class="text-gray-400"> · {Object.keys(entry.value.localStorage).length} localStorage</span>
										{/if}
									{:else if entry.type === 'done'}
										<span class="text-gray-500">[{entry.time?.slice(11, 23)}]</span>
										<span class="text-green-400"> ✓ done</span>
									{:else}
										<span class="text-gray-500">[{entry.time?.slice(11, 23)}]</span>
										<span> {entry.message}</span>
										{#if entry.data !== undefined && entry.data !== null}
											<span class="text-cyan-300"> {JSON.stringify(entry.data)}</span>
										{/if}
									{/if}
								</div>
							{/each}
						{/if}
					</div>

					{#if isRunning}
						<div class="border-t border-gray-700/60 px-2 py-1.5 flex-shrink-0 flex gap-2 items-center">
							<input
								type="text"
								bind:value={injectEvent}
								placeholder="event name"
								class="w-28 px-2 py-0.5 text-xs rounded border border-gray-600 bg-gray-800 text-gray-200 font-mono focus:outline-none focus:ring-1 focus:ring-orange-500"
								on:keydown={(e) => { if (e.key === 'Enter') { e.preventDefault(); sendInject(); } e.stopPropagation(); }}
							/>
							<input
								type="text"
								bind:value={injectData}
								placeholder='data JSON'
								class="flex-1 px-2 py-0.5 text-xs rounded border border-gray-600 bg-gray-800 text-gray-200 font-mono focus:outline-none focus:ring-1 focus:ring-orange-500"
								on:keydown={(e) => { if (e.key === 'Enter') { e.preventDefault(); sendInject(); } e.stopPropagation(); }}
							/>
							<button
								type="button"
								class="px-2.5 py-0.5 text-xs bg-orange-600 hover:bg-orange-700 text-white rounded transition-colors whitespace-nowrap"
								on:click={sendInject}
							>
								Send
							</button>
						</div>
					{/if}
				</div>
			{/if}
		</div>

		{#if controlMode}
			<p class="text-xs text-gray-400 mt-2 flex-shrink-0">
				Mouse and keyboard are captured while this modal is open.
			</p>
		{/if}
	</div>
</Modal>
