/**
 * zero-progress.js — detects autonomous units that spend tool budget without progress.
 *
 * Purpose: fail units that keep calling tools while neither filesystem nor
 * structured progress markers advance, so the autonomous loop stops wasting
 * budget after a research or implementation artifact is already complete.
 */
import { createHash } from "node:crypto";
import { lstatSync, readFileSync } from "node:fs";
import { execFileSync } from "node:child_process";

export const MIN_CALLS_FOR_DETECTION = 10;
export const TOOL_GROWTH = 5;
export const STAGNATION_THRESHOLD = 2;

/**
 * Return whether a unit is stuck in zero-progress tool churn.
 *
 * Purpose: provide the autonomous runaway guard with a low-threshold signal
 * calibrated against accurate per-unit tool-call counters.
 *
 * Consumer: uok/auto-runaway-guard.js when supervising active autonomous units.
 */
export async function detectZeroProgress(
	unitMetrics,
	lastSnapshot,
	options = {},
) {
	return evaluateZeroProgress(unitMetrics, lastSnapshot, options);
}

/**
 * Return the same zero-progress decision synchronously for timer callers.
 *
 * Purpose: keep evaluateRunawayGuard synchronous while sharing detector logic
 * with the exported async detector API.
 *
 * Consumer: uok/auto-runaway-guard.js.
 */
export function evaluateZeroProgress(unitMetrics, lastSnapshot, options = {}) {
	const toolCalls = metricNumber(unitMetrics, "tool_calls", "toolCalls");
	const lastToolCalls = metricNumber(lastSnapshot, "tool_calls", "toolCalls");
	const toolCallsSinceLastChange = toolCalls - lastToolCalls;
	const elapsedMs =
		metricNumber(unitMetrics, "elapsedMs", "elapsed_ms") -
		metricNumber(lastSnapshot, "elapsedMs", "elapsed_ms");
	const fingerprint = resolveFingerprint(unitMetrics, options);
	const previousFingerprint = valueAt(
		lastSnapshot,
		"fingerprint",
		"worktreeFingerprint",
		"worktree_fingerprint",
	);
	const iterationsSinceProgress = metricNumber(
		unitMetrics,
		"iterationsSinceProgress",
		"iterations_since_progress",
	);

	if (
		toolCalls >= MIN_CALLS_FOR_DETECTION &&
		toolCallsSinceLastChange >= TOOL_GROWTH &&
		fingerprint !== null &&
		fingerprint === previousFingerprint &&
		iterationsSinceProgress >= STAGNATION_THRESHOLD
	) {
		return {
			stuck: true,
			reason: "zero-progress",
			signature: {
				toolCallsSinceLastChange,
				elapsedMs: Math.max(0, elapsedMs),
				fingerprint,
			},
		};
	}

	return { stuck: false, reason: "", signature: {} };
}

/**
 * Collect a worktree fingerprint for detector callers that do not already have one.
 *
 * Purpose: give the standalone detector a bounded fallback without importing
 * from the UOK guard module and creating a circular dependency.
 *
 * Consumer: detectZeroProgress() tests and direct detector callers.
 */
export function collectWorktreeFingerprint(cwd) {
	if (!cwd) return null;
	try {
		const status = execFileSync(
			"git",
			["status", "--porcelain=v1", "--untracked-files=all"],
			{
				cwd,
				encoding: "utf8",
				stdio: ["ignore", "pipe", "ignore"],
				timeout: 2000,
			},
		);
		const lines = status
			.split("\n")
			.map((line) => line.trimEnd())
			.filter(Boolean);
		const hash = createHash("sha256");
		if (lines.length === 0) {
			hash.update("git-clean");
			hash.update("\0");
		}
		for (const line of lines) {
			hash.update(line);
			hash.update("\0");
			const filePath = parsePorcelainPath(line);
			if (filePath) appendFileFingerprint(hash, cwd, filePath);
		}
		return hash.digest("hex");
	} catch {
		return null;
	}
}

function resolveFingerprint(unitMetrics, options) {
	const explicit = valueAt(
		unitMetrics,
		"fingerprint",
		"worktreeFingerprint",
		"worktree_fingerprint",
	);
	if (explicit !== undefined) return explicit;
	const collect =
		options.collectWorktreeFingerprint ?? collectWorktreeFingerprint;
	return collect(options.cwd ?? options.basePath);
}

function metricNumber(source, ...keys) {
	const value = valueAt(source, ...keys);
	return typeof value === "number" && Number.isFinite(value) ? value : 0;
}

function valueAt(source, ...keys) {
	if (!source || typeof source !== "object") return undefined;
	for (const key of keys) {
		if (source[key] !== undefined) return source[key];
	}
	return undefined;
}

function appendFileFingerprint(hash, cwd, relativePath) {
	try {
		const stat = lstatSync(`${cwd}/${relativePath}`);
		if (!stat.isFile()) {
			hash.update(
				`type:${relativePath}:${stat.isDirectory() ? "dir" : "other"}`,
			);
			hash.update("\0");
			return;
		}
		hash.update(`file:${relativePath}`);
		hash.update("\0");
		hash.update(readFileSync(`${cwd}/${relativePath}`));
		hash.update("\0");
	} catch {
		hash.update(`unreadable-or-deleted:${relativePath}`);
		hash.update("\0");
	}
}

function parsePorcelainPath(line) {
	if (line.length < 4) return null;
	let filePath = line.slice(3);
	const renameSeparator = " -> ";
	if (filePath.includes(renameSeparator)) {
		filePath = filePath.slice(
			filePath.lastIndexOf(renameSeparator) + renameSeparator.length,
		);
	}
	if (filePath.startsWith('"') && filePath.endsWith('"')) {
		filePath = filePath.slice(1, -1);
	}
	return filePath || null;
}

/**
 * Run the zero-progress detector as a UOK verification gate.
 *
 * Purpose: expose tool-churn detection to the gate runner while preserving the
 * detector API used by the autonomous runaway guard.
 *
 * Consumer: UOK gate registry alignment for detector-based verification gates.
 */
export const zeroProgressGate = {
	id: "zero-progress",
	type: "verification",
	async execute(ctx = {}) {
		const result = await detectZeroProgress(
			ctx.unitMetrics ?? ctx.recentEvents,
			ctx.lastSnapshot ?? ctx.previousSnapshot,
			ctx.options,
		);
		if (result.stuck) {
			return {
				outcome: "fail",
				failureClass: "verification",
				rationale: result.reason ?? "zero-progress signal",
				findings: result.signature,
			};
		}
		return {
			outcome: "pass",
			failureClass: null,
			rationale: "no zero-progress signal",
		};
	},
};
