import * as fs from 'node:fs';
import * as path from 'node:path';
import colors from 'kleur';
import chokidar from 'chokidar';
import { preprocess } from 'svelte/compiler';
import { copy, mkdirp, posixify, rimraf } from './filesystem.js';
import {
	analyze,
	resolve_aliases,
	resolve_ts_endings,
	scan,
	strip_lang_tags,
	write
} from './utils.js';
import { emit_dts, load_tsconfig, transpile_ts } from './typescript.js';
import { create_validator } from './validate.js';

/**
 * @param {import('./types.js').Options} options
 */
export async function build(options) {
	const { analyse_code, validate } = create_validator(options);
	await do_build(options, analyse_code);
	validate();
}

/**
 * @param {import('./types.js').Options} options
 * @param {(name: string, code: string) => void} analyse_code
 */
async function do_build(options, analyse_code) {
	const { input, output, temp, extensions, alias, tsconfig } = normalize_options(options);

	if (!fs.existsSync(input)) {
		throw new Error(`${path.relative('.', input)} does not exist`);
	}

	rimraf(temp);
	mkdirp(temp);

	const files = scan(input, extensions);

	if (options.types) {
		await emit_dts(input, temp, output, options.cwd, alias, files, tsconfig);
	}

	/** @type {Map<string, import('typescript').CompilerOptions>} */
	const tsconfig_cache = new Map();

	for (const file of files) {
		await process_file(
			input,
			temp,
			file,
			options.config.preprocess,
			alias,
			tsconfig,
			analyse_code,
			tsconfig_cache
		);
	}

	if (!options.preserve_output) {
		rimraf(output);
	}

	mkdirp(output);
	copy(temp, output);

	console.log(
		colors
			.bold()
			.green(`${path.relative(options.cwd, input)} -> ${path.relative(options.cwd, output)}`)
	);
}

/**
 * @param {import('./types.js').Options} options
 */
export async function watch(options) {
	const { analyse_code, validate } = create_validator(options);

	await do_build(options, analyse_code);

	validate();

	const { input, output, extensions, alias, tsconfig } = normalize_options(options);

	const message = `\nWatching ${path.relative(options.cwd, input)} for changes...\n`;

	console.log(message);

	/** @type {Array<{ file: import('./types.js').File, type: string }>} */
	const pending = [];

	/** @type {Array<(value?: any) => void>} */
	const fulfillers = [];

	/** @type {NodeJS.Timeout} */
	let timeout;

	/** @type {Map<string, import('typescript').CompilerOptions>} */
	const tsconfig_cache = new Map();

	const watcher = chokidar.watch(input, { ignoreInitial: true });
	/** @type {Promise<void>} */
	const ready = new Promise((resolve) => watcher.on('ready', resolve));

	watcher.on('all', (type, filepath) => {
		const file = analyze(path.relative(input, filepath), extensions);

		pending.push({ file, type });

		if (
			file.name.endsWith('tsconfig.json') ||
			file.name.endsWith('jsconfig.json') ||
			(options.tsconfig && posixify(filepath) === posixify(options.tsconfig))
		) {
			tsconfig_cache.clear();
		}

		clearTimeout(timeout);
		timeout = setTimeout(async () => {
			const files = scan(input, extensions);

			const events = pending.slice();
			pending.length = 0;

			let errored = false;

			for (const { file, type } of events) {
				if (type === 'unlink') {
					for (const candidate of [
						file.name,
						`${file.base}.d.ts`,
						`${file.base}.d.mts`,
						`${file.base}.d.cts`
					]) {
						const resolved = path.join(output, candidate);

						if (fs.existsSync(resolved)) {
							fs.unlinkSync(resolved);

							const parent = path.dirname(resolved);
							if (parent !== output && fs.readdirSync(parent).length === 0) {
								fs.rmdirSync(parent);
							}
						}
					}
					console.log(`Removed ${file.dest}`);
				}

				if (type === 'add' || type === 'change') {
					console.log(`Processing ${file.name}`);
					try {
						await process_file(
							input,
							output,
							file,
							options.config.preprocess,
							alias,
							tsconfig,
							analyse_code,
							tsconfig_cache
						);
					} catch (e) {
						errored = true;
						console.error(e);
					}
				}
			}

			if (!errored && options.types) {
				try {
					await emit_dts(input, output, output, options.cwd, alias, files, tsconfig);
					console.log('Updated .d.ts files');
				} catch (e) {
					errored = true;
					console.error(e);
				}
			}

			if (!errored) {
				validate();
			}

			console.log(message);

			fulfillers.forEach((fn) => fn());
		}, 100);
	});

	return {
		watcher,
		ready,
		settled: () =>
			new Promise((fulfil, reject) => {
				fulfillers.push(fulfil);
				setTimeout(() => reject(new Error('Timed out')), 1000);
			})
	};
}

/**
 * @param {import('./types.js').Options} options
 */
function normalize_options(options) {
	const input = path.resolve(options.cwd, options.input);
	const output = path.resolve(options.cwd, options.output);
	const preserve_output = options.preserve_output;
	const temp = path.resolve(
		options.cwd,
		options.config.kit?.outDir ?? '.svelte-kit',
		'__package__'
	);
	const extensions = options.config.extensions ?? ['.svelte'];
	const tsconfig = options.tsconfig ? path.resolve(options.cwd, options.tsconfig) : undefined;

	const alias = {
		$lib: path.resolve(options.cwd, options.config.kit?.files?.lib ?? 'src/lib'),
		...(options.config.kit?.alias ?? {})
	};

	return {
		input,
		output,
		preserve_output,
		temp,
		extensions,
		alias,
		tsconfig
	};
}

/**
 * @param {string} input
 * @param {string} output
 * @param {import('./types.js').File} file
 * @param {import('svelte/types/compiler/preprocess').PreprocessorGroup | undefined} preprocessor
 * @param {Record<string, string>} aliases
 * @param {string | undefined} tsconfig
 * @param {(name: string, code: string) => void} analyse_code
 * @param {Map<string, import('typescript').CompilerOptions>} tsconfig_cache
 */
async function process_file(
	input,
	output,
	file,
	preprocessor,
	aliases,
	tsconfig,
	analyse_code,
	tsconfig_cache
) {
	const filename = path.join(input, file.name);
	const dest = path.join(output, file.dest);

	if (file.is_svelte || file.name.endsWith('.ts') || file.name.endsWith('.js')) {
		let contents = fs.readFileSync(filename, 'utf-8');

		if (file.is_svelte) {
			if (preprocessor) {
				const preprocessed = (await preprocess(contents, preprocessor, { filename })).code;
				contents = strip_lang_tags(preprocessed);
			}
		}

		// _before_ transpilining TS, that way TS will apply rewriteRelativeImportExtensions
		// to these now-resolved path aliases, too
		contents = resolve_aliases(input, file.name, contents, aliases);

		if (file.name.endsWith('.ts') && !file.name.endsWith('.d.ts')) {
			contents = await transpile_ts(tsconfig, filename, contents, tsconfig_cache);
		} else if (file.is_svelte) {
			const options = await load_tsconfig(tsconfig, filename, tsconfig_cache);
			// Mimic TypeScript's transpileModule behavior for Svelte files
			if (options.rewriteRelativeImportExtensions) {
				contents = resolve_ts_endings(contents);
			}
		}

		analyse_code(file.name, contents);
		write(dest, contents);
	} else {
		copy(filename, dest);
	}
}
