import { Ionicons } from '@expo/vector-icons';
import { type ListenerEvent } from '@fressh/react-native-uniffi-russh';
import {
	XtermJsWebView,
	type XtermWebViewHandle,
} from '@fressh/react-native-xtermjs-webview';

import {
	Stack,
	useLocalSearchParams,
	useRouter,
	useFocusEffect,
} from 'expo-router';
import React, {
	createContext,
	startTransition,
	useEffect,
	useRef,
	useState,
} from 'react';
import { KeyboardAvoidingView, Pressable, Text, View } from 'react-native';
import { useSshStore } from '@/lib/ssh-store';
import { useTheme } from '@/lib/theme';
import { useBottomTabSpacing } from '@/lib/useBottomTabSpacing';
import { useContextSafe } from '@/lib/utils';

type IconName = keyof typeof Ionicons.glyphMap;

export default function TabsShellDetail() {
	const [ready, setReady] = useState(false);

	useFocusEffect(
		React.useCallback(() => {
			startTransition(() => {
				setTimeout(() => {
					// TODO: This is gross. It would be much better to switch
					// after the navigation animation completes.
					setReady(true);
				}, 16);
			});

			return () => {
				setReady(false);
			};
		}, []),
	);

	if (!ready) return <RouteSkeleton />;
	return <ShellDetail />;
}

function RouteSkeleton() {
	const theme = useTheme();
	return (
		<View
			style={{
				flex: 1,
				justifyContent: 'center',
				alignItems: 'center',
				backgroundColor: theme.colors.background,
			}}
		>
			<Text style={{ color: theme.colors.textPrimary, fontSize: 20 }}>
				Loading
			</Text>
		</View>
	);
}

const encoder = new TextEncoder();

function ShellDetail() {
	const xtermRef = useRef<XtermWebViewHandle>(null);
	const terminalReadyRef = useRef(false);
	const listenerIdRef = useRef<bigint | null>(null);

	const searchParams = useLocalSearchParams<{
		connectionId?: string;
		channelId?: string;
	}>();

	if (!searchParams.connectionId || !searchParams.channelId)
		throw new Error('Missing connectionId or channelId');

	const connectionId = searchParams.connectionId;
	const channelId = parseInt(searchParams.channelId);

	const router = useRouter();
	const theme = useTheme();

	const shell = useSshStore(
		(s) => s.shells[`${connectionId}-${channelId}` as const],
	);
	const connection = useSshStore((s) => s.connections[connectionId]);

	useEffect(() => {
		if (shell && connection) return;
		console.log('shell or connection not found, replacing route with /shell');
		router.back();
	}, [connection, router, shell]);

	useEffect(() => {
		const xterm = xtermRef.current;
		return () => {
			if (shell && listenerIdRef.current != null)
				shell.removeListener(listenerIdRef.current);
			listenerIdRef.current = null;
			if (xterm) xterm.flush();
		};
	}, [shell]);

	const marginBottom = useBottomTabSpacing();

	return (
		<>
			<View
				style={{
					justifyContent: 'flex-start',
					backgroundColor: theme.colors.background,
					paddingTop: 2,
					paddingLeft: 8,
					paddingRight: 8,
					paddingBottom: 0,
					marginBottom,
					flex: 1,
				}}
			>
				<Stack.Screen
					options={{
						headerBackVisible: true,
						headerRight: () => (
							<Pressable
								accessibilityLabel="Disconnect"
								hitSlop={10}
								onPress={async () => {
									if (!connection) return;
									try {
										await connection.disconnect();
									} catch (e) {
										console.warn('Failed to disconnect', e);
									}
								}}
							>
								<Ionicons name="power" size={20} color={theme.colors.primary} />
							</Pressable>
						),
					}}
				/>
				<KeyboardAvoidingView
					behavior="height"
					keyboardVerticalOffset={120}
					style={{ flex: 1, gap: 4 }}
				>
					<View
						style={{
							flex: 1,
							borderWidth: 2,
							borderColor: theme.colors.border,
						}}
					>
						<XtermJsWebView
							ref={xtermRef}
							style={{ flex: 1 }}
							webViewOptions={{
								// Prevent iOS from adding automatic top inset inside WebView
								contentInsetAdjustmentBehavior: 'never',
							}}
							logger={{
								log: console.log,
								// debug: console.log,
								warn: console.warn,
								error: console.error,
							}}
							// xterm options
							xtermOptions={{
								theme: {
									background: theme.colors.background,
									foreground: theme.colors.textPrimary,
								},
							}}
							onInitialized={() => {
								if (terminalReadyRef.current) return;
								terminalReadyRef.current = true;

								if (!shell) throw new Error('Shell not found');

								// Replay from head, then attach live listener
								void (async () => {
									const res = shell.readBuffer({ mode: 'head' });
									console.log('readBuffer(head)', {
										chunks: res.chunks.length,
										nextSeq: res.nextSeq,
										dropped: res.dropped,
									});
									if (res.chunks.length) {
										const chunks = res.chunks.map((c) => c.bytes);
										const xr = xtermRef.current;
										if (xr) {
											xr.writeMany(chunks.map((c) => new Uint8Array(c)));
											xr.flush();
										}
									}
									const id = shell.addListener(
										(ev: ListenerEvent) => {
											if ('kind' in ev) {
												console.log('listener.dropped', ev);
												return;
											}
											const chunk = ev;
											const xr3 = xtermRef.current;
											if (xr3) xr3.write(new Uint8Array(chunk.bytes));
										},
										{ cursor: { mode: 'seq', seq: res.nextSeq } },
									);
									console.log('shell listener attached', id.toString());
									listenerIdRef.current = id;
								})();
								// Focus to pop the keyboard (iOS needs the prop we set)
								const xr2 = xtermRef.current;
								if (xr2) xr2.focus();
							}}
							onData={(terminalMessage) => {
								if (!shell) return;
								const bytes = encoder.encode(terminalMessage);
								shell.sendData(bytes.buffer).catch((e: unknown) => {
									console.warn('sendData failed', e);
									router.back();
								});
							}}
						/>
					</View>
					<KeyboardToolbar
						sendBytes={(bytes) => {
							if (!shell) return;
							shell.sendData(bytes.buffer).catch((e: unknown) => {
								console.warn('sendData failed', e);
								router.back();
							});
						}}
					/>
				</KeyboardAvoidingView>
			</View>
			{/* <KeyboardToolbar
				offset={{
					opened: -80,
				}}
			/> */}
		</>
	);
}

type KeyboardToolbarProps = {
	sendBytes: (bytes: Uint8Array<ArrayBuffer>) => void;
};
const KeyboardToolBarContext = createContext<KeyboardToolbarProps | null>(null);

function KeyboardToolbar(props: KeyboardToolbarProps) {
	return (
		<KeyboardToolBarContext value={props}>
			<View
				style={{
					height: 100,
				}}
			>
				<KeyboardToolbarRow>
					<KeyboardToolbarButtonPreset preset="esc" />
					<KeyboardToolbarButtonPreset preset="/" />
					<KeyboardToolbarButtonPreset preset="|" />
					<KeyboardToolbarButtonPreset preset="home" />
					<KeyboardToolbarButtonPreset preset="up" />
					<KeyboardToolbarButtonPreset preset="end" />
					<KeyboardToolbarButtonPreset preset="pgup" />
				</KeyboardToolbarRow>
				<KeyboardToolbarRow>
					<KeyboardToolbarButtonPreset preset="tab" />
					<KeyboardToolbarButtonPreset preset="ctrl" />
					<KeyboardToolbarButtonPreset preset="alt" />
					<KeyboardToolbarButtonPreset preset="left" />
					<KeyboardToolbarButtonPreset preset="down" />
					<KeyboardToolbarButtonPreset preset="right" />
					<KeyboardToolbarButtonPreset preset="pgdn" />
				</KeyboardToolbarRow>
			</View>
		</KeyboardToolBarContext>
	);
}

function KeyboardToolbarRow({ children }: { children?: React.ReactNode }) {
	return <View style={{ flexDirection: 'row', flex: 1 }}>{children}</View>;
}

type KeyboardToolbarButtonPresetType =
	| 'esc'
	| '/'
	| '|'
	| 'home'
	| 'up'
	| 'end'
	| 'pgup'
	| 'pgdn'
	| 'fn'
	| 'tab'
	| 'ctrl'
	| 'alt'
	| 'left'
	| 'down'
	| 'right'
	| 'insert'
	| 'delete'
	| 'pageup'
	| 'pagedown'
	| 'fn';

function KeyboardToolbarButtonPreset({
	preset,
}: {
	preset: KeyboardToolbarButtonPresetType;
}) {
	return (
		<KeyboardToolbarButton {...keyboardToolbarButtonPresetToProps[preset]} />
	);
}

const keyboardToolbarButtonPresetToProps: Record<
	KeyboardToolbarButtonPresetType,
	KeyboardToolbarButtonProps
> = {
	esc: { label: 'ESC', sendBytes: new Uint8Array([27]) },
	'/': { label: '/', sendBytes: new Uint8Array([47]) },
	'|': { label: '|', sendBytes: new Uint8Array([124]) },
	home: { label: 'HOME', sendBytes: new Uint8Array([27, 91, 72]) },
	end: { label: 'END', sendBytes: new Uint8Array([27, 91, 70]) },
	pgup: { label: 'PGUP', sendBytes: new Uint8Array([27, 91, 53, 126]) },
	pgdn: { label: 'PGDN', sendBytes: new Uint8Array([27, 91, 54, 126]) },
	fn: { label: 'FN', isModifier: true },
	tab: { label: 'TAB', sendBytes: new Uint8Array([9]) },
	ctrl: { label: 'CTRL', isModifier: true },
	alt: { label: 'ALT', isModifier: true },
	left: { iconName: 'arrow-back', sendBytes: new Uint8Array([27, 91, 68]) },
	up: { iconName: 'arrow-up', sendBytes: new Uint8Array([27, 91, 65]) },
	down: { iconName: 'arrow-down', sendBytes: new Uint8Array([27, 91, 66]) },
	right: {
		iconName: 'arrow-forward',
		sendBytes: new Uint8Array([27, 91, 67]),
	},
	insert: { label: 'INSERT', sendBytes: new Uint8Array([27, 91, 50, 126]) },
	delete: { label: 'DELETE', sendBytes: new Uint8Array([27, 91, 51, 126]) },
	pageup: { label: 'PAGEUP', sendBytes: new Uint8Array([27, 91, 53, 126]) },
	pagedown: { label: 'PAGEDOWN', sendBytes: new Uint8Array([27, 91, 54, 126]) },
};

type KeyboardToolbarButtonProps = (
	| {
			isModifier: true;
	  }
	| {
			sendBytes: Uint8Array;
	  }
) &
	(
		| {
				label: string;
		  }
		| {
				iconName: IconName;
		  }
	);

function KeyboardToolbarButton(props: KeyboardToolbarButtonProps) {
	const theme = useTheme();
	const [modifierActive, setModifierActive] = useState(false);
	const { sendBytes } = useContextSafe(KeyboardToolBarContext);

	const isTextLabel = 'label' in props;
	const children = isTextLabel ? (
		<Text style={{ color: theme.colors.textPrimary }}>{props.label}</Text>
	) : (
		<Ionicons
			name={props.iconName}
			size={20}
			color={theme.colors.textPrimary}
		/>
	);

	return (
		<Pressable
			onPress={() => {
				console.log('button pressed');
				if ('isModifier' in props && props.isModifier) {
					setModifierActive((active) => !active);
				} else if ('sendBytes' in props) {
					// todo: send key press
					sendBytes(new Uint8Array(props.sendBytes));
				}
			}}
			style={[
				{
					flex: 1,
					alignItems: 'center',
					justifyContent: 'center',
					borderWidth: 1,
					borderColor: theme.colors.border,
				},
				modifierActive && { backgroundColor: theme.colors.primary },
			]}
		>
			{children}
		</Pressable>
	);
}
