import { execSync } from 'node:child_process';
import { adapters } from './adapters.js';
import path from 'node:path';
import fs from 'node:fs';
import process from 'node:process';

/**
 * @template T
 * @template {keyof T} K
 * @typedef {Partial<Omit<T, K>> & Required<Pick<T, K>>} PartialExcept
 */

/**
 * We use a custom `Builder` type here to support the minimum version of SvelteKit.
 * @typedef {PartialExcept<import('@sveltejs/kit').Builder, 'log' | 'rimraf' | 'mkdirp' | 'config' | 'prerendered' | 'routes' | 'createEntries' | 'generateFallback' | 'generateEnvModule' | 'generateManifest' | 'getBuildDirectory' | 'getClientDirectory' | 'getServerDirectory' | 'getAppPath' | 'writeClient' | 'writePrerendered' | 'writePrerendered' | 'writeServer' | 'copy' | 'compress'>} Builder2_0_0
 */

/** @type {Record<string, (name: string, version: string) => string>} */
const commands = {
	npm: (name, version) => `npm install -D ${name}@${version}`,
	pnpm: (name, version) => `pnpm add -D ${name}@${version}`,
	yarn: (name, version) => `yarn add -D ${name}@${version}`,
	bun: (name, version) => `bun add -D ${name}@${version}`,
	deno: (name, version) => `deno install -D npm:${name}@${version}`
};

function detect_lockfile() {
	let dir = process.cwd();

	/** @param {string} file */
	const exists = (file) => fs.existsSync(path.join(dir, file));

	do {
		if (exists('pnpm-lock.yaml')) return 'pnpm';
		if (exists('yarn.lock')) return 'yarn';
		if (exists('package-lock.json')) return 'npm';
		if (exists('bun.lockb') || exists('bun.lock')) return 'bun';
		if (exists('deno.lock')) return 'deno';
	} while (dir !== (dir = path.dirname(dir)));

	return 'npm';
}

function detect_package_manager() {
	const manager = detect_lockfile();

	try {
		execSync(`${manager} --version`);
		return manager;
	} catch {
		return 'npm';
	}
}

/**
 * Resolves a peer dependency relative to the current CWD. Duplicated with `packages/kit`
 * @param {string} dependency
 */
function resolve_peer(dependency) {
	let [name, ...parts] = dependency.split('/');
	if (name[0] === '@') name += `/${parts.shift()}`;

	let dir = process.cwd();

	while (!fs.existsSync(`${dir}/node_modules/${name}/package.json`)) {
		if (dir === (dir = path.dirname(dir))) {
			throw new Error(
				`Could not resolve peer dependency "${name}" relative to your project — please install it and try again.`
			);
		}
	}

	const pkg_dir = `${dir}/node_modules/${name}`;
	const pkg = JSON.parse(fs.readFileSync(`${pkg_dir}/package.json`, 'utf-8'));

	const subpackage = ['.', ...parts].join('/');

	let exported = pkg.exports[subpackage];

	while (typeof exported !== 'string') {
		if (!exported) {
			throw new Error(`Could not find valid "${subpackage}" export in ${name}/package.json`);
		}

		exported = exported['import'] ?? exported['default'];
	}

	return path.resolve(pkg_dir, exported);
}

/** @typedef {import('@sveltejs/kit').Adapter} Adapter */

/**
 * @returns {Promise<Adapter | undefined>} The corresponding adapter for the current environment if found otherwise undefined
 */
async function get_adapter() {
	const match = adapters.find((candidate) => candidate.test());

	if (!match) return;

	/** @type {string} */
	let resolved;

	try {
		resolved = resolve_peer(match.module);
	} catch {
		const package_manager = detect_package_manager();
		const command = commands[package_manager](match.module, match.version);

		try {
			console.log(`Installing ${match.module}...`);

			execSync(command, {
				stdio: 'inherit',
				env: {
					...process.env,
					NODE_ENV: undefined
				}
			});

			resolved = resolve_peer(match.module);

			console.log(`Successfully installed ${match.module}.`);
			console.warn(
				`\nIf you plan to continue deploying to ${match.name}, consider replacing @sveltejs/adapter-auto with ${match.module}. This will give you faster installs and more control over deployment configuration.\n`
			);
		} catch (e) {
			throw new Error(
				`Could not install ${match.module}. Please install it yourself by adding it to your package.json's devDependencies and try building your project again.`,
				{ cause: e }
			);
		}
	}

	/** @type {{ default: () => Adapter }} */
	const module = await import(resolved);

	const adapter = module.default();

	return {
		...adapter,
		adapt: (builder) => {
			builder.log.info(`Detected environment: ${match.name}. Using ${match.module}`);
			return adapter.adapt(builder);
		}
	};
}

/** @type {() => Adapter} */
export default () => ({
	name: '@sveltejs/adapter-auto',
	/** @param {Builder2_0_0} builder */
	adapt: async (builder) => {
		const adapter = await get_adapter();

		if (adapter) return adapter.adapt(/** @type {import('@sveltejs/kit').Builder} */ (builder));

		builder.log.warn(
			'Could not detect a supported production environment. See https://svelte.dev/docs/kit/adapters to learn how to configure your app to run on the platform of your choosing'
		);
	},
	supports: {
		read: () => {
			supports_error(
				'The read function imported from $app/server only works in certain environments'
			);
		},
		instrumentation: () => {
			supports_error('`instrumentation.server.js` only works in certain environments');
		}
	}
});

/**
 * @param {string} message
 * @returns {never}
 * @throws {Error}
 */
function supports_error(message) {
	throw new Error(
		`${message}. Since you're using @sveltejs/adapter-auto, SvelteKit cannot determine whether it will work when your app is deployed. Please replace it with an adapter tailored to your target environment.`
	);
}
