import { execSync } from "node:child_process";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import chalk from "chalk";
import { appRoot } from "./app-paths.js";

const CACHE_FILE = join(appRoot, ".update-check");
export const NPM_PACKAGE_NAME = "singularity-forge";
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
const FETCH_TIMEOUT_MS = 5000;
const DEFAULT_REGISTRY_URL = `https://registry.npmjs.org/${NPM_PACKAGE_NAME}/latest`;

interface UpdateCheckCache {
	lastCheck: number;
	latestVersion: string;
}

/**
 * Compares two semver strings. Returns 1 if a > b, -1 if a < b, 0 if equal.
 */
export function compareSemver(a: string, b: string): number {
	const pa = a.split(".").map(Number);
	const pb = b.split(".").map(Number);
	for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
		const va = pa[i] || 0;
		const vb = pb[i] || 0;
		if (va > vb) return 1;
		if (va < vb) return -1;
	}
	return 0;
}

export function readUpdateCache(
	cachePath: string = CACHE_FILE,
): UpdateCheckCache | null {
	try {
		if (!existsSync(cachePath)) return null;
		return JSON.parse(readFileSync(cachePath, "utf-8"));
	} catch {
		return null;
	}
}

export function writeUpdateCache(
	cache: UpdateCheckCache,
	cachePath: string = CACHE_FILE,
): void {
	try {
		mkdirSync(dirname(cachePath), { recursive: true });
		writeFileSync(cachePath, JSON.stringify(cache));
	} catch {
		// Non-fatal — don't block startup if cache write fails
	}
}

function normalizeLatestVersion(version: unknown): string | null {
	if (typeof version !== "string") return null;
	const trimmed = version.trim().replace(/^v/, "");
	return trimmed.length > 0 ? trimmed : null;
}

export async function fetchLatestVersionFromRegistry(
	registryUrl: string = DEFAULT_REGISTRY_URL,
	fetchTimeoutMs: number = FETCH_TIMEOUT_MS,
): Promise<string | null> {
	const controller = new AbortController();
	const timeout = setTimeout(() => controller.abort(), fetchTimeoutMs);

	try {
		const res = await fetch(registryUrl, { signal: controller.signal });
		if (!res.ok) return null;

		const data = (await res.json()) as { version?: string };
		return normalizeLatestVersion(data.version);
	} catch {
		return null;
	} finally {
		clearTimeout(timeout);
	}
}

export function resolveInstallCommand(pkg: string): string {
	if ("bun" in process.versions) {
		return `bun add -g ${pkg}`;
	}
	return `npm install -g ${pkg}`;
}

function printUpdateBanner(current: string, latest: string): void {
	const installCmd = resolveInstallCommand(`${NPM_PACKAGE_NAME}@latest`);
	process.stderr.write(
		`  ${chalk.yellow("Update available:")} ${chalk.dim(`v${current}`)} → ${chalk.bold(`v${latest}`)}\n` +
			`  ${chalk.dim("Run")} ${installCmd} ${chalk.dim("or")} /sf update ${chalk.dim("to upgrade")}\n\n`,
	);
}

export interface UpdateCheckOptions {
	currentVersion?: string;
	cachePath?: string;
	registryUrl?: string;
	checkIntervalMs?: number;
	fetchTimeoutMs?: number;
	onUpdate?: (current: string, latest: string) => void;
}

/**
 * Non-blocking update check. Queries npm registry at most once per 24h,
 * caches the result, and prints a banner if a newer version is available.
 */
export async function checkForUpdates(
	options: UpdateCheckOptions = {},
): Promise<void> {
	const currentVersion =
		options.currentVersion || process.env.SF_VERSION || "0.0.0";
	const cachePath = options.cachePath || CACHE_FILE;
	const registryUrl = options.registryUrl || DEFAULT_REGISTRY_URL;
	const checkIntervalMs = options.checkIntervalMs ?? CHECK_INTERVAL_MS;
	const fetchTimeoutMs = options.fetchTimeoutMs ?? FETCH_TIMEOUT_MS;
	const onUpdate = options.onUpdate || printUpdateBanner;

	// Check cache — skip network if checked recently
	const cache = readUpdateCache(cachePath);
	if (cache && Date.now() - cache.lastCheck < checkIntervalMs) {
		if (compareSemver(cache.latestVersion, currentVersion) > 0) {
			onUpdate(currentVersion, cache.latestVersion);
		}
		return;
	}

	try {
		const latestVersion = await fetchLatestVersionFromRegistry(
			registryUrl,
			fetchTimeoutMs,
		);
		if (!latestVersion) return;

		writeUpdateCache({ lastCheck: Date.now(), latestVersion }, cachePath);

		if (compareSemver(latestVersion, currentVersion) > 0) {
			onUpdate(currentVersion, latestVersion);
		}
	} catch {
		// Network error or timeout — silently ignore, don't block startup
	}
}

const PROMPT_TIMEOUT_MS = 30_000;

/**
 * Interactive update prompt shown at startup when a newer version is available.
 * Fetches the latest version (with cache), then asks the user whether to
 * update now or skip. Runs at most once per 24 hours (same cache as checkForUpdates).
 * Defaults to skip after 30 seconds of inactivity.
 *
 * Returns true if an update was performed, false otherwise.
 */
export async function checkAndPromptForUpdates(
	options: UpdateCheckOptions = {},
): Promise<boolean> {
	const currentVersion =
		options.currentVersion || process.env.SF_VERSION || "0.0.0";
	const cachePath = options.cachePath || CACHE_FILE;
	const registryUrl = options.registryUrl || DEFAULT_REGISTRY_URL;
	const checkIntervalMs = options.checkIntervalMs ?? CHECK_INTERVAL_MS;
	const fetchTimeoutMs = options.fetchTimeoutMs ?? FETCH_TIMEOUT_MS;

	// Determine latest version (from cache or network)
	let latestVersion: string | null = null;

	const cache = readUpdateCache(cachePath);
	if (cache && Date.now() - cache.lastCheck < checkIntervalMs) {
		latestVersion = cache.latestVersion;
	} else {
		try {
			latestVersion = await fetchLatestVersionFromRegistry(
				registryUrl,
				fetchTimeoutMs,
			);
			if (latestVersion) {
				writeUpdateCache({ lastCheck: Date.now(), latestVersion }, cachePath);
			}
		} catch {
			// Network unavailable — silently skip
		}
	}

	if (!latestVersion || compareSemver(latestVersion, currentVersion) <= 0) {
		return false;
	}

	// Update available — show interactive prompt
	// Measure visible (ANSI-free) width to size the box, then render with chalk.
	const midContent = `  ${chalk.bold("Update available!")} ${chalk.dim(`v${currentVersion}`)} → ${chalk.bold.green(`v${latestVersion}`)}  `;
	const midVisible = `  Update available! v${currentVersion} → v${latestVersion}  `;
	const innerWidth = midVisible.length;
	const top = "╔" + "═".repeat(innerWidth) + "╗";
	const bot = "╚" + "═".repeat(innerWidth) + "╝";

	process.stderr.write("\n");
	process.stderr.write(
		`  ${chalk.yellow(top)}\n` +
			`  ${chalk.yellow("║")}${midContent}${chalk.yellow("║")}\n` +
			`  ${chalk.yellow(bot)}\n\n`,
	);

	// Use readline for a simple two-option prompt that works without @clack/prompts
	const readline = await import("node:readline");
	const rl = readline.createInterface({
		input: process.stdin,
		output: process.stderr,
	});

	const choice = await new Promise<string>((resolve) => {
		process.stderr.write(
			`  ${chalk.bold("[1]")} Update now   ${chalk.dim(resolveInstallCommand(`${NPM_PACKAGE_NAME}@latest`))}\n` +
				`  ${chalk.bold("[2]")} Skip\n\n`,
		);

		// Default to skip if the user doesn't respond within PROMPT_TIMEOUT_MS
		const timer = setTimeout(() => {
			process.stderr.write("\n");
			rl.close();
			resolve("2");
		}, PROMPT_TIMEOUT_MS);

		rl.question(`  ${chalk.bold("Choose [1/2]:")} `, (answer) => {
			clearTimeout(timer);
			resolve(answer.trim());
		});
	});

	rl.close();

	// Clean up stdin state so the TUI can start with a clean slate
	process.stdin.removeAllListeners("data");
	process.stdin.removeAllListeners("keypress");
	if (process.stdin.setRawMode) process.stdin.setRawMode(false);
	process.stdin.pause();

	if (choice === "1") {
		const installCmd = resolveInstallCommand(`${NPM_PACKAGE_NAME}@latest`);
		process.stderr.write(`\n  ${chalk.dim("Running:")} ${installCmd}\n\n`);
		try {
			execSync(installCmd, { stdio: "inherit" });
			process.stderr.write(
				`\n  ${chalk.green.bold(`✓ Updated to v${latestVersion}`)}\n\n`,
			);
			return true;
		} catch {
			process.stderr.write(
				`\n  ${chalk.yellow(`Update failed. You can run: ${installCmd}`)}\n\n`,
			);
		}
	} else {
		process.stderr.write(
			`  ${chalk.dim("Skipped. Run")} sf update ${chalk.dim("anytime to upgrade.")}\n\n`,
		);
	}

	return false;
}
