import React, {
	useEffect,
	useImperativeHandle,
	useRef,
	useState,
	useCallback,
} from "react";
import { Terminal } from "xterm";
import { FitAddon } from "xterm-addon-fit";
import { WebLinksAddon } from "xterm-addon-web-links";
import "xterm/css/xterm.css";

import {
	applyModifierToChar,
	type ModifierKey,
} from "../../../hooks/use-special-keys";

export interface TerminalProps {
	instanceId: string;
	sessionId?: string;
	onClose?: () => void;
	isMobile?: boolean;
	showControls?: boolean;
	activeModifier?: ModifierKey | null;
	onModifierChange?: (modifier: ModifierKey | null) => void;
	onTerminalReady?: (
		sendData: (data: string) => void,
		connectionStatus:
			| "connecting"
			| "connected"
			| "disconnected"
			| "error"
			| "resetting",
		focusInput: () => void,
		changeFontSize: (delta: number) => void,
	) => void;
}

export interface TerminalRef {
	fit: () => void;
	focus: () => void;
	reset: () => void;
}

const FONT_SIZE_KEY = "terminal-font-size";
const MIN_FONT_SIZE = 4;
const MAX_FONT_SIZE = 24;
const RECONNECT_ATTEMPTS = 3;
const RECONNECT_DELAY_BASE = 1000;

export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
	(
		{
			instanceId,
			sessionId,
			onClose,
			isMobile = false,
			showControls = true,
			activeModifier,
			onModifierChange,
			onTerminalReady,
		},
		ref,
	) => {
		const terminalRef = useRef<HTMLDivElement>(null);
		const hiddenInputRef = useRef<HTMLInputElement>(null);
		const wsRef = useRef<WebSocket | null>(null);
		const termRef = useRef<Terminal | null>(null);
		const fitAddonRef = useRef<FitAddon | null>(null);
		const reconnectAttemptsRef = useRef(0);
		const onTerminalReadyRef = useRef(onTerminalReady);
		onTerminalReadyRef.current = onTerminalReady;
		const handleFontSizeChangeRef = useRef<(delta: number) => void>(() => {});
		const [status, setStatus] = useState<
			"connecting" | "connected" | "disconnected" | "error" | "resetting"
		>("connecting");
		const [error, setError] = useState<string | null>(null);
		const [showResetConfirm, setShowResetConfirm] = useState(false);
		const activeModifierRef = useRef(activeModifier);
		activeModifierRef.current = activeModifier;
		const [fontSize, setFontSize] = useState(() => {
			if (typeof window === "undefined") return isMobile ? 8 : 8;
			const stored = localStorage.getItem(FONT_SIZE_KEY);
			if (stored) {
				const parsed = parseInt(stored, 10);
				return Math.max(MIN_FONT_SIZE, Math.min(MAX_FONT_SIZE, parsed));
			}
			return isMobile ? 8 : 8;
		});
		const lastPingRef = useRef<number>(0);
		const heartbeatCheckRef = useRef<number | null>(null);
		const isUnmountingRef = useRef(false);
		const permanentErrorRef = useRef<string | null>(null);

		const calculateFontSize = useCallback(() => {
			return fontSize;
		}, [fontSize]);

		const connectWebSocket = useCallback(() => {
			const apiUrl = import.meta.env.VITE_API_BASE_URL || "";
			const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
			const wsHost = apiUrl.replace(/^https?:\/\//, "").replace(/\/+$/, "");
			const wsPath = sessionId
				? `/ws/tool-instances/${instanceId}/terminal/${sessionId}`
				: `/ws/tool-instances/${instanceId}/terminal`;
			const wsUrl = `${wsProtocol}//${wsHost}${wsPath}`;

			// WebSocket connection established
			const ws = new WebSocket(wsUrl);
			ws.binaryType = "arraybuffer";
			wsRef.current = ws;

			ws.onopen = () => {
				setStatus("connected");
				setError(null);
				reconnectAttemptsRef.current = 0;
				lastPingRef.current = Date.now();

				// Send current terminal size immediately on connect
				if (termRef.current) {
					const { cols, rows } = termRef.current;
					// Only send if we have valid dimensions
					if (cols > 0 && rows > 0) {
						ws.send(JSON.stringify({ type: "resize", cols, rows }));
					}
				}

				// Start heartbeat check
				if (heartbeatCheckRef.current) {
					window.clearInterval(heartbeatCheckRef.current);
				}
				heartbeatCheckRef.current = window.setInterval(() => {
					const elapsed = Date.now() - lastPingRef.current;
					if (elapsed > 60000) {
						// No ping for 60 seconds, connection may be dead
						ws.close(4000, "Heartbeat timeout");
					}
				}, 30000);
			};

			// Flow control: accumulate processed bytes and send ack
			let ackAccumulator = 0;
			const ACK_THRESHOLD = 4096;
			let ackTimeout: ReturnType<typeof setTimeout> | null = null;

			const flushAck = () => {
				if (ackAccumulator > 0 && ws.readyState === WebSocket.OPEN) {
					ws.send(JSON.stringify({ type: "ack", chars: ackAccumulator }));
					ackAccumulator = 0;
				}
			};

			ws.onmessage = (event) => {
				if (!termRef.current) return;

				if (event.data instanceof ArrayBuffer) {
					const data = new Uint8Array(event.data);
					termRef.current.write(data);

					// Flow control: accumulate processed bytes
					ackAccumulator += data.length;
					if (ackAccumulator >= ACK_THRESHOLD) {
						flushAck();
					} else if (!ackTimeout) {
						ackTimeout = setTimeout(() => {
							ackTimeout = null;
							flushAck();
						}, 100);
					}
				} else if (typeof event.data === "string") {
					try {
						const msg = JSON.parse(event.data);
						if (msg.type === "status") {
							if (msg.status === "connected") {
								setStatus("connected");
								setError(null);
								// Clear terminal and refit after reset/reconnect
								if (termRef.current) {
									termRef.current.clear();
									requestAnimationFrame(() => {
										if (fitAddonRef.current && termRef.current) {
											fitAddonRef.current.fit();
											const { cols, rows } = termRef.current;
											const currentWs = wsRef.current;
											if (currentWs?.readyState === WebSocket.OPEN) {
												currentWs.send(
													JSON.stringify({ type: "resize", cols, rows }),
												);
											}
										}
									});
								}
							} else if (msg.status === "resetting") {
								setStatus("resetting");
							}
						} else if (msg.type === "ping") {
							// Respond with pong and update last ping time
							lastPingRef.current = Date.now();
							if (ws.readyState === WebSocket.OPEN) {
								ws.send(JSON.stringify({ type: "pong" }));
							}
						}
					} catch {
						termRef.current?.write(event.data);
					}
				}
			};

			ws.onclose = (event) => {
				// Clean up heartbeat check
				if (heartbeatCheckRef.current) {
					window.clearInterval(heartbeatCheckRef.current);
					heartbeatCheckRef.current = null;
				}

				// Permanent errors: do not retry
				if (event.code === 4001 || event.code === 4003 || event.code === 4004) {
					const reason = event.reason || `Instance error (code: ${event.code})`;
					setStatus("error");
					setError(reason);
					permanentErrorRef.current = reason;
					return;
				}

				if (event.code === 1000) {
					setStatus("disconnected");
					return;
				}

				if (event.code === 4000) {
					// Server closed old connection for concurrent connection - don't reconnect
					// The new connection is already established
					return;
				}

				// Transient errors: attempt reconnection
				setStatus("disconnected");
				setError(`Connection closed (code: ${event.code})`);

				if (reconnectAttemptsRef.current < RECONNECT_ATTEMPTS) {
					reconnectAttemptsRef.current++;
					const delay =
						RECONNECT_DELAY_BASE *
						Math.pow(2, reconnectAttemptsRef.current - 1);
					setTimeout(() => {
						if (isUnmountingRef.current) {
							return;
						}
						if (document.visibilityState !== "hidden") {
							connectWebSocket();
						}
					}, delay);
				}
			};

			ws.onerror = () => {
				setStatus("error");
				setError("WebSocket error");
			};

			return ws;
		}, [instanceId, sessionId]);

		useEffect(() => {
			if (!terminalRef.current) return;

			// Initialize terminal
			const currentFontSize = calculateFontSize();
			const term = new Terminal({
				cursorBlink: true,
				fontSize: currentFontSize,
				fontFamily: 'Menlo, Monaco, "Courier New", monospace',
				lineHeight: 1.2,
				letterSpacing: 0,
				allowTransparency: false,
				scrollback: 10000,
				ignoreBracketedPasteMode: false,
				fastScrollSensitivity: 5,
				scrollSensitivity: 1,
				smoothScrollDuration: 0,
				theme: {
					background: "#1e1e1e",
					foreground: "#d4d4d4",
					cursor: "#d4d4d4",
					selectionBackground: "#264f78",
					black: "#000000",
					red: "#cd3131",
					green: "#0dbc79",
					yellow: "#e5e510",
					blue: "#2472c8",
					magenta: "#bc3fbc",
					cyan: "#11a8cd",
					white: "#e5e5e5",
					brightBlack: "#666666",
					brightRed: "#f14c4c",
					brightGreen: "#23d18b",
					brightYellow: "#f5f543",
					brightBlue: "#3b8eea",
					brightMagenta: "#d670d6",
					brightCyan: "#29b8db",
					brightWhite: "#e5e5e5",
				},
			});

			termRef.current = term;

			const fitAddon = new FitAddon();
			fitAddonRef.current = fitAddon;
			term.loadAddon(fitAddon);
			term.loadAddon(new WebLinksAddon());

			// NOTE: WebGL renderer disabled.
			// The WebGL addon causes black-on-black rendering artifacts with
			// tmux/vim reverse-video (inverse color) sequences on desktop.
			// Mobile already uses the DOM renderer (WebGL fails there), which
			// handles these color attributes correctly. The DOM renderer is
			// fast enough for typical terminal workloads.
			// See: xterm.js WebGL known issues with reverse video / minimumContrastRatio

			const container = terminalRef.current;

			// Define fitTerminal before connectWebSocket so it's available in onmessage
			let lastSentCols = 0;
			let lastSentRows = 0;
			const fitTerminal = () => {
				if (!fitAddonRef.current || !termRef.current) return;
				try {
					fitAddonRef.current.fit();
				} catch {
					// Ignore fit errors during initialization
					return;
				}
				const { cols, rows } = termRef.current;
				// Only send resize when dimensions actually changed
				if (
					cols > 0 &&
					rows > 0 &&
					(cols !== lastSentCols || rows !== lastSentRows)
				) {
					lastSentCols = cols;
					lastSentRows = rows;
					const currentWs = wsRef.current;
					if (currentWs?.readyState === WebSocket.OPEN) {
						currentWs.send(JSON.stringify({ type: "resize", cols, rows }));
					}
				}
			};

			// Open xterm first (must happen before fit)
			term.open(container);
			term.focus();
			const ws = connectWebSocket();

			// Mobile touch scroll.
			// In normal mode xterm.js has a scrollable viewport; in alternate
			// screen (tmux/vim) there is no scrollback and the only way to
			// scroll is to send mouse-wheel protocol sequences to the
			// application.  We detect the active buffer by reference equality
			// (term.buffer.active === term.buffer.alternate) because the
			// `type` string can be unreliable in some xterm.js versions.
			let touchCleanup: (() => void) | undefined;
			if (isMobile) {
				let startY = 0;
				let startX = 0;
				let isScrolling = false;
				let scrollPending = 0;
				const WHEEL_DISTANCE = 12;

				const flushScroll = (force = false) => {
					if (scrollPending === 0) return;
					if (!force && Math.abs(scrollPending) < WHEEL_DISTANCE) return;
					const direction = Math.sign(scrollPending);
					const steps = Math.max(
						1,
						Math.floor(Math.abs(scrollPending) / WHEEL_DISTANCE),
					);
					const ws = wsRef.current;
					if (ws?.readyState === WebSocket.OPEN && termRef.current) {
						const buf = termRef.current.buffer.active;
						const col = buf.cursorX + 1;
						const row = buf.cursorY + 1;
						const btn = direction > 0 ? 64 : 65;
						for (let i = 0; i < steps; i++) {
							ws.send(`\x1b[<${btn};${col};${row}M`);
						}
					}
					scrollPending = 0;
				};

				const isAlternateScreen = () => {
					const term = termRef.current;
					if (!term) return false;
					return term.buffer.active === term.buffer.alternate;
				};

				const onTouchStart = (e: TouchEvent) => {
					if (e.touches.length === 1) {
						startY = e.touches[0].clientY;
						startX = e.touches[0].clientX;
						isScrolling = false;
						scrollPending = 0;
					}
				};
				const onTouchMove = (e: TouchEvent) => {
					if (e.touches.length !== 1) return;
					const touch = e.touches[0];
					const deltaY = startY - touch.clientY;
					const deltaX = Math.abs(startX - touch.clientX);

					// Decide early whether this is a vertical scroll gesture.
					if (!isScrolling) {
						if (Math.abs(deltaY) > deltaX && Math.abs(deltaY) > 2) {
							isScrolling = true;
						}
					}
					if (!isScrolling) return;

					// Take over the gesture so the page/toolbar never scrolls.
					e.preventDefault();

					if (isAlternateScreen()) {
						// Alternate screen (tmux/vim): accumulate the swipe and
						// send SGR 1006 mouse-wheel events in steps.
						scrollPending += deltaY;
						flushScroll();
					} else if (termRef.current) {
						// Normal buffer: let xterm.js scroll its own viewport by
						// the number of lines corresponding to the swipe distance.
						const lineHeight =
							termRef.current.options.fontSize != null
								? (termRef.current.options.fontSize as number) * 1.2
								: 10;
						const lines = Math.round(deltaY / lineHeight);
						if (lines !== 0) {
							termRef.current.scrollLines(lines);
						}
					}
					startY = touch.clientY;
				};
				const onTouchEnd = () => {
					if (isScrolling) {
						flushScroll(true);
					}
					isScrolling = false;
				};

				container.addEventListener("touchstart", onTouchStart, {
					passive: false,
					capture: true,
				});
				container.addEventListener("touchmove", onTouchMove, {
					passive: false,
					capture: true,
				});
				container.addEventListener("touchend", onTouchEnd, {
					capture: true,
				});
				touchCleanup = () => {
					container.removeEventListener("touchstart", onTouchStart, {
						capture: true,
					});
					container.removeEventListener("touchmove", onTouchMove, {
						capture: true,
					});
					container.removeEventListener("touchend", onTouchEnd, {
						capture: true,
					});
				};
			}

			// Initial fit after layout settles (terminal must be opened first)
			let fitAttempts = 0;
			const doInitialFit = () => {
				if (!container.isConnected) return;
				fitAttempts++;
				// Ensure container has dimensions before fitting
				if (container.clientWidth > 0 && container.clientHeight > 0) {
					fitTerminal();
				} else if (fitAttempts < 50) {
					// Container not ready yet, try again (max 50 attempts ~ 1s)
					requestAnimationFrame(doInitialFit);
				}
			};
			requestAnimationFrame(doInitialFit);

			// Refit after font load (metrics may change)
			document.fonts.ready.then(() => {
				requestAnimationFrame(() => fitTerminal());
			});

			// Handle terminal input
			term.onData((data) => {
				const currentWs = wsRef.current;
				if (currentWs?.readyState !== WebSocket.OPEN) return;

				// Apply active modifier to single-character input
				const modifier = activeModifierRef.current;
				if (modifier && data.length === 1) {
					const modified = applyModifierToChar(data, modifier);
					if (modified) {
						currentWs.send(modified);
						onModifierChange?.(null);
						return;
					}
				}

				currentWs.send(data);
			});

			// Handle container resize with ResizeObserver for accurate dimension tracking
			let resizeTimeout: ReturnType<typeof setTimeout>;
			let lastWidth = 0;
			let lastHeight = 0;
			const resizeObserver = new ResizeObserver((entries) => {
				const entry = entries[0];
				if (!entry) return;

				const { width, height } = entry.contentRect;
				// Only trigger if dimensions actually changed
				if (width === lastWidth && height === lastHeight) return;
				lastWidth = width;
				lastHeight = height;

				clearTimeout(resizeTimeout);
				resizeTimeout = setTimeout(() => {
					requestAnimationFrame(() => {
						if (!container.isConnected) return;
						fitTerminal();
					});
				}, 50);
			});
			resizeObserver.observe(container);

			// Window resize fallback (for viewport changes that don't affect container dimensions)
			let windowResizeTimeout: ReturnType<typeof setTimeout>;
			const handleWindowResize = () => {
				clearTimeout(windowResizeTimeout);
				windowResizeTimeout = setTimeout(() => {
					requestAnimationFrame(() => fitTerminal());
				}, 250);
			};
			window.addEventListener("resize", handleWindowResize);

			// Refit after mobile header auto-hides (3s delay + 0.3s transition)
			const headerHideTimeout = setTimeout(() => {
				fitTerminal();
			}, 4000);

			// Notify parent about terminal readiness
			if (onTerminalReadyRef.current) {
				const sendData = (data: string) => {
					const currentWs = wsRef.current;
					if (currentWs?.readyState === WebSocket.OPEN) {
						currentWs.send(data);
					}
				};
				const focusInput = () => {
					termRef.current?.focus();
				};
				const changeFontSize = (delta: number) => {
					handleFontSizeChangeRef.current(delta);
				};
				onTerminalReadyRef.current(
					sendData,
					status,
					focusInput,
					changeFontSize,
				);
			}

			// Visibility API for reconnection
			const handleVisibilityChange = () => {
				if (
					document.visibilityState === "visible" &&
					ws &&
					ws.readyState !== WebSocket.OPEN
				) {
					if (permanentErrorRef.current) {
						return;
					}
					reconnectAttemptsRef.current = 0;
					connectWebSocket();
				}
			};
			document.addEventListener("visibilitychange", handleVisibilityChange);

			return () => {
				isUnmountingRef.current = true;
				clearTimeout(resizeTimeout);
				clearTimeout(windowResizeTimeout);
				clearTimeout(headerHideTimeout);
				resizeObserver.disconnect();
				window.removeEventListener("resize", handleWindowResize);
				document.removeEventListener(
					"visibilitychange",
					handleVisibilityChange,
				);
				if (touchCleanup) touchCleanup();
				if (ws) {
					ws.close(1000, "Component unmounting");
				}
				if (heartbeatCheckRef.current) {
					window.clearInterval(heartbeatCheckRef.current);
					heartbeatCheckRef.current = null;
				}
				try {
					term.dispose();
				} catch {
					// Ignore disposal errors from partially torn-down terminal
				}
			};
		}, [instanceId, connectWebSocket]);

		useImperativeHandle(ref, () => ({
			fit: () => {
				if (fitAddonRef.current && termRef.current) {
					try {
						fitAddonRef.current.fit();
						const { cols, rows } = termRef.current;
						if (
							wsRef.current?.readyState === WebSocket.OPEN &&
							cols > 0 &&
							rows > 0
						) {
							wsRef.current.send(
								JSON.stringify({ type: "resize", cols, rows }),
							);
						}
					} catch {
						// Ignore fit errors
					}
				}
			},
			focus: () => {
				termRef.current?.focus();
			},
			reset: () => {
				if (wsRef.current?.readyState === WebSocket.OPEN) {
					wsRef.current.send(JSON.stringify({ type: "reset" }));
				}
			},
		}));

		// Update parent about status changes
		useEffect(() => {
			if (onTerminalReady && termRef.current) {
				const sendData = (data: string) => {
					if (wsRef.current?.readyState === WebSocket.OPEN) {
						wsRef.current.send(data);
					}
				};
				const focusInput = () => {
					termRef.current?.focus();
				};
				const changeFontSize = (delta: number) => {
					handleFontSizeChangeRef.current(delta);
				};
				onTerminalReady(sendData, status, focusInput, changeFontSize);
			}
		}, [status, onTerminalReady]);

		const handleFontSizeChange = (delta: number) => {
			const newSize = Math.max(
				MIN_FONT_SIZE,
				Math.min(MAX_FONT_SIZE, fontSize + delta),
			);
			setFontSize(newSize);
			localStorage.setItem(FONT_SIZE_KEY, newSize.toString());
			if (termRef.current && fitAddonRef.current) {
				termRef.current.options.fontSize = newSize;
				requestAnimationFrame(() => {
					if (termRef.current && fitAddonRef.current) {
						try {
							fitAddonRef.current.fit();
							const { cols, rows } = termRef.current;
							if (wsRef.current?.readyState === WebSocket.OPEN) {
								wsRef.current.send(
									JSON.stringify({
										type: "resize",
										cols,
										rows,
									}),
								);
							}
						} catch {
							// Ignore fit errors during re-initialization
						}
					}
				});
			}
		};
		handleFontSizeChangeRef.current = handleFontSizeChange;

		const handleCopy = async () => {
			if (!termRef.current) return;
			const selection = termRef.current.getSelection();
			if (selection) {
				try {
					await navigator.clipboard.writeText(selection);
				} catch {
					// Fallback for older browsers
					const textarea = document.createElement("textarea");
					textarea.value = selection;
					document.body.appendChild(textarea);
					textarea.select();
					document.execCommand("copy");
					document.body.removeChild(textarea);
				}
			}
		};

		const handlePaste = async () => {
			try {
				const text = await navigator.clipboard.readText();
				if (wsRef.current?.readyState === WebSocket.OPEN) {
					wsRef.current.send(text);
				}
			} catch {
				// Clipboard API not available
			}
		};

		// Focus terminal on mobile to keep keyboard open
		const handleTerminalClick = () => {
			if (isMobile && termRef.current) {
				termRef.current.focus();
			}
		};

		return (
			<div
				className={`terminal-wrapper ${isMobile ? "mobile" : ""} ${!showControls ? "no-controls" : ""}`}
			>
				{showControls && (
					<div className="terminal-header">
						<div className="terminal-header-left">
							<div className="terminal-status">
								<span
									className={`status-dot ${status}`}
									aria-label={`Terminal status: ${status}`}
								/>
								<span className="status-text">
									{status === "resetting"
										? "Resetting..."
										: reconnectAttemptsRef.current > 0 && status !== "connected"
											? `Reconnecting (${reconnectAttemptsRef.current}/${RECONNECT_ATTEMPTS})...`
											: status}
								</span>
							</div>
							{isMobile && (
								<>
									<button
										className="terminal-header-button"
										onClick={handleCopy}
										type="button"
										aria-label="Copy selection"
									>
										Copy
									</button>
									<button
										className="terminal-header-button"
										onClick={handlePaste}
										type="button"
										aria-label="Paste from clipboard"
									>
										Paste
									</button>
								</>
							)}
						</div>
						<div className="terminal-header-right">
							<button
								className="terminal-header-button"
								onClick={() => handleFontSizeChange(-1)}
								type="button"
								aria-label="Decrease font size"
							>
								A-
							</button>
							<button
								className="terminal-header-button"
								onClick={() => handleFontSizeChange(1)}
								type="button"
								aria-label="Increase font size"
							>
								A+
							</button>
							<button
								className="terminal-header-button"
								onClick={() => setShowResetConfirm(true)}
								type="button"
								aria-label="Reset terminal"
							>
								Reset
							</button>
							{onClose && (
								<button
									className="terminal-close"
									onClick={onClose}
									type="button"
								>
									Close
								</button>
							)}
						</div>
					</div>
				)}
				{showResetConfirm && (
					<div className="terminal-reset-confirm">
						<div className="terminal-reset-confirm-content">
							<p>
								Reset terminal? This will kill the current shell session and
								start fresh.
							</p>
							<div className="terminal-reset-confirm-buttons">
								<button
									className="terminal-reset-confirm-button cancel"
									onClick={() => setShowResetConfirm(false)}
									type="button"
								>
									Cancel
								</button>
								<button
									className="terminal-reset-confirm-button confirm"
									onClick={() => {
										setShowResetConfirm(false);
										if (wsRef.current?.readyState === WebSocket.OPEN) {
											wsRef.current.send(JSON.stringify({ type: "reset" }));
										}
									}}
									type="button"
								>
									Reset
								</button>
							</div>
						</div>
					</div>
				)}
				{error && (
					<div className="terminal-error">
						{error}
						{status === "error" && (
							<button
								className="terminal-reconnect"
								onClick={() => {
									reconnectAttemptsRef.current = 0;
									connectWebSocket();
								}}
								type="button"
							>
								Reconnect
							</button>
						)}
					</div>
				)}
				<div
					ref={terminalRef}
					className="terminal-container"
					onClick={handleTerminalClick}
				/>
				{isMobile && (
					<input
						ref={hiddenInputRef}
						type="text"
						className="terminal-hidden-input"
						aria-hidden="true"
					/>
				)}
			</div>
		);
	},
);

TerminalComponent.displayName = "TerminalComponent";
