/*
 * Copyright (C) 2026 Fluxer Contributors
 *
 * This file is part of Fluxer.
 *
 * Fluxer is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Fluxer is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
 */

import {createRequire} from 'node:module';
import {createChildLogger} from '@electron/common/Logger';
import {getMainWindow} from '@electron/main/Window';
import {ipcMain} from 'electron';
import type {UiohookKeyboardEvent, UiohookMouseEvent} from 'uiohook-napi';
import {UiohookKey, uIOhook} from 'uiohook-napi';

const logger = createChildLogger('GlobalKeyHook');

interface KeybindRegistration {
	id: string;
	keycode: number;
	mouseButton?: number;
	modifiers: {
		ctrl: boolean;
		alt: boolean;
		shift: boolean;
		meta: boolean;
	};
}

const registeredKeybinds = new Map<string, KeybindRegistration>();
const activeKeys = new Set<number>();
let hookStarted = false;
const requireModule = createRequire(import.meta.url);

function keycodeToKeyName(keycode: number): string {
	const keyMap: Record<number, string> = {
		[UiohookKey.Escape]: 'Escape',
		[UiohookKey.F1]: 'F1',
		[UiohookKey.F2]: 'F2',
		[UiohookKey.F3]: 'F3',
		[UiohookKey.F4]: 'F4',
		[UiohookKey.F5]: 'F5',
		[UiohookKey.F6]: 'F6',
		[UiohookKey.F7]: 'F7',
		[UiohookKey.F8]: 'F8',
		[UiohookKey.F9]: 'F9',
		[UiohookKey.F10]: 'F10',
		[UiohookKey.F11]: 'F11',
		[UiohookKey.F12]: 'F12',
		[UiohookKey.Backquote]: 'Backquote',
		[UiohookKey['1']]: '1',
		[UiohookKey['2']]: '2',
		[UiohookKey['3']]: '3',
		[UiohookKey['4']]: '4',
		[UiohookKey['5']]: '5',
		[UiohookKey['6']]: '6',
		[UiohookKey['7']]: '7',
		[UiohookKey['8']]: '8',
		[UiohookKey['9']]: '9',
		[UiohookKey['0']]: '0',
		[UiohookKey.Minus]: 'Minus',
		[UiohookKey.Equal]: 'Equal',
		[UiohookKey.Backspace]: 'Backspace',
		[UiohookKey.Tab]: 'Tab',
		[UiohookKey.Q]: 'Q',
		[UiohookKey.W]: 'W',
		[UiohookKey.E]: 'E',
		[UiohookKey.R]: 'R',
		[UiohookKey.T]: 'T',
		[UiohookKey.Y]: 'Y',
		[UiohookKey.U]: 'U',
		[UiohookKey.I]: 'I',
		[UiohookKey.O]: 'O',
		[UiohookKey.P]: 'P',
		[UiohookKey.BracketLeft]: 'BracketLeft',
		[UiohookKey.BracketRight]: 'BracketRight',
		[UiohookKey.Backslash]: 'Backslash',
		[UiohookKey.CapsLock]: 'CapsLock',
		[UiohookKey.A]: 'A',
		[UiohookKey.S]: 'S',
		[UiohookKey.D]: 'D',
		[UiohookKey.F]: 'F',
		[UiohookKey.G]: 'G',
		[UiohookKey.H]: 'H',
		[UiohookKey.J]: 'J',
		[UiohookKey.K]: 'K',
		[UiohookKey.L]: 'L',
		[UiohookKey.Semicolon]: 'Semicolon',
		[UiohookKey.Quote]: 'Quote',
		[UiohookKey.Enter]: 'Enter',
		[UiohookKey.Shift]: 'ShiftLeft',
		[UiohookKey.Z]: 'Z',
		[UiohookKey.X]: 'X',
		[UiohookKey.C]: 'C',
		[UiohookKey.V]: 'V',
		[UiohookKey.B]: 'B',
		[UiohookKey.N]: 'N',
		[UiohookKey.M]: 'M',
		[UiohookKey.Comma]: 'Comma',
		[UiohookKey.Period]: 'Period',
		[UiohookKey.Slash]: 'Slash',
		[UiohookKey.ShiftRight]: 'ShiftRight',
		[UiohookKey.Ctrl]: 'ControlLeft',
		[UiohookKey.Meta]: 'MetaLeft',
		[UiohookKey.Alt]: 'AltLeft',
		[UiohookKey.Space]: 'Space',
		[UiohookKey.AltRight]: 'AltRight',
		[UiohookKey.MetaRight]: 'MetaRight',
		[UiohookKey.CtrlRight]: 'ControlRight',
		[UiohookKey.ArrowLeft]: 'ArrowLeft',
		[UiohookKey.ArrowUp]: 'ArrowUp',
		[UiohookKey.ArrowRight]: 'ArrowRight',
		[UiohookKey.ArrowDown]: 'ArrowDown',
		[UiohookKey.Insert]: 'Insert',
		[UiohookKey.Delete]: 'Delete',
		[UiohookKey.Home]: 'Home',
		[UiohookKey.End]: 'End',
		[UiohookKey.PageUp]: 'PageUp',
		[UiohookKey.PageDown]: 'PageDown',
	};

	return keyMap[keycode] ?? `Key${keycode}`;
}

function handleKeyEvent(event: UiohookKeyboardEvent, type: 'keydown' | 'keyup') {
	const mainWindow = getMainWindow();
	if (!mainWindow) return;

	const keycode = event.keycode;
	const keyName = keycodeToKeyName(keycode);

	if (type === 'keydown') {
		activeKeys.add(keycode);
	} else {
		activeKeys.delete(keycode);
	}

	mainWindow.webContents.send('global-key-event', {
		type,
		keycode,
		keyName,
		altKey: event.altKey,
		ctrlKey: event.ctrlKey,
		shiftKey: event.shiftKey,
		metaKey: event.metaKey,
	});

	for (const [id, keybind] of registeredKeybinds) {
		if (keybind.keycode === keycode) {
			const modifiersMatch =
				keybind.modifiers.ctrl === event.ctrlKey &&
				keybind.modifiers.alt === event.altKey &&
				keybind.modifiers.shift === event.shiftKey &&
				keybind.modifiers.meta === event.metaKey;

			if (modifiersMatch || !Object.values(keybind.modifiers).some(Boolean)) {
				mainWindow.webContents.send('global-keybind-triggered', {
					id,
					type,
				});
			}
		}
	}
}

function handleMouseEvent(event: UiohookMouseEvent, type: 'mousedown' | 'mouseup') {
	const mainWindow = getMainWindow();
	if (!mainWindow) return;

	const button = event.button;

	mainWindow.webContents.send('global-mouse-event', {
		type,
		button,
	});

	for (const [id, keybind] of registeredKeybinds) {
		if (keybind.mouseButton === button) {
			mainWindow.webContents.send('global-keybind-triggered', {
				id,
				type: type === 'mousedown' ? 'keydown' : 'keyup',
			});
		}
	}
}

async function startHook(): Promise<boolean> {
	if (hookStarted) return true;

	try {
		uIOhook.on('keydown', (event) => handleKeyEvent(event, 'keydown'));
		uIOhook.on('keyup', (event) => handleKeyEvent(event, 'keyup'));
		uIOhook.on('mousedown', (event) => handleMouseEvent(event, 'mousedown'));
		uIOhook.on('mouseup', (event) => handleMouseEvent(event, 'mouseup'));

		uIOhook.start();
		hookStarted = true;
		return true;
	} catch (error) {
		logger.error('Failed to start:', error);
		return false;
	}
}

const INPUT_MONITORING_PERMISSION = 'input-monitoring';
const INPUT_MONITORING_STATUS_ALLOWLIST = new Set(['authorized', 'not-determined']);

function getInputMonitoringStatus(): string | null {
	const permissionsModule = (() => {
		try {
			return requireModule('node-mac-permissions');
		} catch (error) {
			logger.error('Failed to load node-mac-permissions:', error);
			return null;
		}
	})();

	if (!permissionsModule || typeof permissionsModule.getAuthStatus !== 'function') {
		return null;
	}

	try {
		return permissionsModule.getAuthStatus(INPUT_MONITORING_PERMISSION);
	} catch (error) {
		logger.error('Failed to query Input Monitoring auth status:', error);
		return null;
	}
}

async function checkInputMonitoringAccess(): Promise<boolean> {
	if (process.platform !== 'darwin') {
		return true;
	}

	if (hookStarted) {
		return true;
	}

	const status = getInputMonitoringStatus();
	if (status === null) {
		return true;
	}

	if (INPUT_MONITORING_STATUS_ALLOWLIST.has(status)) {
		return true;
	}

	logger.warn('Input Monitoring access denied, status:', status);
	return false;
}

function stopHook(): void {
	if (!hookStarted || !uIOhook) return;

	try {
		uIOhook.stop();
		hookStarted = false;
	} catch (error) {
		logger.error('Failed to stop:', error);
	}
}

export function registerGlobalKeyHookHandlers(): void {
	ipcMain.handle('global-key-hook-start', async (): Promise<boolean> => {
		return startHook();
	});

	ipcMain.handle('global-key-hook-stop', (): void => {
		stopHook();
	});

	ipcMain.handle('global-key-hook-is-running', (): boolean => {
		return hookStarted;
	});

	ipcMain.handle('check-input-monitoring-access', async (): Promise<boolean> => {
		return checkInputMonitoringAccess();
	});

	ipcMain.handle(
		'global-key-hook-register',
		(
			_event,
			options: {
				id: string;
				keycode?: number;
				mouseButton?: number;
				ctrl?: boolean;
				alt?: boolean;
				shift?: boolean;
				meta?: boolean;
			},
		): void => {
			registeredKeybinds.set(options.id, {
				id: options.id,
				keycode: options.keycode ?? 0,
				mouseButton: options.mouseButton,
				modifiers: {
					ctrl: options.ctrl ?? false,
					alt: options.alt ?? false,
					shift: options.shift ?? false,
					meta: options.meta ?? false,
				},
			});
		},
	);

	ipcMain.handle('global-key-hook-unregister', (_event, id: string): void => {
		registeredKeybinds.delete(id);
	});

	ipcMain.handle('global-key-hook-unregister-all', (): void => {
		registeredKeybinds.clear();
	});
}

export function cleanupGlobalKeyHook(): void {
	stopHook();
	registeredKeybinds.clear();
	activeKeys.clear();
}
