/**
 * stale-lock.js — detect lock files whose owning process is gone.
 *
 * Purpose: unblock autonomous operation when a prior SF process died after
 * writing a lock but before releasing it.
 *
 * Consumer: periodic detector sweeps before dispatch selection.
 */
import { promises as fs } from "node:fs";

function parsePid(contents) {
	const trimmed = contents.trim();
	if (!trimmed) return null;
	try {
		const parsed = JSON.parse(trimmed);
		const pid = Number(parsed?.pid);
		return Number.isInteger(pid) && pid > 0 ? pid : null;
	} catch {
		const pid = Number(trimmed);
		return Number.isInteger(pid) && pid > 0 ? pid : null;
	}
}

function lockedAtMs(contents, stats) {
	try {
		const parsed = JSON.parse(contents);
		const lockedAt = parsed?.lockedAt;
		if (typeof lockedAt === "number" && Number.isFinite(lockedAt)) {
			return lockedAt;
		}
		if (typeof lockedAt === "string") {
			const value = Date.parse(lockedAt);
			if (Number.isFinite(value)) return value;
		}
	} catch {
		// Plain PID locks fall back to mtime.
	}
	return stats.mtimeMs;
}

function isProcessAlive(pid) {
	try {
		process.kill(pid, 0);
		return true;
	} catch (error) {
		return error?.code === "EPERM";
	}
}

/**
 * Detect stale lock files and optionally remove the first stale lock found.
 *
 * Purpose: let SF recover from abandoned process locks without treating live
 * locks as failures.
 *
 * Consumer: periodic detector runner.
 */
export async function detectStaleLock(lockPaths, options = {}) {
	const autoRecover = options.autoRecover ?? false;
	const now = options.now ?? Date.now();
	const paths = Array.isArray(lockPaths) ? lockPaths : [];

	for (const lockPath of paths) {
		let contents;
		let stats;
		try {
			[contents, stats] = await Promise.all([
				fs.readFile(lockPath, "utf8"),
				fs.stat(lockPath),
			]);
		} catch (error) {
			if (error?.code === "ENOENT") continue;
			throw error;
		}

		const pid = parsePid(contents);
		if (!pid || isProcessAlive(pid)) continue;

		let recovered = false;
		if (autoRecover) {
			await fs.unlink(lockPath);
			recovered = true;
		}

		return {
			stuck: true,
			reason: "stale-lock",
			signature: {
				lockPath,
				deadPid: pid,
				ageMs: Math.max(0, now - lockedAtMs(contents, stats)),
			},
			recovered,
		};
	}

	return { stuck: false };
}

/**
 * Run the stale-lock detector as a UOK security gate.
 *
 * Purpose: expose abandoned lock detection to security gates so dead process
 * ownership can block or recover autonomous operation.
 *
 * Consumer: UOK gate registry alignment for detector-based security gates.
 */
export const staleLockGate = {
	id: "stale-lock",
	type: "security",
	async execute(ctx = {}) {
		const result = await detectStaleLock(
			ctx.lockInfo ?? ctx.lockPaths,
			ctx.options,
		);
		if (result.stuck) {
			return {
				outcome: "fail",
				failureClass: "verification",
				rationale: result.reason ?? "stale-lock signal",
				findings: result.signature,
			};
		}
		return {
			outcome: "pass",
			failureClass: null,
			rationale: "no stale-lock signal",
		};
	},
};
