/**
 * UOK Multi-Package Healing Gate
 *
 * Purpose: detect when code changes impact monorepo packages, build a
 * workspace dependency graph, and run verification only on affected packages
 * and their downstream dependents. Prevents full-workspace typecheck bottlenecks.
 *
 * Consumer: UOK gate-runner during post-unit verification.
 */

import { execFile } from "node:child_process";
import { existsSync, readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";

const DEFAULT_TIMEOUT_MS = 120_000;
const DEFAULT_ADDITIONAL_CHECKS = ["typecheck"];

function execFilePromise(file, args, options) {
	return new Promise((resolve, reject) => {
		const child = execFile(file, args, options, (error, stdout, stderr) => {
			if (error) {
				reject(Object.assign(error, { stdout, stderr }));
			} else {
				resolve({ stdout, stderr });
			}
		});
		if (options?.timeout && options.timeout > 0) {
			const t = setTimeout(() => {
				child.kill();
				reject(
					Object.assign(new Error("command timed out"), {
						stdout: "",
						stderr: "",
					}),
				);
			}, options.timeout);
			child.on("exit", () => clearTimeout(t));
			child.on("error", () => clearTimeout(t));
		}
	});
}

function readJsonSafe(path) {
	if (!existsSync(path)) return null;
	try {
		return JSON.parse(readFileSync(path, "utf-8"));
	} catch {
		return null;
	}
}

function discoverWorkspacePackages(basePath) {
	const rootPkg = readJsonSafe(join(basePath, "package.json"));
	if (!rootPkg) return [];

	const packages = [];
	// npm workspaces
	const workspaces = rootPkg.workspaces;
	if (Array.isArray(workspaces)) {
		for (const pattern of workspaces) {
			// Simple glob-like expansion: packages/*
			if (pattern.endsWith("/*")) {
				const dir = pattern.slice(0, -2);
				// We can't list directories easily without fs.readdir, but we can
				// discover packages by looking for package.json files
				// For now, rely on the workspace pattern being directories
				const baseDir = join(basePath, dir);
				if (existsSync(baseDir)) {
					for (const entry of readdirSync(baseDir, { withFileTypes: true })) {
						if (entry.isDirectory()) {
							const pkgPath = join(baseDir, entry.name, "package.json");
							const pkg = readJsonSafe(pkgPath);
							if (pkg) {
								packages.push({
									name: pkg.name,
									path: join(dir, entry.name),
									dependencies: Object.keys(pkg.dependencies ?? {}),
									devDependencies: Object.keys(pkg.devDependencies ?? {}),
								});
							}
						}
					}
				}
			} else {
				const pkgPath = join(basePath, pattern, "package.json");
				const pkg = readJsonSafe(pkgPath);
				if (pkg) {
					packages.push({
						name: pkg.name,
						path: pattern,
						dependencies: Object.keys(pkg.dependencies ?? {}),
						devDependencies: Object.keys(pkg.devDependencies ?? {}),
					});
				}
			}
		}
	}

	// pnpm workspaces
	const pnpmWorkspace = readJsonSafe(join(basePath, "pnpm-workspace.yaml"));
	if (pnpmWorkspace && Array.isArray(pnpmWorkspace.packages)) {
		for (const pattern of pnpmWorkspace.packages) {
			if (pattern.endsWith("/*")) {
				const dir = pattern.slice(0, -2);
				const baseDir = join(basePath, dir);
				if (existsSync(baseDir)) {
					for (const entry of readdirSync(baseDir, { withFileTypes: true })) {
						if (entry.isDirectory()) {
							const pkgPath = join(baseDir, entry.name, "package.json");
							const pkg = readJsonSafe(pkgPath);
							if (pkg) {
								packages.push({
									name: pkg.name,
									path: join(dir, entry.name),
									dependencies: Object.keys(pkg.dependencies ?? {}),
									devDependencies: Object.keys(pkg.devDependencies ?? {}),
								});
							}
						}
					}
				}
			}
		}
	}

	return packages;
}

function buildDependencyGraph(packages) {
	const graph = new Map();
	const nameToPackage = new Map();
	for (const pkg of packages) {
		nameToPackage.set(pkg.name, pkg);
		graph.set(pkg.name, new Set());
	}
	for (const pkg of packages) {
		const deps = [...pkg.dependencies, ...pkg.devDependencies];
		for (const dep of deps) {
			if (nameToPackage.has(dep)) {
				graph.get(dep).add(pkg.name);
			}
		}
	}
	return { graph, nameToPackage };
}

function getAffectedPackages(changedFiles, packages) {
	const affected = new Set();
	for (const file of changedFiles) {
		for (const pkg of packages) {
			const prefix = pkg.path + "/";
			if (file.startsWith(prefix) || file === pkg.path) {
				affected.add(pkg.name);
			}
		}
	}
	return affected;
}

function getDownstreamDependents(graph, affected) {
	const result = new Set(affected);
	const queue = [...affected];
	while (queue.length > 0) {
		const current = queue.shift();
		for (const dependent of graph.get(current) ?? []) {
			if (!result.has(dependent)) {
				result.add(dependent);
				queue.push(dependent);
			}
		}
	}
	return result;
}

export class MultiPackageGate {
	constructor(options = {}) {
		this.id = "multi-package-healing";
		this.type = "verification";
		this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
		this.additionalChecks =
			options.additionalChecks ?? DEFAULT_ADDITIONAL_CHECKS;
	}

	/**
	 * @param {import("./contracts.js").UokContext} ctx
	 */
	async execute(ctx) {
		try {
			// 1. Discover workspace packages
			const packages = discoverWorkspacePackages(ctx.basePath);
			if (packages.length === 0) {
				return {
					outcome: "pass",
					rationale:
						"No workspace packages detected; skipping multi-package verification.",
				};
			}

			// 2. Find changed files
			const diffOutput = (
				await execFilePromise("git", ["diff", "--name-only", "HEAD"], {
					cwd: ctx.basePath,
					stdio: ["ignore", "pipe", "pipe"],
					encoding: "utf-8",
					timeout: 10_000,
				})
			).stdout;
			const changedFiles = diffOutput.split("\n").filter(Boolean);
			if (changedFiles.length === 0) {
				return {
					outcome: "pass",
					rationale:
						"No uncommitted changes; skipping multi-package verification.",
				};
			}

			// 3. Determine affected packages
			const affected = getAffectedPackages(changedFiles, packages);
			if (affected.size === 0) {
				return {
					outcome: "pass",
					rationale: "Changes do not impact any workspace packages.",
				};
			}

			// 4. Build dependency graph and find downstream dependents
			const { graph } = buildDependencyGraph(packages);
			const toVerify = getDownstreamDependents(graph, affected);

			// 5. Run targeted checks
			const failures = [];
			for (const check of this.additionalChecks) {
				for (const pkgName of toVerify) {
					const pkg = packages.find((p) => p.name === pkgName);
					if (!pkg) continue;
					const pkgPath = join(ctx.basePath, pkg.path);
					if (!existsSync(join(pkgPath, "package.json"))) continue;

					try {
						await execFilePromise("npm", ["run", check, "--if-present"], {
							cwd: pkgPath,
							stdio: ["ignore", "pipe", "pipe"],
							encoding: "utf-8",
							timeout: this.timeoutMs,
						});
					} catch (err) {
						const output = `${err.stdout ?? ""}${err.stderr ?? ""}`.trim();
						failures.push({
							pkg: pkgName,
							check,
							output: output || String(err.message || err),
						});
					}
				}
			}

			if (failures.length > 0) {
				return {
					outcome: "fail",
					failureClass: "verification",
					rationale: `Multi-Package regression detected in ${failures.length} package/check combination(s).`,
					findings: failures
						.map((f) => `${f.pkg} (${f.check}): ${f.output.slice(0, 500)}`)
						.join("\n---\n"),
				};
			}

			return {
				outcome: "pass",
				rationale: `Verified ${toVerify.size} affected package(s) including downstream dependents: ${[...toVerify].join(", ")}.`,
			};
		} catch (err) {
			const output = `${err.stdout ?? ""}${err.stderr ?? ""}`.trim();
			return {
				outcome: "fail",
				failureClass: "verification",
				rationale: "Multi-Package verification infrastructure failed.",
				findings: output || String(err),
			};
		}
	}
}
