<script lang="ts">
	import { onMount, onDestroy, tick, createEventDispatcher } from 'svelte';
	import { api, wsClient, type StreamEvent, type TerminalDiff } from '$lib/api';
	import AnsiToHtml from 'ansi-to-html';

	export let sessionId: string;
	export let autoScroll: boolean = true;
	export let autoFocus: boolean = true;
	export let lastSessionId: string | null = null;

	const dispatch = createEventDispatcher<{ aliveChange: boolean; goToLastSession: void }>();

	// ANSI to HTML converter for terminal colors
	const ansiConverter = new AnsiToHtml({
		fg: '#22c55e', // green-500 default foreground
		bg: 'transparent',
		newline: false,
		escapeXML: true
	});

	// Internal state: store content as array of lines for efficient diffing
	let terminalLines: string[] = [];
	let isAlive = false;
	let loading = true;
	let error = '';
	let terminalElement: HTMLPreElement;
	let terminalInput: HTMLInputElement;
	let refreshInterval: ReturnType<typeof setInterval> | null = null;
	let unsubscribe: (() => void) | null = null;
	let ctrlMode = false;
	let lastHash: number | null = null;
	let lastFrameId: number | null = null; // Track frame ordering to prevent out-of-order updates
	let initialLoadComplete = false; // Track whether initial load has happened
	let screenMode: 'fullscreen' | 'desktop' | 'landscape' | 'portrait' | null = null;
	let resizing = false;
	let inputBuffer = '';
	let batchTimeout: ReturnType<typeof setTimeout> | null = null;
	let isSending = false;
	let fontScale = 1; // 50% to 150% in 5% increments
	const fontScales = [0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 1, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5];

	// Consistent button styling for quick action bar
	const btnBase = 'px-2.5 py-1.5 rounded-sm text-sm font-mono transition-colors disabled:opacity-50';
	const btnDefault = `${btnBase} bg-zinc-700 hover:bg-zinc-600 text-zinc-200`;
	const btnRed = `${btnBase} bg-red-600/80 hover:bg-red-600 text-white`;
	const btnGreen = `${btnBase} bg-green-600/80 hover:bg-green-600 text-white`;
	const btnAmber = `${btnBase} bg-amber-600/80 hover:bg-amber-600 text-white`;
	const btnCyan = `${btnBase} bg-cyan-600/80 hover:bg-cyan-600 text-white`;
	const btnViolet = `${btnBase} bg-violet-600/80 hover:bg-violet-600 text-white`;

	function zoomIn() {
		const idx = fontScales.indexOf(fontScale);
		if (idx < fontScales.length - 1) {
			fontScale = fontScales[idx + 1];
		}
	}

	function zoomOut() {
		const idx = fontScales.indexOf(fontScale);
		if (idx > 0) {
			fontScale = fontScales[idx - 1];
		}
	}

	// Big mode: desktop sizing (120x36) with 70% zoom for mobile users who want more content
	export function setBigMode(enabled: boolean) {
		if (enabled) {
			fontScale = 0.7;
			resizeScreen('desktop');
		}
	}

	// Computed content from lines
	$: terminalContent = terminalLines.join('\n');

	// Convert ANSI codes to HTML for rendering
	$: terminalHtml = terminalContent ? ansiConverter.toHtml(terminalContent) : '';

	/**
	 * Check if a frame should be applied based on frame ID ordering.
	 * Handles wrap-around: if new ID is much smaller than current, it's likely a wrap.
	 * Threshold for wrap detection: if difference > MAX_SAFE_INTEGER / 2
	 */
	function shouldApplyFrame(newFrameId: number | undefined): boolean {
		if (newFrameId === undefined || newFrameId === null) return true; // No frame ID, always apply
		if (lastFrameId === null) return true; // First frame, always apply

		// Handle wrap-around: if new ID is much smaller, it probably wrapped
		const wrapThreshold = Number.MAX_SAFE_INTEGER / 2;
		if (lastFrameId > wrapThreshold && newFrameId < wrapThreshold / 2) {
			// Likely a wrap-around, accept the new frame
			return true;
		}

		// Normal case: only apply if frame ID is newer (greater)
		return newFrameId > lastFrameId;
	}

	/**
	 * Apply a terminal diff to the current lines array.
	 * Handles full refresh, partial updates, and unchanged states.
	 * Checks frame ID to prevent out-of-order updates.
	 */
	function applyDiff(diff: TerminalDiff): boolean {
		if (!diff) return false;

		const frameId = diff['frame-id'] ?? diff.frameId;
		const totalLines = diff['total-lines'] ?? diff.totalLines ?? 0;

		// Check frame ordering (skip out-of-order frames)
		if (!shouldApplyFrame(frameId)) {
			console.debug('[Terminal] Skipping out-of-order frame:', frameId, 'last:', lastFrameId);
			return false;
		}

		switch (diff.type) {
			case 'unchanged':
				// Content hasn't changed
				lastHash = diff.hash ?? null;
				if (frameId !== undefined) lastFrameId = frameId;
				return false;

			case 'full':
				// Full refresh - replace all lines
				terminalLines = diff.lines ?? [];
				lastHash = diff.hash ?? null;
				if (frameId !== undefined) lastFrameId = frameId;
				return true;

			case 'diff':
				// Partial update - apply changes to specific lines
				if (diff.changes) {
					// Create a working copy to avoid in-place mutation issues
					let newLines = [...terminalLines];
					// Ensure array is long enough
					while (newLines.length < totalLines) {
						newLines.push('');
					}
					// Truncate if needed
					if (newLines.length > totalLines) {
						newLines = newLines.slice(0, totalLines);
					}
					// Apply changes
					for (const [lineNumStr, content] of Object.entries(diff.changes)) {
						const lineNum = parseInt(lineNumStr, 10);
						if (lineNum >= 0 && lineNum < totalLines) {
							newLines[lineNum] = content ?? '';
						}
					}
					// Assign new array reference to trigger reactivity
					terminalLines = newLines;
				}
				lastHash = diff.hash ?? null;
				if (frameId !== undefined) lastFrameId = frameId;
				return true;

			default:
				return false;
		}
	}

	async function fetchTerminalContent(fresh: boolean = false) {
		try {
			const result = await api.getTerminalContent(sessionId, fresh);

			let changed = false;

			// On initial load, always use full content directly for reliability
			// Use diffs only for incremental updates after initial load
			if (!initialLoadComplete) {
				// First load: use raw content, ignore diff
				const newLines = result.content ? result.content.split('\n') : [];
				terminalLines = newLines;
				lastHash = result.diff?.hash ?? null;
				// Initialize frame ID tracking from first response
				lastFrameId = result.diff?.['frame-id'] ?? result.diff?.frameId ?? null;
				changed = newLines.length > 0;
				initialLoadComplete = true;
				// Set screen mode from server-detected layout
				if (result.layout) {
					screenMode = result.layout;
				}
			} else if (result.diff) {
				// Subsequent loads: apply diff for efficiency
				changed = applyDiff(result.diff);
			} else if (result.content !== undefined) {
				// Fallback: full content replacement
				const newLines = result.content ? result.content.split('\n') : [];
				if (newLines.join('\n') !== terminalLines.join('\n')) {
					terminalLines = newLines;
					changed = true;
				}
			}

			if (isAlive !== result.alive) {
				isAlive = result.alive;
				dispatch('aliveChange', isAlive);
			}
			error = '';

			if (changed) {
				await tick();
				scrollToBottom();
			}
		} catch (e) {
			error = e instanceof Error ? e.message : 'Failed to fetch terminal content';
		} finally {
			loading = false;
		}
	}

	function scrollToBottom(force = false) {
		if ((force || autoScroll) && terminalElement) {
			terminalElement.scrollTop = terminalElement.scrollHeight;
		}
	}

	async function pasteFromClipboard() {
		try {
			const text = await navigator.clipboard.readText();
			if (text) {
				await sendInput(text);
			}
		} catch (e) {
			console.error('Failed to read clipboard:', e);
		}
	}

	function copySelection() {
		const selection = window.getSelection();
		const text = selection?.toString();
		if (text) {
			navigator.clipboard.writeText(text).catch(e => {
				console.error('Failed to copy:', e);
			});
		}
	}

	async function handleKeydown(event: KeyboardEvent) {
		// Ctrl+Down scrolls to bottom (don't send to tmux)
		if (event.ctrlKey && event.key === 'ArrowDown') {
			event.preventDefault();
			scrollToBottom(true);
			return;
		}

		// Ctrl+Shift+V pastes from clipboard
		if (event.ctrlKey && event.shiftKey && event.key === 'V') {
			event.preventDefault();
			await pasteFromClipboard();
			return;
		}

		if (!isAlive) return;

		// Prevent default for all keys we handle
		event.preventDefault();

		if (event.key === 'Enter' && event.shiftKey) {
			// Shift+Enter sends literal newline
			await sendInput('\n');
		} else if (event.key === 'Enter') {
			// Enter sends Enter key
			await sendInput('\r');
		} else if (event.key === 'Backspace') {
			await sendInput('\x7f');
		} else if (event.key === 'Tab' && event.shiftKey) {
			// Shift+Tab escape sequence
			await sendInput('\x1b[Z');
		} else if (event.key === 'Tab') {
			await sendInput('\t');
		} else if (event.key === 'Escape') {
			await sendInput('\x1b');
		} else if (event.key === 'ArrowUp') {
			await sendInput('\x1b[A');
		} else if (event.key === 'ArrowDown') {
			await sendInput('\x1b[B');
		} else if (event.key === 'ArrowRight') {
			await sendInput('\x1b[C');
		} else if (event.key === 'ArrowLeft') {
			await sendInput('\x1b[D');
		} else if (event.ctrlKey && event.key.length === 1) {
			// Ctrl+letter = control character
			const code = event.key.toLowerCase().charCodeAt(0) - 96;
			if (code > 0 && code < 27) {
				await sendInput(String.fromCharCode(code));
			}
		} else if (ctrlMode && event.key.length === 1 && /[a-z]/i.test(event.key)) {
			// Ctrl mode toggle + letter = control character
			const code = event.key.toLowerCase().charCodeAt(0) - 96;
			if (code > 0 && code < 27) {
				await sendInput(String.fromCharCode(code));
				ctrlMode = false;
			}
		} else if (event.key.length === 1) {
			// Regular printable character
			await sendInput(event.key);
		}
	}

	async function sendInput(input: string) {
		// Scroll to bottom when sending input (force regardless of autoScroll)
		scrollToBottom(true);

		// Buffer the input for batching
		inputBuffer += input;

		// If already scheduled to send, let it pick up the new input
		if (batchTimeout) return;

		// Schedule a batch send after a short delay to collect rapid keystrokes
		batchTimeout = setTimeout(flushInputBuffer, 30);
	}

	async function flushInputBuffer() {
		batchTimeout = null;

		// Skip if nothing to send or already sending
		if (!inputBuffer || isSending) return;

		const toSend = inputBuffer;
		inputBuffer = '';
		isSending = true;

		try {
			await api.sendTerminalInput(sessionId, toSend);
			// Fetch update shortly after
			setTimeout(() => fetchTerminalContent(false), 100);
		} catch (e) {
			error = e instanceof Error ? e.message : 'Failed to send input';
		} finally {
			isSending = false;
			// If more input accumulated while sending, flush it
			if (inputBuffer) {
				batchTimeout = setTimeout(flushInputBuffer, 30);
			}
		}
	}

	async function sendCtrlC() {
		await sendInput('\x03');
	}

	async function sendKey(key: string) {
		await sendInput(key);
	}

	async function resizeScreen(mode: 'fullscreen' | 'desktop' | 'landscape' | 'portrait') {
		if (resizing) return;
		resizing = true;
		try {
			await api.resizeTerminal(sessionId, mode);
			screenMode = mode;
			// Invalidate terminal cache and fetch fresh content after resize
			setTimeout(() => fetchTerminalContent(true), 150);
		} catch (e) {
			console.error('Resize failed:', e);
			error = e instanceof Error ? e.message : 'Failed to resize terminal';
		} finally {
			resizing = false;
		}
	}

	function handleWebSocketEvent(event: StreamEvent) {
		if (event.event === 'terminal-update') {
			// Apply diff if provided (includes frame ID ordering check)
			if (event.diff) {
				const changed = applyDiff(event.diff);
				if (changed) {
					tick().then(() => scrollToBottom());
				}
			} else if (event.content !== undefined) {
				// Fallback: full content replacement (no frame ID available)
				const newContent = event.content as string;
				const newLines = newContent ? newContent.split('\n') : [];
				if (newLines.join('\n') !== terminalLines.join('\n')) {
					terminalLines = newLines;
					tick().then(() => scrollToBottom());
				}
			}
		}
	}

	function handleOrientationChange() {
		// Only auto-switch on mobile (screen width < 640px or height < 450px)
		const isMobile = window.innerWidth < 640 || window.innerHeight < 450;
		if (!isMobile) return;

		const orientation = screen.orientation?.type || '';
		if (orientation.includes('portrait')) {
			resizeScreen('portrait');
		} else if (orientation.includes('landscape')) {
			resizeScreen('landscape');
		}
	}

	onMount(async () => {
		// Initial fetch with fresh=true to ensure we get full current content
		await fetchTerminalContent(true);

		// Subscribe to WebSocket updates
		await wsClient.connect();
		unsubscribe = wsClient.subscribe(sessionId, handleWebSocketEvent);

		// Periodic refresh every 1 second (no fresh flag for incremental diffs)
		refreshInterval = setInterval(() => fetchTerminalContent(false), 1000);

		// Auto-focus input after content loads
		if (autoFocus) {
			setTimeout(() => terminalInput?.focus(), 100);
		}

		// Listen for orientation changes on mobile
		if (screen.orientation) {
			screen.orientation.addEventListener('change', handleOrientationChange);
		}
	});

	onDestroy(() => {
		if (refreshInterval) {
			clearInterval(refreshInterval);
		}
		if (unsubscribe) {
			unsubscribe();
		}
		if (batchTimeout) {
			clearTimeout(batchTimeout);
		}
		if (screen.orientation) {
			screen.orientation.removeEventListener('change', handleOrientationChange);
		}
	});
</script>

<div class="flex flex-col flex-1 min-h-0 bg-black">
	{#if loading}
		<div class="flex-1 flex items-center justify-center">
			<svg class="animate-spin h-8 w-8 text-cyan-500" fill="none" viewBox="0 0 24 24">
				<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
				<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
			</svg>
		</div>
	{:else if error}
		<div class="flex-1 flex items-center justify-center p-4">
			<div class="text-red-400">{error}</div>
		</div>
	{:else}
		<!-- Terminal output area -->
		<!-- svelte-ignore a11y-click-events-have-key-events -->
		<!-- svelte-ignore a11y-no-static-element-interactions -->
		<pre
			bind:this={terminalElement}
			on:mouseup={copySelection}
			on:click={() => terminalInput?.focus()}
			class="flex-1 min-h-0 overflow-auto p-3 font-mono text-green-400 whitespace-pre-wrap break-words leading-relaxed terminal-content cursor-text"
			style="font-size: {fontScale * 0.875}rem;"
		>{@html terminalHtml}</pre>

		<!-- Quick action buttons -->
		<div class="flex-shrink-0 px-2 py-1.5 bg-zinc-900 border-t border-zinc-800 flex items-center gap-1.5 overflow-x-auto">
			<button
				on:click={() => ctrlMode = !ctrlMode}
				disabled={!isAlive}
				class="portrait-hide {btnBase} bg-zinc-700 hover:bg-zinc-600 text-zinc-200 {ctrlMode ? 'font-bold ring-1 ring-cyan-400 rounded' : 'rounded-sm'}"
			>^</button>
			<button
				on:click={sendCtrlC}
				disabled={!isAlive}
				class={btnRed}
			>^C</button>
			<button
				on:click={() => sendInput('\x04')}
				disabled={!isAlive}
				class={btnAmber}
			>^D</button>
			<span class="w-px h-6 bg-zinc-700 portrait-hide"></span>
			<button
				on:click={() => sendKey('y')}
				disabled={!isAlive}
				class="portrait-hide {btnGreen}"
			>y</button>
			<button
				on:click={() => sendKey('n')}
				disabled={!isAlive}
				class="portrait-hide {btnRed}"
			>n</button>
			<span class="w-px h-6 bg-zinc-700 portrait-hide"></span>
			{#each ['1', '2', '3', '4'] as num}
				<button
					on:click={() => sendKey(num)}
					disabled={!isAlive}
					class="portrait-hide {btnDefault}"
				>{num}</button>
			{/each}
			<span class="w-px h-6 bg-zinc-700"></span>
			<button
				on:click={() => sendInput('\t')}
				disabled={!isAlive}
				class={btnCyan}
			>⇥</button>
			<button
				on:click={() => sendInput('\x1b[Z')}
				disabled={!isAlive}
				class={btnCyan}
			>⇤</button>
			<button
				on:click={() => sendInput('\r')}
				disabled={!isAlive}
				class={btnGreen}
			>↵</button>
			<span class="w-px h-6 bg-zinc-700 portrait-hide"></span>
			<button
				on:click={pasteFromClipboard}
				disabled={!isAlive}
				class="portrait-hide {btnViolet}"
				title="Paste (Ctrl+Shift+V)"
			>📋</button>
			<!-- Screen size selector (pushed right on wider screens) -->
			<span class="w-px h-6 bg-zinc-700 ml-auto"></span>
			<!-- Text zoom (hidden on mobile portrait, visible on landscape/desktop) -->
			<div class="hidden landscape:flex sm:flex items-center gap-1.5">
				<button
					on:click={zoomOut}
					disabled={fontScale <= fontScales[0]}
					class={btnDefault}
					title="Zoom out"
				>-</button>
				<button
					on:click={() => fontScale = 1}
					class="text-xs text-zinc-400 hover:text-zinc-200 font-mono w-8 text-center transition-colors"
					title="Reset to 100%"
				>{Math.round(fontScale * 100)}%</button>
				<button
					on:click={zoomIn}
					disabled={fontScale >= fontScales[fontScales.length - 1]}
					class={btnDefault}
					title="Zoom in"
				>+</button>
				<span class="w-px h-6 bg-zinc-700"></span>
			</div>
			<button
				on:click={() => resizeScreen('portrait')}
				disabled={resizing}
				class="mobile-only {btnBase} {screenMode === 'portrait' ? 'bg-cyan-600 text-white' : 'bg-zinc-700 hover:bg-zinc-600 text-zinc-200'}"
				title="Portrait (42x24)"
			>
				<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-3 inline-block" fill="none" viewBox="0 0 10 16" stroke="currentColor" stroke-width="1.5">
					<rect x="1" y="1" width="8" height="14" rx="1" />
				</svg>
			</button>
			<button
				on:click={() => resizeScreen('landscape')}
				disabled={resizing}
				class="mobile-only {btnBase} {screenMode === 'landscape' ? 'bg-cyan-600 text-white' : 'bg-zinc-700 hover:bg-zinc-600 text-zinc-200'}"
				title="Landscape (86x24)"
			>
				<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-5 inline-block" fill="none" viewBox="0 0 16 10" stroke="currentColor" stroke-width="1.5">
					<rect x="1" y="1" width="14" height="8" rx="1" />
				</svg>
			</button>
			<button
				on:click={() => resizeScreen('desktop')}
				disabled={resizing}
				class="desktop-only {btnBase} {screenMode === 'desktop' ? 'bg-cyan-600 text-white' : 'bg-zinc-700 hover:bg-zinc-600 text-zinc-200'}"
				title="Split screen (120x36)"
			>
				<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-5 inline-block" fill="none" viewBox="0 0 20 14" stroke="currentColor" stroke-width="1.5">
					<rect x="1" y="1" width="8" height="12" rx="1" />
					<rect x="11" y="1" width="8" height="12" rx="1" />
				</svg>
			</button>
			<button
				on:click={() => resizeScreen('fullscreen')}
				disabled={resizing}
				class="desktop-only {btnBase} {screenMode === 'fullscreen' ? 'bg-cyan-600 text-white' : 'bg-zinc-700 hover:bg-zinc-600 text-zinc-200'}"
				title="Fullscreen (260x36)"
			>
				<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-6 inline-block" fill="none" viewBox="0 0 22 14" stroke="currentColor" stroke-width="1.5">
					<rect x="1" y="1" width="20" height="12" rx="1" />
				</svg>
			</button>
			<span class="w-px h-6 bg-zinc-700"></span>
			<button
				on:click={() => scrollToBottom(true)}
				class={btnDefault}
				aria-label="Scroll to bottom"
			>
				<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
					<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3" />
				</svg>
			</button>
			{#if lastSessionId}
				<span class="w-px h-6 bg-zinc-700"></span>
				<button
					on:click={() => dispatch('goToLastSession')}
					class="{btnBase} bg-spice-600/80 hover:bg-spice-600 text-white"
					aria-label="Go to last session"
					title="Go to last session"
				>
					<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
						<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
					</svg>
				</button>
			{/if}
		</div>

		<!-- Hidden input for keyboard capture (invisible but functional for mobile) -->
		<input
			bind:this={terminalInput}
			type="text"
			on:keydown={handleKeydown}
			on:focus={() => setTimeout(() => scrollToBottom(true), 59)}
			class="sr-only"
			disabled={!isAlive}
			aria-label="Terminal input"
		/>
	{/if}
</div>
