#!/usr/bin/env node
// Freshness stamp for admin/dist (milestone-15 ruling 5). A stale dist has
// already shipped a crashing settings page once. Write mode runs from admin/ as
// part of `npm run build`; check mode runs from the repository root as a
// build.zig system command. Every path resolves from this file's own location
// so both working directories hash the same set.

import { createHash } from "node:crypto";
import { readdirSync, readFileSync, writeFileSync, statSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

const webRoot = dirname(dirname(fileURLToPath(import.meta.url)));
const distDir = join(webRoot, "dist");
const stampFile = join(distDir, ".src-hash");
const stampRelative = "admin/dist/.src-hash";

const inputDirs = ["src", "public"];
const inputFiles = [
	"index.html",
	"package.json",
	"package-lock.json",
	"vite.config.ts",
	"tsconfig.json",
	"tsconfig.app.json",
	"tsconfig.node.json",
];

const staleMessage = "admin/dist is stale: rebuild the frontend (npm run build)";

function fail(message) {
	process.stderr.write(`${message}\n`);
	process.exit(1);
}

function walk(relativeDir) {
	const absolute = join(webRoot, relativeDir);
	let entries;
	try {
		entries = readdirSync(absolute, { withFileTypes: true });
	} catch (err) {
		fail(`stamp-dist: cannot read admin/${relativeDir}: ${err.message}`);
	}
	const found = [];
	for (const entry of entries) {
		const child = `${relativeDir}/${entry.name}`;
		if (entry.isDirectory()) {
			found.push(...walk(child));
		} else if (entry.isFile()) {
			found.push(child);
		}
	}
	return found;
}

function inputSet() {
	const paths = [...inputFiles, ...inputDirs.flatMap(walk)];
	for (const path of inputFiles) {
		try {
			if (!statSync(join(webRoot, path)).isFile()) fail(`stamp-dist: admin/${path} is not a file`);
		} catch (err) {
			fail(`stamp-dist: cannot stat admin/${path}: ${err.message}`);
		}
	}
	// Sorted by path so the digest does not depend on directory order.
	return paths.sort();
}

function digest() {
	const hash = createHash("sha256");
	for (const path of inputSet()) {
		hash.update(path);
		hash.update("\0");
		hash.update(readFileSync(join(webRoot, path)));
		hash.update("\0");
	}
	return hash.digest("hex");
}

const check = process.argv.includes("--check");
const computed = digest();

if (check) {
	let recorded;
	try {
		recorded = readFileSync(stampFile, "utf8").trim();
	} catch {
		fail(staleMessage);
	}
	if (recorded !== computed) fail(staleMessage);
	process.exit(0);
}

try {
	writeFileSync(stampFile, `${computed}\n`);
} catch (err) {
	fail(`stamp-dist: cannot write ${stampRelative}: ${err.message}`);
}
process.stdout.write(`${stampRelative} ${computed}\n`);
