import path from 'node:path';
import os from 'node:os';
import { spawn, type ChildProcess, execFileSync } from 'node:child_process';

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

export type ProtonInstall = {
	/** Display name, e.g. "Proton 9.0" or "GE-Proton9-22". */
	name: string;
	/** Directory containing the `proton` launcher script. */
	path: string;
};

const steamRoots = (): string[] => {
	const home = os.homedir();
	return [
		path.join(home, '.local', 'share', 'Steam'),
		path.join(home, '.steam', 'steam'),
		path.join(home, '.steam', 'root'),
		path.join(
			home,
			'.var',
			'app',
			'com.valvesoftware.Steam',
			'.local',
			'share',
			'Steam'
		),
		path.join(
			home,
			'.var',
			'app',
			'com.valvesoftware.Steam',
			'data',
			'Steam'
		)
	];
};

const isProtonDir = async (dir: string): Promise<boolean> => {
	try {
		const proton = path.join(dir, 'proton');
		const st = await fs.stat(proton);
		return st.isFile();
	} catch {
		return false;
	}
};

const scanCommon = async (steamRoot: string): Promise<ProtonInstall[]> => {
	const common = path.join(steamRoot, 'steamapps', 'common');
	if (!(await fs.pathExists(common))) return [];

	const entries = await fs.readdir(common);
	const found: ProtonInstall[] = [];
	for (const name of entries) {
		if (!/^Proton/i.test(name)) continue;
		const dir = path.join(common, name);
		if (await isProtonDir(dir)) found.push({ name, path: dir });
	}
	return found;
};

const scanCompatTools = async (steamRoot: string): Promise<ProtonInstall[]> => {
	const toolsDir = path.join(steamRoot, 'compatibilitytools.d');
	if (!(await fs.pathExists(toolsDir))) return [];

	const entries = await fs.readdir(toolsDir);
	const found: ProtonInstall[] = [];
	for (const name of entries) {
		const dir = path.join(toolsDir, name);
		if (await isProtonDir(dir)) found.push({ name, path: dir });
	}
	return found;
};

/** Unique existing Steam roots (`.steam/steam` etc. are usually symlinks). */
const uniqueSteamRoots = async (): Promise<string[]> => {
	const seen = new Set<string>();
	const roots: string[] = [];
	for (const root of steamRoots()) {
		if (!(await fs.pathExists(root))) continue;
		let real: string;
		try {
			real = await fs.realpath(root);
		} catch {
			real = root;
		}
		if (seen.has(real)) continue;
		seen.add(real);
		roots.push(real);
	}
	return roots;
};

/**
 * Discover installed Proton versions under common Steam paths.
 *
 * @example
 * ```ts
 * const versions = await listProtonVersions();
 * // [{ name: 'Proton 9.0', path: '/home/…/steamapps/common/Proton 9.0' }, …]
 * ```
 */
export const listProtonVersions = async (): Promise<ProtonInstall[]> => {
	if (os.platform() !== 'linux') return [];

	const byRealPath = new Map<string, ProtonInstall>();
	for (const root of await uniqueSteamRoots()) {
		for (const install of [
			...(await scanCommon(root)),
			...(await scanCompatTools(root))
		]) {
			let real: string;
			try {
				real = await fs.realpath(install.path);
			} catch {
				real = install.path;
			}
			if (byRealPath.has(real)) continue;
			byRealPath.set(real, { name: install.name, path: real });
		}
	}

	return [...byRealPath.values()].sort((a, b) =>
		b.name.localeCompare(a.name, undefined, { numeric: true })
	);
};

const resolveSteamClientPath = async (): Promise<string | undefined> => {
	const roots = await uniqueSteamRoots();
	return roots[0];
};

export type ProtonLaunchOptions = {
	protonDir: string;
	exePath: string;
	args?: string[];
	cwd: string;
	env?: NodeJS.ProcessEnv;
};

/** Env that ties child windows to the Electron/AppImage launcher on Linux DEs. */
const LAUNCHER_IDENTITY_ENV = [
	'BAMF_DESKTOP_FILE_HINT',
	'DESKTOP_STARTUP_ID',
	'GIO_LAUNCHED_DESKTOP_FILE',
	'GIO_LAUNCHED_DESKTOP_FILE_PID',
	'XDG_ACTIVATION_TOKEN',
	// AppImage — Plasma groups children that still carry these as the AppImage app
	'APPIMAGE',
	'APPDIR',
	'OWD',
	'ARGV0',
	'APPIMAGE_EXTRACT_AND_RUN',
	'APPIMAGE_SILENT_MESSAGE'
] as const;

/**
 * Env for a game process that should not inherit the launcher's taskbar identity.
 */
export const gameLaunchEnv = (
	extra: NodeJS.ProcessEnv = {}
): NodeJS.ProcessEnv => {
	const env: NodeJS.ProcessEnv = { ...process.env, ...extra };
	for (const key of LAUNCHER_IDENTITY_ENV) delete env[key];
	return env;
};

const hasSystemdRun = (): boolean => {
	try {
		execFileSync('systemd-run', ['--version'], { stdio: 'ignore' });
		return true;
	} catch {
		return false;
	}
};

/**
 * Spawn outside Electron's process/app scope so Plasma/GNOME don't paint the
 * game with OctoLauncher's icon.
 *
 * Prefers `systemd-run --user --scope` (new cgroup), else `setsid`.
 */
export const spawnDetachedGame = (
	command: string,
	args: string[],
	opts: { cwd: string; env: NodeJS.ProcessEnv }
): ChildProcess => {
	const { cwd, env } = opts;

	if (hasSystemdRun()) {
		const unit = `octolauncher-game-${process.pid}-${Date.now()}`;
		Logger.log(`Launching via systemd-run scope (${unit})`);
		const child = spawn(
			'systemd-run',
			[
				'--user',
				'--scope',
				'--collect',
				`--unit=${unit}`,
				`--working-directory=${cwd}`,
				command,
				...args
			],
			{ env, detached: true, stdio: 'ignore' }
		);
		child.unref();
		return child;
	}

	Logger.log('Launching via setsid (systemd-run unavailable)');
	const child = spawn('setsid', ['--fork', command, ...args], {
		env,
		cwd,
		detached: true,
		stdio: 'ignore'
	});
	child.unref();
	return child;
};

/**
 * Register a .desktop entry so the DE matches Wine's WM_CLASS to WoW, not us.
 * Wine typically reports class `wow.exe` / `WoW.exe`.
 */
export const ensureWowDesktopEntry = async (
	clientDir: string
): Promise<void> => {
	if (os.platform() !== 'linux') return;

	const appsDir = path.join(os.homedir(), '.local', 'share', 'applications');
	await fs.ensureDir(appsDir);
	const desktopPath = path.join(appsDir, 'octolauncher-wow.desktop');

	// Prefer a client-local icon if present; else omit (Wine supplies _NET_WM_ICON).
	const iconCandidates = [
		path.join(clientDir, 'Wow.ico'),
		path.join(clientDir, 'WoW.ico'),
		path.join(clientDir, 'wow.ico'),
		path.join(clientDir, 'Wow.png'),
		path.join(clientDir, 'WoW.png')
	];
	let iconLine = '';
	for (const p of iconCandidates) {
		if (await fs.pathExists(p)) {
			iconLine = `Icon=${p}\n`;
			break;
		}
	}

	const body =
		'[Desktop Entry]\n' +
		'Type=Application\n' +
		'Name=World of Warcraft\n' +
		'Comment=Launched via OctoLauncher\n' +
		'Exec=true\n' +
		'Terminal=false\n' +
		'NoDisplay=true\n' +
		'StartupNotify=false\n' +
		'StartupWMClass=wow.exe\n' +
		iconLine;

	await fs.writeFile(desktopPath, body, 'utf8');
	try {
		execFileSync('update-desktop-database', [appsDir], { stdio: 'ignore' });
	} catch {
		/* optional */
	}
};

/**
 * Spawn `proton run <exe> …` with the STEAM_COMPAT_* env Proton expects
 * for non-Steam titles.
 */
export const spawnWithProton = async ({
	protonDir,
	exePath,
	args = [],
	cwd,
	env = {}
}: ProtonLaunchOptions): Promise<ChildProcess> => {
	const protonBin = path.join(protonDir, 'proton');
	if (!(await fs.pathExists(protonBin)))
		throw new Error(`Proton launcher not found at ${protonBin}`);

	const steamClient =
		(await resolveSteamClientPath()) ?? path.dirname(protonDir);
	const compatData = path.join(app.getPath('userData'), 'proton-prefix');
	await fs.ensureDir(compatData);

	const protonEnv = gameLaunchEnv({
		...env,
		STEAM_COMPAT_CLIENT_INSTALL_PATH: steamClient,
		STEAM_COMPAT_DATA_PATH: compatData
	});

	Logger.log(
		`Launching via Proton (${path.basename(protonDir)}): ${protonBin} run ${exePath}`
	);

	return spawnDetachedGame(protonBin, ['run', exePath, ...args], {
		cwd,
		env: protonEnv
	});
};
