import path from 'path';

import { screen } from 'electron';
import fs from 'fs-extra';
import Logger from 'electron-log/main';

import Preferences from '~main/modules/preferences';
import { ConfigWtfSchema, type PreferencesSchema } from '~common/schemas';
import { isNotUndef } from '~common/utils';
import { readPristineWow } from '~main/modules/aria2';
import { enumerateDisplays } from '~main/modules/displays';

const Servers = {
	live: {
		realmList: 'octowow.st',
		patchList: 'octowow.st',
		realmName: 'OctoWoW'
	},
	ptr: {
		realmList: import.meta.env.MAIN_VITE_PTR_REALMLIST || 'octowow.st',
		patchList: import.meta.env.MAIN_VITE_PTR_REALMLIST || 'octowow.st',
		realmName: 'OctoWoW PTR'
	}
} as const;

const LOCALES = {
	enUS: { tag: 'enUS', index: 0 },
	deDE: { tag: 'deDE', index: 3 },
	zhCN: { tag: 'zhCN', index: 4 },
	ruRU: { tag: 'ruRU', index: 5 },
	esES: { tag: 'esES', index: 6 },
	ptBR: { tag: 'ptBR', index: 7 }
} as const satisfies Record<
	PreferencesSchema['locale'],
	{ tag: string; index: number }
>;

const LOCALE_NAMES = [
	'enUS',
	'koKR',
	'frFR',
	'deDE',
	'zhCN',
	'zhTW',
	'esES',
	'xxYY'
] as const;

const localeNameOffset = (index: number) => 0x45591c - index * 8;

const carrierName = (index: number) => LOCALE_NAMES[index];

type TweakKey =
	| { synthetic?: false; key: keyof PreferencesSchema['config'] }
	| { synthetic: true; key: string };

type Tweak = TweakKey & {
	default?: unknown;
	forced?: boolean;
} & (
		| {
				type: 'bytes';
				tweaks: [number, number[], number[]?][];
		  }
		| {
				type: 'int8' | 'uint16' | 'float';
				offset: number;
				value?: number;
		  }
	);

const hex = (bytes: number[]) =>
	bytes.map(b => b.toString(16).padStart(2, '0')).join(' ');

export const patchExecutable = async () => {
	Logger.log('Patching WoW.exe...');

	const { clientDir, config, locale } = Preferences.data;
	if (!clientDir) return;
	const exePath = path.join(clientDir, 'WoW.exe');

	try {
		Logger.log('Reading clean WoW.exe base...');
		const buffer = await readPristineWow(clientDir);

		const loc = LOCALES[locale];

		const Tweaks = [
			{
				key: 'largeAddress',
				type: 'uint16',
				offset: 0x126,
				value: buffer.readUint16LE(0x126) | 0x20,
				default: false
			},
			{ key: 'farClip', type: 'float', offset: 0x40fed8 },
			{
				key: 'fieldOfView',
				type: 'float',
				offset: 0x4089b4,
				value: (config.fieldOfView ?? 1) * (Math.PI / 180),
				default: 90
			},
			{ key: 'frillDistance', type: 'float', offset: 0x467958 },
			{
				key: 'soundInBackground',
				type: 'int8',
				offset: 0x3a4869,
				value: config.soundInBackground ? 0x27 : 0x14,
				default: false
			},
			{
				key: 'alwaysAutoLoot',
				type: 'bytes',
				tweaks: [
					[0x0c1ecf, [0x75]],
					[0x0c2b25, [0x75]]
				]
			},
			{ key: 'nameplateRange', type: 'float', offset: 0x40c448 },
			{ key: 'cameraDistance', type: 'float', offset: 0x4089a4 },
			{
				synthetic: true,
				key: 'skillUiGateHijack',
				type: 'bytes',
				default: true,
				forced: true,
				tweaks: [
					[
						0x002ddf90,
						[
							0x55, 0x8b, 0xec, 0x83, 0xec, 0x08, 0x53, 0x56, 0x57, 0x8b, 0x3d,
							0x60, 0xab, 0xce, 0x00, 0x83, 0xff, 0xff, 0x89, 0x55, 0xfc, 0x89,
							0x4d, 0xf8, 0x74, 0x79, 0x8b, 0x75, 0x08, 0x8b, 0x15, 0x58, 0xab,
							0xce, 0x00, 0x8b, 0xc7, 0x23, 0xc6, 0x8d, 0x04, 0x40, 0x8b, 0x4c,
							0x82, 0x08, 0xf6, 0xc1, 0x01, 0x8d, 0x44, 0x82, 0x04, 0x75, 0x04,
							0x85, 0xc9, 0x75, 0x05, 0x33, 0xc9, 0x8d, 0x49, 0x00, 0xf6, 0xc1,
							0x01, 0x75, 0x4e, 0x85, 0xc9, 0x74, 0x4a, 0x39, 0x31, 0x74, 0x13,
							0x8b, 0xc7, 0x23, 0xc6, 0x8d, 0x04, 0x40, 0x8d, 0x04, 0x82, 0x8b,
							0x00, 0x03, 0xc1, 0x8b, 0x48, 0x04, 0xeb, 0xe0, 0x8b, 0x59, 0x1c,
							0x8b, 0x71, 0x18, 0x33, 0xff, 0x85, 0xdb, 0x7e, 0x27, 0x8d, 0x64,
							0x24, 0x00, 0x8b, 0x4e, 0x0c, 0x8b, 0x56, 0x08, 0x6a, 0x00, 0x6a,
							0x00, 0x51, 0x8b, 0x4d, 0xf8, 0x52, 0x8b, 0x55, 0xfc, 0xe8, 0xb9,
							0xfd, 0xff, 0xff, 0x84, 0xc0, 0x75, 0x13, 0x47, 0x83, 0xc6, 0x20,
							0x3b, 0xfb, 0x7c, 0xdd, 0x5f, 0x5e, 0x33, 0xc0, 0x5b, 0x8b, 0xe5,
							0x5d, 0xc2, 0x04, 0x00, 0x5f, 0x8b, 0xc6, 0x5e, 0x5b, 0x8b, 0xe5,
							0x5d, 0xc2, 0x04, 0x00, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90
						]
					]
				]
			},
			{
				synthetic: true,
				key: 'octowowUrlAllowlist',
				type: 'bytes',
				default: true,
				forced: true,
				tweaks: [
					[
						0x45ccd8,
						[
							0x6f, 0x63, 0x74, 0x6f, 0x77, 0x6f, 0x77, 0x2e, 0x73, 0x74, 0x00,
							0x00, 0x00, 0x00, 0x00, 0x00
						]
					]
				]
			},
			{
				synthetic: true,
				key: 'localeTag',
				type: 'bytes',
				default: true,
				forced: true,
				tweaks: [
					[
						0x1b2115,
						[0xb8, ...Buffer.from(carrierName(loc.index), 'latin1').reverse()],
						[0xa1, 0xa4, 0xa2, 0xc2, 0x00]
					]
				]
			},
			{
				synthetic: true,
				key: 'localeIndex',
				type: 'bytes',
				default: true,
				forced: true,
				tweaks: [
					[
						0x253c,
						[0xbe, loc.index, 0x00, 0x00, 0x00, 0xeb, 0x1f],
						[0x33, 0xf6, 0x8b, 0xff, 0x8b, 0x04, 0xb5]
					]
				]
			},
			{
				synthetic: true,
				key: 'localeName',
				type: 'bytes',
				default: true,
				forced: true,
				tweaks: [
					[
						localeNameOffset(loc.index),
						[...Buffer.from(loc.tag, 'latin1')],
						[...Buffer.from(LOCALE_NAMES[loc.index], 'latin1')]
					]
				]
			}
		] satisfies Tweak[];

		Tweaks.forEach(t => {
			const val = t.synthetic
				? t.default
				: config[t.key] ?? t.default ?? ConfigWtfSchema.parse({})[t.key];

			Logger.log(`Applying "${t.key}" patch with value: ${val}`);
			if (t.type === 'float') {
				buffer.writeFloatLE(t.value ?? (val as number), t.offset);
			} else if (t.type === 'int8') {
				buffer.writeInt8(t.value ?? (val as number), t.offset);
			} else if (t.type === 'uint16') {
				if (!t.forced && !val) return;
				buffer.writeUInt16LE(t.value ?? (val as number), t.offset);
			} else if (t.type === 'bytes') {
				if (!t.forced && !val) return;
				t.tweaks.forEach(
					([offset, bytes, expect]: [number, number[], number[]?]) => {
						if (expect) {
							const current = buffer.subarray(offset, offset + expect.length);
							if (current.equals(Buffer.from(bytes))) return;
							if (!current.equals(Buffer.from(expect)))
								throw new Error(
									`"${t.key}" expected [${hex(expect)}] at 0x${offset.toString(
										16
									)} ` +
										`but found [${hex([
											...current
										])}]; refusing to patch WoW.exe`
								);
						}
						const written = Buffer.from(bytes).copy(buffer, offset);
						if (written !== bytes.length)
							Logger.error(
								`"${t.key}" wrote ${written}/${bytes.length} bytes at ` +
									`0x${offset.toString(16)}: past end of file (${
										buffer.length
									} bytes). ` +
									'This tweak is a no-op; the offset is probably a virtual address.'
							);
					}
				);
			}
		});

		await fs.writeFile(exePath, buffer);
		Preferences.data = { patchedLocale: locale };
		Logger.log(`WoW.exe successfully patched (language: ${locale})`);
	} catch (e) {
		Logger.error('Failed to patch WoW.exe', e);
		throw e instanceof Error ? e : new Error('Failed to patch WoW.exe');
	}
};

const repairResolution = async (
	clientDir: string,
	current: string | undefined,
	lastWritten: string | undefined
): Promise<{ gxResolution?: string }> => {
	const devices = await enumerateDisplays();
	if (!devices?.length) return {};

	const pinnedRaw = await fs
		.readFile(path.join(clientDir, 'VMMFix_preferred_monitor.txt'), 'utf8')
		.then(s => s.trim())
		.catch(() => '');
	const pinned = pinnedRaw ? Number(pinnedRaw) : NaN;
	const target =
		devices.find(d => d.index === pinned && d.attached) ??
		devices.find(d => d.primary && d.attached);
	if (!target?.modes.length) return {};

	const native = `${target.width}x${target.height}`;
	if (!target.modes.includes(native)) return {};

	let owned = !current || current === lastWritten;

	if (!owned && lastWritten === undefined && current) {
		const width = Number(current.split('x')[0]);
		if (Number.isFinite(width) && width * 2 < target.width) {
			Logger.warn(
				`gxResolution ${current} is far below ${target.deviceName}'s ${native} and predates resolution tracking; treating it as a client fallback`
			);
			owned = true;
		}
	}

	if (!owned) return {};
	if (current === native) return {};

	Logger.warn(
		`gxResolution ${current ?? '<unset>'} is launcher-owned; correcting to ${
			target.deviceName
		}'s ${native}`
	);
	return { gxResolution: native };
};

const applyRealmlist = async (clientDir: string, host: string) => {
	const body = `set realmlist "${host}"\n`;
	const write = async (target: string) => {
		const tmp = `${target}.tmp`;
		try {
			await fs.writeFile(tmp, body);
			await fs.move(tmp, target, { overwrite: true });
		} catch (e) {
			await fs.remove(tmp).catch(() => undefined);
			throw e;
		}
	};
	await write(path.join(clientDir, 'realmlist.wtf'));
	const dataDir = path.join(clientDir, 'Data');
	if (await fs.pathExists(dataDir))
		for (const entry of await fs.readdir(dataDir)) {
			const scoped = path.join(dataDir, entry, 'realmlist.wtf');
			if (!(await fs.pathExists(scoped))) continue;
			try {
				await write(scoped);
			} catch (e) {
				Logger.warn(`Could not rewrite ${scoped}: ${String(e)}`);
			}
		}
};

export const patchConfig = async (forceTweaks = false) => {
	const { clientDir, config, locale } = Preferences.data;
	if (!clientDir) return;

	const server: keyof typeof Servers = import.meta.env.MAIN_VITE_PTR_REALMLIST
		? 'ptr'
		: 'live';

	const configPath = path.join(clientDir, 'WTF', 'Config.wtf');
	await fs.ensureDir(path.dirname(configPath));
	const raw = (await fs.pathExists(configPath))
		? await fs.readFile(configPath, { encoding: 'utf-8' })
		: '';

	const configWtf = Object.fromEntries(
		raw
			.split(/\r?\n/)
			.map(l => {
				const [, k, v] = l.match(/SET (\w+) "(.*)"/) ?? [];
				return !k || v === undefined ? undefined : [k, v];
			})
			.filter(isNotUndef)
	);

	const isFirstRun = Object.keys(configWtf).length === 0;

	const primaryDisplay = screen.getPrimaryDisplay();
	const scale = primaryDisplay.scaleFactor || 1;
	const width = Math.round(primaryDisplay.bounds.width * scale);
	const height = Math.round(primaryDisplay.bounds.height * scale);

	const seededResolution = `${width}x${height}`;

	const seed = isFirstRun
		? {
				scriptMemory: 512000,
				gxResolution: seededResolution,
				gxColorBits: primaryDisplay.colorDepth,
				gxDepthBits: primaryDisplay.colorDepth,
				gxRefresh: 60,
				gxMultisample: 8,
				gxMultisampleQuality: 0,
				gxTripleBuffer: 1,
				anisotropic: 16,
				frillDensity: 48,
				fullAlpha: 1,
				SmallCull: 0.01,
				DistCull: 888.8,
				shadowLevel: 0,
				trilinear: 1,
				specular: 1,
				pixelShaders: 1,
				M2UsePixelShaders: 1,
				M2UseShaders: 1,
				particleDensity: 1,
				unitDrawDist: 300,
				weatherDensity: 3,
				movieSubtitle: 1,
				minimapZoom: 0,
				minimapInsideZoom: 0,
				SoundZoneMusicNoDelay: 1,
				gxWindow: 1,
				gxMaximize: 1,
				gxCursor: 1,
				checkAddonVersion: 0,
				farClip: config.farClip,
				CameraDistanceMax: config.cameraDistance,
				patchList: Servers[server].patchList,
				realmName: Servers[server].realmName
		  }
		: {};

	const owned = {
		locale: carrierName(LOCALES[locale].index),
		patchList: configWtf['patchList'] ?? Servers[server].patchList,
		realmName: configWtf['realmName'] ?? Servers[server].realmName,
		hwDetect: 0,
		BackgroundSound: config.soundInBackground ? 1 : 0
	};

	const repaired = await repairResolution(
		clientDir,
		configWtf['gxResolution'],
		Preferences.data.lastWrittenResolution
	);

	const parsed = {
		...seed,
		...configWtf,
		...repaired,
		...owned,
		...(forceTweaks
			? { farClip: config.farClip, CameraDistanceMax: config.cameraDistance }
			: {})
	};

	const body = Object.entries(parsed)
		.filter(v => v[1] !== undefined && v[1] !== null)
		.filter(([k]) => !/^realmlist$/i.test(k))
		.map(l => `SET ${l[0]} "${l[1]}"`)
		.join('\n');
	const tmpPath = `${configPath}.tmp`;
	await fs.writeFile(tmpPath, body);
	await fs.move(tmpPath, configPath, { overwrite: true });

	await applyRealmlist(clientDir, Servers[server].realmList);

	const chosen =
		repaired.gxResolution ?? (isFirstRun ? seededResolution : undefined);
	if (chosen && chosen !== Preferences.data.lastWrittenResolution)
		Preferences.data = { lastWrittenResolution: chosen };

	Logger.log('Config.wtf successfully patched');
};

export const ensureDxvkConf = async (clientDir: string) => {
	if (!(await fs.pathExists(path.join(clientDir, 'd3d9.dll')))) return;
	const confPath = path.join(clientDir, 'dxvk.conf');
	if (await fs.pathExists(confPath)) return;
	await fs.writeFile(
		confPath,
		[
			'# Cap the texture memory the 32-bit client believes it has so it cannot',
			'# over-commit its address space (the common DXVK out-of-memory crash).',
			'd3d9.maxAvailableMemory = 2048',
			'd3d9.maxFrameLatency = 1',
			'dxvk.numCompilerThreads = 2',
			'dxvk.logLevel = none',
			''
		].join('\n')
	);
	Logger.log('Wrote dxvk.conf');
};
