#!/usr/bin/env node
/**
 * ensure-workspace-builds.cjs
 *
 * Checks whether workspace packages have been compiled (dist/ exists with
 * index.js) and that the build is not stale (no src/ file newer than dist/).
 * If any are missing or stale, runs the build for those packages.
 *
 * Designed for the postinstall hook so that `npm install` in a fresh clone
 * produces a working runtime without a manual `npm run build` step. Also
 * catches the common case where `git pull` updates package sources but the
 * old dist/ remains, causing TypeScript type errors.
 *
 * Skipped in CI (where the full build pipeline handles this) and when
 * installing as an end-user dependency (no packages/ directory).
 */
const { existsSync, statSync, readdirSync } = require("node:fs");
const { resolve, join } = require("node:path");
const { execSync } = require("node:child_process");

/**
 * Returns the most recent mtime (ms) of any .ts file under dir, recursively.
 * Returns 0 if no .ts files found.
 */
function newestSrcMtime(dir) {
	if (!existsSync(dir)) return 0;
	let newest = 0;
	for (const entry of readdirSync(dir, { withFileTypes: true })) {
		if (entry.name === "node_modules") continue;
		const full = join(dir, entry.name);
		if (entry.isDirectory()) {
			newest = Math.max(newest, newestSrcMtime(full));
		} else if (entry.isFile() && entry.name.endsWith(".ts")) {
			newest = Math.max(newest, statSync(full).mtimeMs);
		}
	}
	return newest;
}

/**
 * Detects workspace packages whose dist/ is missing or stale.
 *
 * Missing dist/index.js is always reported (the package won't work at all).
 *
 * Staleness (src/ newer than dist/) is ONLY checked when a .git directory
 * exists at root — indicating a development clone. In npm tarball installs,
 * file timestamps are unreliable (npm sets all files to a canonical date,
 * but extraction ordering can cause src/ to appear 1-2 seconds newer than
 * dist/). Attempting to rebuild in that scenario is dangerous: devDependencies
 * (including TypeScript) are not installed, and any globally-installed tsc
 * may produce broken output that overwrites the known-good dist/.
 *
 * @param {string} root    Project root directory
 * @param {string[]} packages  Package directory names to check
 * @returns {string[]} Package names that need rebuilding
 */
function detectStalePackages(root, packages) {
	const packagesDir = join(root, "packages");
	const isDevClone = existsSync(join(root, ".git"));

	const stale = [];
	for (const pkg of packages) {
		const distIndex = join(packagesDir, pkg, "dist", "index.js");
		if (!existsSync(distIndex)) {
			stale.push(pkg);
			continue;
		}
		// Only check src vs dist timestamps in development clones.
		// In npm tarball installs, timestamps are unreliable and rebuilding
		// without devDependencies can corrupt the pre-built dist/ (#2877).
		if (isDevClone) {
			const distMtime = statSync(distIndex).mtimeMs;
			const srcMtime = newestSrcMtime(join(packagesDir, pkg, "src"));
			if (srcMtime > distMtime) {
				stale.push(pkg);
			}
		}
	}
	return stale;
}

if (require.main === module) {
	const root = resolve(__dirname, "..");
	const packagesDir = join(root, "packages");

	// Skip if packages/ doesn't exist (published tarball / end-user install)
	if (!existsSync(packagesDir)) process.exit(0);

	// Skip in CI — the pipeline runs `npm run build` explicitly
	if (process.env.CI === "true" || process.env.CI === "1") process.exit(0);

	// Workspace packages that need dist/index.js at runtime.
	// Order matters: dependencies must build before dependents.
	const WORKSPACE_PACKAGES = [
		"native",
		"pi-tui",
		"google-gemini-cli-provider",
		"pi-ai",
		"pi-agent-core",
		"pi-coding-agent",
		"rpc-client",
		"daemon",
	];

	const stale = detectStalePackages(root, WORKSPACE_PACKAGES);

	if (stale.length === 0) process.exit(0);

	process.stderr.write(
		`  Building ${stale.length} workspace package(s) with stale or missing dist/: ${stale.join(", ")}\n`,
	);

	for (const pkg of stale) {
		const pkgDir = join(packagesDir, pkg);
		try {
			// execSync is safe here: the command is a hardcoded string, not user input
			execSync("npm run build", { cwd: pkgDir, stdio: "pipe" });
			process.stderr.write(`  ✓ ${pkg}\n`);
		} catch (err) {
			process.stderr.write(`  ✗ ${pkg} build failed: ${err.message}\n`);
			// Non-fatal — the user can run `npm run build` manually
		}
	}
}

module.exports = { newestSrcMtime, detectStalePackages };
