import { log } from "../log.js";
import { defineSource } from "../source.js";
import { exec } from "tinyexec";
import { z } from "zod";
import { fileURLToPath } from "node:url";
import { coerce, diff } from "semver";
//#region src/lib/sources/dependency-updates.ts
const depSchema = z.object({
	age: z.string().optional(),
	info: z.string().optional(),
	new: z.string(),
	old: z.string()
});
const updatesOutputSchema = z.object({ results: z.record(z.string(), z.record(z.string(), z.record(z.string(), depSchema))) });
/**
* Resolve the path to the `updates` CLI binary.
*
* Consumers that bundle metascope must externalize it (e.g. via `neverBundle`)
* so that `import.meta.resolve` can find the `updates` dependency.
*/
function resolveUpdatesBinary() {
	return fileURLToPath(import.meta.resolve("updates/dist/index.js"));
}
/**
* Parse an age string from the `updates` CLI (via the `timerel` library) into fractional years.
*
* Possible formats: "now", "<n> sec(s)", "<n> min(s)", "<n> hour(s)",
* "<n> day(s)", "<n> week(s)", "<n> month(s)", "<n> year(s)"
*/
function parseAgeToYears(age) {
	if (age === "now") return 0;
	const match = /^(\d+)\s+(\w+)$/.exec(age.trim());
	if (!match) return 0;
	const value = Number(match[1]);
	switch (match[2]) {
		case "day":
		case "days": return value / 365.25;
		case "hour":
		case "hours": return value / (365.25 * 24);
		case "min":
		case "mins": return value / (365.25 * 24 * 60);
		case "month":
		case "months": return value / 12;
		case "sec":
		case "secs": return value / (365.25 * 24 * 60 * 60);
		case "week":
		case "weeks": return value * 7 / 365.25;
		case "year":
		case "years": return value;
		default: return 0;
	}
}
/**
* Classify a version bump as major, minor, or patch using semver.
* Falls back to 'major' for non-semver versions (e.g. GitHub Actions tags).
*/
function classifyBump(oldVersion, newVersion) {
	const oldSemver = coerce(oldVersion);
	const newSemver = coerce(newVersion);
	if (!oldSemver || !newSemver) return "major";
	const result = diff(oldSemver, newSemver);
	if (!result) return "major";
	if (result.startsWith("major") || result === "premajor") return "major";
	if (result.startsWith("minor") || result === "preminor") return "minor";
	return "patch";
}
const dependencyUpdatesSource = defineSource({
	async discover(context) {
		return [context.options.path];
	},
	key: "dependencyUpdates",
	async parse(input) {
		log.debug("Extracting dependency update information via updates...");
		const result = await exec("node", [
			resolveUpdatesBinary(),
			"--file",
			input,
			"--json"
		]);
		let parsed;
		try {
			parsed = updatesOutputSchema.parse(JSON.parse(result.stdout));
		} catch {
			log.debug("No dependency files found for updates analysis.");
			return;
		}
		const major = [];
		const minor = [];
		const patch = [];
		let libyears = 0;
		for (const mode of Object.values(parsed.results)) for (const depGroup of Object.values(mode)) for (const [name, dep] of Object.entries(depGroup)) {
			if (name === "@types/node") continue;
			if (dep.age) libyears += parseAgeToYears(dep.age);
			const packageStatus = {
				name,
				new: dep.new,
				old: dep.old
			};
			if (dep.age) packageStatus.age = dep.age;
			if (dep.info) packageStatus.info = dep.info;
			switch (classifyBump(dep.old, dep.new)) {
				case "major":
					major.push(packageStatus);
					break;
				case "minor":
					minor.push(packageStatus);
					break;
				case "patch":
					patch.push(packageStatus);
					break;
			}
		}
		return {
			data: {
				major,
				minor,
				patch
			},
			extra: {
				libyears: Math.round(libyears * 10) / 10,
				total: major.length + minor.length + patch.length
			},
			source: input
		};
	},
	phase: 1
});
//#endregion
export { dependencyUpdatesSource };
