/**
 * KeybindContext — minimal localStorage-backed keybind registry.
 *
 * Defines a small set of built-in actions, each with a default combo.
 * Stores user overrides in localStorage under `keybinds.combos`.
 * Installs a single window keydown listener that, on a matching combo,
 * dispatches `brycord:keybind:<actionId>` as a window CustomEvent —
 * components listen for those events to execute the action.
 *
 * The context is the single source of truth for combo strings, so
 * KeybindsTab can rebind while the listener picks up the change via
 * `useEffect` re-subscription.
 */
import {
	createContext,
	useCallback,
	useContext,
	useEffect,
	useMemo,
	useState,
	type ReactNode,
} from 'react';

export type KeybindCategory = 'voice' | 'navigation' | 'popouts' | 'messaging';

export interface KeybindAction {
	id: string;
	label: string;
	description: string;
	category: KeybindCategory;
	defaultCombo: string;
}

const DEFAULT_ACTIONS: KeybindAction[] = [
	{
		id: 'voice.toggleMute',
		label: 'Toggle Mute',
		description: 'Mute or unmute your microphone while in a voice call.',
		category: 'voice',
		defaultCombo: 'Ctrl+Shift+M',
	},
	{
		id: 'voice.toggleDeafen',
		label: 'Toggle Deafen',
		description: 'Deafen or undeafen — stops all incoming audio.',
		category: 'voice',
		defaultCombo: 'Ctrl+Shift+D',
	},
	{
		id: 'voice.disconnect',
		label: 'Disconnect from Call',
		description: 'Leave the current voice call.',
		category: 'voice',
		defaultCombo: '',
	},
	{
		id: 'navigation.goToDMs',
		label: 'Go to Direct Messages',
		description: 'Jump to the direct-messages home view.',
		category: 'navigation',
		defaultCombo: 'Ctrl+Alt+H',
	},
	{
		id: 'navigation.focusSearch',
		label: 'Focus Search',
		description: 'Focus the channel search box.',
		category: 'navigation',
		defaultCombo: 'Ctrl+K',
	},
	{
		id: 'popouts.openUserSettings',
		label: 'Open User Settings',
		description: 'Open the user settings modal.',
		category: 'popouts',
		defaultCombo: 'Ctrl+,',
	},
	{
		id: 'popouts.openPins',
		label: 'Toggle Pins',
		description: 'Open or close the pinned messages popover.',
		category: 'popouts',
		defaultCombo: 'Ctrl+P',
	},
	{
		id: 'popouts.toggleMembers',
		label: 'Toggle Member List',
		description: 'Show or hide the channel members panel.',
		category: 'popouts',
		defaultCombo: 'Ctrl+U',
	},
	{
		id: 'popouts.openEmojiPicker',
		label: 'Open Emoji Picker',
		description: 'Open the emoji picker in the composer.',
		category: 'popouts',
		defaultCombo: 'Ctrl+E',
	},
	{
		id: 'popouts.openGifPicker',
		label: 'Open GIF Picker',
		description: 'Open the expression picker on the GIFs tab.',
		category: 'popouts',
		defaultCombo: 'Ctrl+G',
	},
	{
		id: 'messaging.markAllRead',
		label: 'Mark Channel as Read',
		description: 'Clear the unread indicator for the current channel.',
		category: 'messaging',
		defaultCombo: 'Escape',
	},
	{
		id: 'messaging.focusComposer',
		label: 'Focus Composer',
		description: 'Jump the caret to the message input box.',
		category: 'messaging',
		defaultCombo: 'Tab',
	},
];

const STORAGE_KEY = 'keybinds.combos';

function loadOverrides(): Record<string, string> {
	if (typeof localStorage === 'undefined') return {};
	try {
		const raw = localStorage.getItem(STORAGE_KEY);
		if (!raw) return {};
		const parsed = JSON.parse(raw);
		return typeof parsed === 'object' && parsed ? parsed : {};
	} catch {
		return {};
	}
}

function saveOverrides(overrides: Record<string, string>) {
	if (typeof localStorage === 'undefined') return;
	try {
		localStorage.setItem(STORAGE_KEY, JSON.stringify(overrides));
	} catch {
		/* ignore quota errors */
	}
}

export function eventToCombo(e: KeyboardEvent): string | null {
	// Pure modifier press — wait for a real key.
	const key = e.key;
	if (key === 'Control' || key === 'Shift' || key === 'Alt' || key === 'Meta') {
		return null;
	}
	const parts: string[] = [];
	if (e.ctrlKey) parts.push('Ctrl');
	if (e.shiftKey) parts.push('Shift');
	if (e.altKey) parts.push('Alt');
	if (e.metaKey) parts.push('Meta');
	// Normalise letters to uppercase; punctuation stays as-is.
	const normalised = key.length === 1 ? key.toUpperCase() : key;
	parts.push(normalised);
	return parts.join('+');
}

export function formatCombo(combo: string): string {
	if (!combo) return 'Unbound';
	return combo;
}

interface KeybindContextValue {
	actions: KeybindAction[];
	getCombo: (actionId: string) => string;
	setCombo: (actionId: string, combo: string) => void;
	resetCombo: (actionId: string) => void;
	resetAll: () => void;
	findConflict: (combo: string, ignoreActionId?: string) => string | null;
}

const KeybindCtx = createContext<KeybindContextValue | null>(null);

export function KeybindProvider({ children }: { children: ReactNode }) {
	const [overrides, setOverrides] = useState<Record<string, string>>(() => loadOverrides());

	const combos = useMemo<Record<string, string>>(() => {
		const out: Record<string, string> = {};
		for (const action of DEFAULT_ACTIONS) {
			out[action.id] = overrides[action.id] ?? action.defaultCombo;
		}
		return out;
	}, [overrides]);

	const getCombo = useCallback(
		(actionId: string): string => combos[actionId] ?? '',
		[combos],
	);

	const findConflict = useCallback(
		(combo: string, ignoreActionId?: string): string | null => {
			if (!combo) return null;
			for (const action of DEFAULT_ACTIONS) {
				if (action.id === ignoreActionId) continue;
				if ((combos[action.id] ?? '') === combo) return action.id;
			}
			return null;
		},
		[combos],
	);

	const setCombo = useCallback(
		(actionId: string, combo: string) => {
			setOverrides((prev) => {
				const next: Record<string, string> = { ...prev, [actionId]: combo };
				// Clear conflicting binding: last-write-wins, matches the
				// new UI's "other binding loses" behaviour.
				if (combo) {
					for (const action of DEFAULT_ACTIONS) {
						if (action.id === actionId) continue;
						const current = next[action.id] ?? action.defaultCombo;
						if (current === combo) {
							next[action.id] = '';
						}
					}
				}
				saveOverrides(next);
				return next;
			});
		},
		[],
	);

	const resetCombo = useCallback((actionId: string) => {
		setOverrides((prev) => {
			const next = { ...prev };
			delete next[actionId];
			saveOverrides(next);
			return next;
		});
	}, []);

	const resetAll = useCallback(() => {
		setOverrides({});
		saveOverrides({});
	}, []);

	// Install the global keydown dispatcher. Uses capture phase so it
	// wins against components that use keydown for their own shortcuts
	// — rebinding in settings disables the default behaviour cleanly.
	useEffect(() => {
		const onKeyDown = (e: KeyboardEvent) => {
			// Never intercept keys typed into inputs / contenteditable —
			// a bare `Escape` would otherwise cancel active composition.
			const target = e.target as HTMLElement | null;
			if (target) {
				const tag = target.tagName;
				if (
					tag === 'INPUT' ||
					tag === 'TEXTAREA' ||
					target.isContentEditable
				) {
					// Allow Ctrl- / Ctrl+Shift- combinations through —
					// those are deliberate shortcuts, never accidental
					// typing. Plain keys still wait for focus to leave.
					if (!(e.ctrlKey || e.metaKey)) {
						return;
					}
				}
			}
			const combo = eventToCombo(e);
			if (!combo) return;
			for (const action of DEFAULT_ACTIONS) {
				if ((combos[action.id] ?? '') === combo) {
					e.preventDefault();
					e.stopPropagation();
					window.dispatchEvent(
						new CustomEvent(`brycord:keybind:${action.id}`),
					);
					return;
				}
			}
		};
		window.addEventListener('keydown', onKeyDown, { capture: true });
		return () => {
			window.removeEventListener('keydown', onKeyDown, {
				capture: true,
			} as EventListenerOptions);
		};
	}, [combos]);

	const value = useMemo<KeybindContextValue>(
		() => ({
			actions: DEFAULT_ACTIONS,
			getCombo,
			setCombo,
			resetCombo,
			resetAll,
			findConflict,
		}),
		[getCombo, setCombo, resetCombo, resetAll, findConflict],
	);

	return <KeybindCtx.Provider value={value}>{children}</KeybindCtx.Provider>;
}

export function useKeybinds(): KeybindContextValue {
	const ctx = useContext(KeybindCtx);
	if (!ctx) {
		// Fall back to a read-only default when used outside the
		// provider (e.g. during SSR or in isolated tests).
		return {
			actions: DEFAULT_ACTIONS,
			getCombo: (id) =>
				DEFAULT_ACTIONS.find((a) => a.id === id)?.defaultCombo ?? '',
			setCombo: () => {},
			resetCombo: () => {},
			resetAll: () => {},
			findConflict: () => null,
		};
	}
	return ctx;
}
