import process from 'node:process';
import path from 'node:path';
import {fileURLToPath} from 'node:url';
import childProcess from 'node:child_process';
import fs, {constants as fsConstants} from 'node:fs/promises';
import {
	isWsl,
	powerShellPath,
	convertWslPathToWindows,
	canAccessPowerShell,
	wslDefaultBrowser,
} from 'wsl-utils';
import {executePowerShell} from 'powershell-utils';
import defineLazyProperty from 'define-lazy-prop';
import defaultBrowser, {_windowsBrowserProgIdMap} from 'default-browser';
import isInsideContainer from 'is-inside-container';
import isInSsh from 'is-in-ssh';

const fallbackAttemptSymbol = Symbol('fallbackAttempt');

// Path to included `xdg-open`.
const __dirname = import.meta.url ? path.dirname(fileURLToPath(import.meta.url)) : '';
const localXdgOpenPath = path.join(__dirname, 'xdg-open');

const {platform, arch} = process;

const tryEachApp = async (apps, opener) => {
	if (apps.length === 0) {
		// No app was provided
		return;
	}

	const errors = [];

	for (const app of apps) {
		try {
			return await opener(app); // eslint-disable-line no-await-in-loop
		} catch (error) {
			errors.push(error);
		}
	}

	throw new AggregateError(errors, 'Failed to open in all supported apps');
};

// eslint-disable-next-line complexity
const baseOpen = async options => {
	options = {
		wait: false,
		background: false,
		newInstance: false,
		allowNonzeroExitCode: false,
		...options,
	};

	const isFallbackAttempt = options[fallbackAttemptSymbol] === true;
	delete options[fallbackAttemptSymbol];

	if (Array.isArray(options.app)) {
		return tryEachApp(options.app, singleApp => baseOpen({
			...options,
			app: singleApp,
			[fallbackAttemptSymbol]: true,
		}));
	}

	let {name: app, arguments: appArguments = []} = options.app ?? {};
	appArguments = [...appArguments];

	if (Array.isArray(app)) {
		return tryEachApp(app, appName => baseOpen({
			...options,
			app: {
				name: appName,
				arguments: appArguments,
			},
			[fallbackAttemptSymbol]: true,
		}));
	}

	if (app === 'browser' || app === 'browserPrivate') {
		// IDs from default-browser for macOS and windows are the same.
		// IDs are lowercased to increase chances of a match.
		const ids = {
			'com.google.chrome': 'chrome',
			'google-chrome.desktop': 'chrome',
			'com.brave.browser': 'brave',
			'org.mozilla.firefox': 'firefox',
			'firefox.desktop': 'firefox',
			'com.microsoft.msedge': 'edge',
			'com.microsoft.edge': 'edge',
			'com.microsoft.edgemac': 'edge',
			'microsoft-edge.desktop': 'edge',
			'com.apple.safari': 'safari',
		};

		// Incognito flags for each browser in `apps`.
		const flags = {
			chrome: '--incognito',
			brave: '--incognito',
			firefox: '--private-window',
			edge: '--inPrivate',
			// Safari doesn't support private mode via command line
		};

		let browser;
		if (isWsl) {
			const progId = await wslDefaultBrowser();
			const browserInfo = _windowsBrowserProgIdMap.get(progId);
			browser = browserInfo ?? {};
		} else {
			browser = await defaultBrowser();
		}

		if (browser.id in ids) {
			const browserName = ids[browser.id.toLowerCase()];

			if (app === 'browserPrivate') {
				// Safari doesn't support private mode via command line
				if (browserName === 'safari') {
					throw new Error('Safari doesn\'t support opening in private mode via command line');
				}

				appArguments.push(flags[browserName]);
			}

			return baseOpen({
				...options,
				app: {
					name: apps[browserName],
					arguments: appArguments,
				},
			});
		}

		throw new Error(`${browser.name} is not supported as a default browser`);
	}

	let command;
	const cliArguments = [];
	const childProcessOptions = {};

	// Determine if we should use Windows/PowerShell behavior in WSL.
	// We only use Windows integration if PowerShell is actually accessible.
	// This allows the package to work in sandboxed WSL environments where Windows access is restricted.
	let shouldUseWindowsInWsl = false;
	if (isWsl && !isInsideContainer() && !isInSsh && !app) {
		shouldUseWindowsInWsl = await canAccessPowerShell();
	}

	if (platform === 'darwin') {
		command = 'open';

		if (options.wait) {
			cliArguments.push('--wait-apps');
		}

		if (options.background) {
			cliArguments.push('--background');
		}

		if (options.newInstance) {
			cliArguments.push('--new');
		}

		if (app) {
			cliArguments.push('-a', app);
		}
	} else if (platform === 'win32' || shouldUseWindowsInWsl) {
		command = await powerShellPath();

		cliArguments.push(...executePowerShell.argumentsPrefix);

		if (!isWsl) {
			childProcessOptions.windowsVerbatimArguments = true;
		}

		// Convert WSL Linux paths to Windows paths
		if (isWsl && options.target) {
			options.target = await convertWslPathToWindows(options.target);
		}

		// Suppress PowerShell progress messages that are written to stderr
		const encodedArguments = ['$ProgressPreference = \'SilentlyContinue\';', 'Start'];

		if (options.wait) {
			encodedArguments.push('-Wait');
		}

		if (app) {
			encodedArguments.push(executePowerShell.escapeArgument(app));
			if (options.target) {
				appArguments.push(options.target);
			}
		} else if (options.target) {
			encodedArguments.push(executePowerShell.escapeArgument(options.target));
		}

		if (appArguments.length > 0) {
			appArguments = appArguments.map(argument => executePowerShell.escapeArgument(argument));
			encodedArguments.push('-ArgumentList', appArguments.join(','));
		}

		// Using Base64-encoded command, accepted by PowerShell, to allow special characters.
		options.target = executePowerShell.encodeCommand(encodedArguments.join(' '));

		if (!options.wait) {
			// PowerShell will keep the parent process alive unless stdio is ignored.
			childProcessOptions.stdio = 'ignore';
		}
	} else {
		if (app) {
			command = app;
		} else {
			// When bundled by Webpack, there's no actual package file path and no local `xdg-open`.
			const isBundled = !__dirname || __dirname === '/';

			// Check if local `xdg-open` exists and is executable.
			let exeLocalXdgOpen = false;
			try {
				await fs.access(localXdgOpenPath, fsConstants.X_OK);
				exeLocalXdgOpen = true;
			} catch {}

			const useSystemXdgOpen = process.versions.electron
				?? (platform === 'android' || isBundled || !exeLocalXdgOpen);
			command = useSystemXdgOpen ? 'xdg-open' : localXdgOpenPath;
		}

		if (appArguments.length > 0) {
			cliArguments.push(...appArguments);
		}

		if (!options.wait) {
			// `xdg-open` will block the process unless stdio is ignored
			// and it's detached from the parent even if it's unref'd.
			childProcessOptions.stdio = 'ignore';
			childProcessOptions.detached = true;
		}
	}

	if (platform === 'darwin' && appArguments.length > 0) {
		cliArguments.push('--args', ...appArguments);
	}

	// IMPORTANT: On macOS, the target MUST come AFTER '--args'.
	// When using --args, ALL following arguments are passed to the app.
	// Example: open -a "chrome" --args --incognito https://site.com
	// This passes BOTH --incognito AND https://site.com to Chrome.
	// Without this order, Chrome won't open in incognito. See #332.
	if (options.target) {
		cliArguments.push(options.target);
	}

	const subprocess = childProcess.spawn(command, cliArguments, childProcessOptions);

	if (options.wait) {
		return new Promise((resolve, reject) => {
			subprocess.once('error', reject);

			subprocess.once('close', exitCode => {
				if (!options.allowNonzeroExitCode && exitCode !== 0) {
					reject(new Error(`Exited with code ${exitCode}`));
					return;
				}

				resolve(subprocess);
			});
		});
	}

	// When we're in a fallback attempt, we need to detect launch failures before trying the next app.
	// Wait for the close event to check the exit code before unreffing.
	// The launcher (open/xdg-open/PowerShell) exits quickly (~10-30ms) even on success.
	if (isFallbackAttempt) {
		return new Promise((resolve, reject) => {
			subprocess.once('error', reject);

			subprocess.once('spawn', () => {
				// Keep error handler active for post-spawn errors
				subprocess.once('close', exitCode => {
					subprocess.off('error', reject);

					if (exitCode !== 0) {
						reject(new Error(`Exited with code ${exitCode}`));
						return;
					}

					subprocess.unref();
					resolve(subprocess);
				});
			});
		});
	}

	subprocess.unref();

	// Handle spawn errors before the caller can attach listeners.
	// This prevents unhandled error events from crashing the process.
	return new Promise((resolve, reject) => {
		subprocess.once('error', reject);

		// Wait for the subprocess to spawn before resolving.
		// This ensures the process is established before the caller continues,
		// preventing issues when process.exit() is called immediately after.
		subprocess.once('spawn', () => {
			subprocess.off('error', reject);
			resolve(subprocess);
		});
	});
};

const open = (target, options) => {
	if (typeof target !== 'string') {
		throw new TypeError('Expected a `target`');
	}

	return baseOpen({
		...options,
		target,
	});
};

export const openApp = (name, options) => {
	if (typeof name !== 'string' && !Array.isArray(name)) {
		throw new TypeError('Expected a valid `name`');
	}

	const {arguments: appArguments = []} = options ?? {};
	if (appArguments !== undefined && appArguments !== null && !Array.isArray(appArguments)) {
		throw new TypeError('Expected `appArguments` as Array type');
	}

	return baseOpen({
		...options,
		app: {
			name,
			arguments: appArguments,
		},
	});
};

function detectArchBinary(binary) {
	if (typeof binary === 'string' || Array.isArray(binary)) {
		return binary;
	}

	const {[arch]: archBinary} = binary;

	if (!archBinary) {
		throw new Error(`${arch} is not supported`);
	}

	return archBinary;
}

function detectPlatformBinary({[platform]: platformBinary}, {wsl} = {}) {
	if (wsl && isWsl) {
		return detectArchBinary(wsl);
	}

	if (!platformBinary) {
		throw new Error(`${platform} is not supported`);
	}

	return detectArchBinary(platformBinary);
}

export const apps = {
	browser: 'browser',
	browserPrivate: 'browserPrivate',
};

defineLazyProperty(apps, 'chrome', () => detectPlatformBinary({
	darwin: 'google chrome',
	win32: 'chrome',
	// `chromium-browser` is the older deb package name used by Ubuntu/Debian before snap.
	linux: ['google-chrome', 'google-chrome-stable', 'chromium', 'chromium-browser'],
}, {
	wsl: {
		ia32: '/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe',
		x64: ['/mnt/c/Program Files/Google/Chrome/Application/chrome.exe', '/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe'],
	},
}));

defineLazyProperty(apps, 'brave', () => detectPlatformBinary({
	darwin: 'brave browser',
	win32: 'brave',
	linux: ['brave-browser', 'brave'],
}, {
	wsl: {
		ia32: '/mnt/c/Program Files (x86)/BraveSoftware/Brave-Browser/Application/brave.exe',
		x64: ['/mnt/c/Program Files/BraveSoftware/Brave-Browser/Application/brave.exe', '/mnt/c/Program Files (x86)/BraveSoftware/Brave-Browser/Application/brave.exe'],
	},
}));

defineLazyProperty(apps, 'firefox', () => detectPlatformBinary({
	darwin: 'firefox',
	win32: String.raw`C:\Program Files\Mozilla Firefox\firefox.exe`,
	linux: 'firefox',
}, {
	wsl: '/mnt/c/Program Files/Mozilla Firefox/firefox.exe',
}));

defineLazyProperty(apps, 'edge', () => detectPlatformBinary({
	darwin: 'microsoft edge',
	win32: 'msedge',
	linux: ['microsoft-edge', 'microsoft-edge-dev'],
}, {
	wsl: '/mnt/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe',
}));

defineLazyProperty(apps, 'safari', () => detectPlatformBinary({
	darwin: 'Safari',
}));

export default open;
