#!/usr/bin/env node
import { spawn } from 'child_process';
import { watch } from 'fs';
import { existsSync } from 'fs';
import { dirname, resolve } from 'path';
import { fileURLToPath } from 'url';
import treeKill from 'tree-kill';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

// Configuration
const CE_TYPINGS_FLAG_PATH = resolve(__dirname, '../../meet-ce/typings/dist/typings-ready.flag');
const CE_TYPINGS_DIST = resolve(__dirname, '../../meet-ce/typings/dist');
const PRO_TYPINGS_FLAG_PATH = resolve(__dirname, '../../meet-pro/typings/dist/typings-ready.flag');
const PRO_TYPINGS_DIST = resolve(__dirname, '../../meet-pro/typings/dist');
const DEBOUNCE_MS = 500; // Wait 500ms after flag appears before restarting
const KILL_TIMEOUT_MS = 5000; // Max time to wait for process to die
const POLL_DIR_MS = 1000; // Polling interval for directories that don't yet exist

// Get command from arguments
const command = process.argv.slice(2).join(' ');

if (!command) {
	console.error('❌ Error: No command provided');
	console.error('Usage: watch-with-typings-guard.mjs <command>');
	process.exit(1);
}

let childProcess = null;
// We'll support multiple targets (CE and PRO). Each target has its own ready state.
const targets = [];
let pendingRestart = null;
let hasStartedOnce = false;
let isKilling = false;

/**
 * Start the child process
 */
async function startProcess() {
	// Process should start only when all required targets report ready
	const allReady = targets.length > 0 && targets.every((t) => t.isReady);
	if (!allReady) {
		if (!hasStartedOnce) {
			const names = targets.map((t) => t.name).join(', ');
			console.log(`Waiting for typings to be ready for: ${names}...`);
		}
		return;
	}

	if (childProcess) {
		console.log('Restarting process...');
		await killProcess();
	}

	if (isKilling) {
		console.log('Waiting for previous process to terminate...');
		return;
	}

	console.log(`Starting: ${command}`);
	childProcess = spawn(command, {
		shell: true,
		stdio: 'inherit',
		env: { ...process.env },
		detached: false
	});

	hasStartedOnce = true;

	childProcess.on('exit', (code) => {
		if (code !== null && code !== 0 && !isKilling) {
			console.error(`❌ Process exited with code ${code}`);
		}
		childProcess = null;
		isKilling = false;
	});
}

/**
 * Kill the child process gracefully (and wait for it to die)
 */
function killProcess() {
	return new Promise((resolve) => {
		if (!childProcess || isKilling) {
			resolve();
			return;
		}

		isKilling = true;
		const pid = childProcess.pid;

		// Set a timeout in case the process doesn't die
		const timeout = setTimeout(() => {
			console.error(`⚠️  Process ${pid} didn't terminate gracefully, force killing...`);
			if (childProcess) {
				try {
					treeKill(pid, 'SIGKILL');
				} catch (err) {
					console.error('Error force killing process:', err.message);
				}
			}
			isKilling = false;
			childProcess = null;
			resolve();
		}, KILL_TIMEOUT_MS);

		// Try graceful shutdown first
		childProcess.once('exit', () => {
			clearTimeout(timeout);
			isKilling = false;
			childProcess = null;
			resolve();
		});

		try {
			// Kill the entire process tree (important for shells that spawn subprocesses)
			treeKill(pid, 'SIGTERM', (err) => {
				if (err) {
					console.error(`⚠️  Error killing process tree: ${err.message}`);
					// Fallback to direct kill
					try {
						childProcess?.kill('SIGTERM');
					} catch (e) {
						console.error('Error in fallback kill:', e.message);
					}
				}
			});
		} catch (err) {
			console.error('Error killing process:', err.message);
			clearTimeout(timeout);
			isKilling = false;
			childProcess = null;
			resolve();
		}
	});
}

/**
 * Schedule a restart with debouncing
 */
function scheduleRestart() {
	// Don't schedule if we're currently killing a process
	if (isKilling) {
		console.log('Process is being killed, will restart when done...');
		// Will retry after kill completes
		setTimeout(scheduleRestart, 100);
		return;
	}

	if (pendingRestart) {
		clearTimeout(pendingRestart);
	}

	pendingRestart = setTimeout(async () => {
		pendingRestart = null;
		await startProcess();
	}, DEBOUNCE_MS);
}

/**
 * Check if typings are ready
 */
function checkTypingsReady() {
	let changed = false;
	for (const t of targets) {
		const wasReady = t.isReady;
		t.isReady = existsSync(t.flagPath);
		if (!wasReady && t.isReady) {
			console.log(`✅ Typings ready for ${t.name}!`);
			changed = true;
		} else if (wasReady && !t.isReady) {
			console.log(`Typings recompiling for ${t.name}... (process will restart when ready)`);
			changed = true;
		}
	}

	if (changed) {
		scheduleRestart();
	}

	return targets.every((t) => t.isReady);
}

/**
 * Watch the flag file
 */
/**
 * Create a watcher for a single target that watches its flag dir and dist dir.
 * If the directories don't exist yet, we poll until they appear then create watchers.
 */
function watchTarget(target) {
	console.log(`Setting up watchers for ${target.name}`);

	// Initial check
	checkTypingsReady();

	const flagDir = dirname(target.flagPath);
	const distDir = target.distPath;

	function createWatchers() {
		try {
			if (!target.flagWatcher && existsSync(flagDir)) {
				target.flagWatcher = watch(flagDir, { recursive: false }, (eventType, filename) => {
					if (filename === 'typings-ready.flag') {
						checkTypingsReady();
					}
				});

				target.flagWatcher.on('error', (error) => {
					console.error(`❌ Watcher error for ${target.name} flag:`, error);
				});
			}

			if (!target.distWatcher && existsSync(distDir)) {
				target.distWatcher = watch(distDir, { recursive: true }, (eventType, filename) => {
					if (filename === 'typings-ready.flag') return;
					if (childProcess && target.isReady) {
						console.log(`Detected change in ${target.name} typings: ${filename} (will restart when compilation finishes)`);
					}
				});

				target.distWatcher.on('error', (error) => {
					console.error(`❌ Watcher error for ${target.name} dist:`, error);
				});
			}

			// If we have at least one watcher, stop polling
			if ((target.flagWatcher || target.distWatcher) && target.poller) {
				clearInterval(target.poller);
				target.poller = null;
			}
		} catch (err) {
			// Rare race - ignore and keep polling
			// console.error(`Error creating watcher for ${target.name}:`, err.message);
		}
	}

	// If directories are already present, create watchers immediately
	if (existsSync(flagDir) || existsSync(distDir)) {
		createWatchers();
	}

	// Start polling for directory creation if watchers not yet created
	if (!target.flagWatcher && !target.distWatcher) {
		target.poller = setInterval(() => {
			createWatchers();
		}, POLL_DIR_MS);
	}

	return () => {
		if (target.flagWatcher) {
			try { target.flagWatcher.close(); } catch (e) {}
			target.flagWatcher = null;
		}
		if (target.distWatcher) {
			try { target.distWatcher.close(); } catch (e) {}
			target.distWatcher = null;
		}
		if (target.poller) {
			clearInterval(target.poller);
			target.poller = null;
		}
	};
}

/**
 * Watch typings/dist for changes (to trigger restart when ready)
 */
// Setup targets depending on the provided command. If command mentions meet-pro we include PRO
function setupTargetsFromCommand(cmd) {
	const normalized = (cmd || '').toLowerCase();
	const usePro = /\b(?:meet-pro|pro)\b/.test(normalized);

	// Always include CE by default unless the command explicitly targets only pro and not CE.
	// If both are mentioned include both.
	const includeCE = true; // keep CE by default

	if (includeCE) {
		targets.push({
			name: 'CE',
			flagPath: CE_TYPINGS_FLAG_PATH,
			distPath: CE_TYPINGS_DIST,
			isReady: false,
			flagWatcher: null,
			distWatcher: null,
			poller: null,
		});
	}

	if (usePro) {
		targets.push({
			name: 'PRO',
			flagPath: PRO_TYPINGS_FLAG_PATH,
			distPath: PRO_TYPINGS_DIST,
			isReady: false,
			flagWatcher: null,
			distWatcher: null,
			poller: null,
		});
	}

	// If the command only mentions PRO and you don't want CE, you could tweak logic here.
}

// Setup targets based on command
setupTargetsFromCommand(command);

// Create watchers for each target and keep cleanup functions
const cleanupFns = [];
for (const t of targets) {
	const cleanup = watchTarget(t);
	cleanupFns.push(cleanup);
}

// Kick an initial check/start attempt (in case flags already ready)
checkTypingsReady();

// Cleanup on exit
async function doCleanupAndExit(code = 0) {
	console.log('\n🛑 Stopping...');
	await killProcess();
	for (const fn of cleanupFns) {
		try { fn(); } catch (e) {}
	}
	process.exit(code);
}

process.on('SIGINT', async () => {
	await doCleanupAndExit(0);
});

process.on('SIGTERM', async () => {
	await doCleanupAndExit(0);
});
