/**
 * crash-loop-classifier.js — detect repeated fast failures on one source hash.
 *
 * Purpose: quarantine autonomous runtime regressions before the watchdog or
 * server-owned swarm supervisor restarts the same broken source forever.
 *
 * Consumer: M048 regression firewall periodic detector sweep and S04 rollback.
 */
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
import { join } from "node:path";

import { compareToLastGreen, getLastGreenEntry } from "../last-green.js";

export const CRASH_LOOP_WINDOW_MS = 90_000;
export const CRASH_LOOP_THRESHOLD = 3;
const EVENTS_FILE = ".sf/runtime/crash-loop-events.json";
const MAX_EVENTS = 50;

/**
 * Detect whether a current dispatch result is a crash-loop regression.
 *
 * Purpose: turn the R066 signature (<90s, non-zero, same source hash 3x) into a
 * durable detector result that can stop autonomous promotion.
 *
 * Consumer: periodic-runner.js and S04 rollback controller.
 */
export function detectCrashLoop(basePath, currentResult = {}, options = {}) {
	const lastGreen = options.lastGreen ?? getLastGreenEntry(basePath);
	if (!lastGreen) {
		return {
			stuck: false,
			reason: "missing-last-green",
			signature: { eventCount: 0 },
		};
	}

	const event = normalizeCrashEvent(currentResult, options.now ?? Date.now());
	if (!event.sourceHash) {
		return {
			stuck: false,
			reason: "missing-source-hash",
			signature: { eventCount: 0 },
		};
	}

	const previousEvents = Array.isArray(options.history)
		? options.history
		: readCrashLoopEvents(basePath);
	const events = appendCrashLoopEvent(basePath, previousEvents, event, options);
	const threshold = options.threshold ?? CRASH_LOOP_THRESHOLD;
	const windowMs = options.windowMs ?? CRASH_LOOP_WINDOW_MS;
	const cutoff = event.finishedAt - windowMs;
	const matching = events.filter(
		(row) =>
			row.sourceHash === event.sourceHash &&
			row.failed === true &&
			row.elapsedMs < windowMs &&
			row.finishedAt >= cutoff,
	);
	const comparison = compareCurrentToLastGreen(currentResult, lastGreen);

	if (matching.length >= threshold && comparison.match === false) {
		return {
			stuck: true,
			reason: "crash-loop-regression",
			signature: {
				sourceHash: event.sourceHash,
				eventCount: matching.length,
				windowMs,
				elapsedMs: event.elapsedMs,
				exitCode: event.exitCode,
				lastGreenChecksum: lastGreen.checksum ?? null,
				delta: comparison.delta,
			},
		};
	}

	return {
		stuck: false,
		reason: "",
		signature: {
			sourceHash: event.sourceHash,
			eventCount: matching.length,
			windowMs,
			delta: comparison.delta,
		},
	};
}

/**
 * Run the crash-loop classifier as a UOK verification gate.
 *
 * Purpose: expose R066 quarantine to the gate runner and web/server status
 * surfaces without coupling them to the detector implementation.
 *
 * Consumer: detector gate registry and periodicDetectorSweepGate.
 */
export const crashLoopGate = {
	id: "crash-loop",
	type: "verification",
	async execute(ctx = {}) {
		const result = detectCrashLoop(
			ctx.basePath ?? ctx.cwd,
			ctx.currentResult ?? ctx.result,
			ctx.options,
		);
		if (result.stuck) {
			return {
				outcome: "fail",
				failureClass: "verification",
				rationale: result.reason,
				findings: result.signature,
			};
		}
		return {
			outcome: "pass",
			failureClass: null,
			rationale: result.reason || "no crash-loop signal",
		};
	},
};

function normalizeCrashEvent(result, now) {
	const exitCode =
		typeof result.exitCode === "number"
			? result.exitCode
			: result.ok === false
				? 1
				: 0;
	const elapsedMs =
		typeof result.elapsedMs === "number" && Number.isFinite(result.elapsedMs)
			? Math.max(0, result.elapsedMs)
			: 0;
	return {
		sourceHash:
			stringValue(result.sourceHash) ??
			stringValue(result.source_hash) ??
			stringValue(result.checksum),
		exitCode,
		elapsedMs,
		failed: result.ok === false || exitCode !== 0,
		finishedAt:
			typeof result.finishedAt === "number" &&
			Number.isFinite(result.finishedAt)
				? result.finishedAt
				: now,
	};
}

function compareCurrentToLastGreen(currentResult, lastGreen) {
	const currentEntry = {
		result: {
			ok: currentResult.ok === true,
			exitCode:
				typeof currentResult.exitCode === "number" ? currentResult.exitCode : 0,
			outputLength:
				typeof currentResult.outputLength === "number"
					? currentResult.outputLength
					: String(currentResult.output ?? "").length,
			elapsedMs:
				typeof currentResult.elapsedMs === "number"
					? currentResult.elapsedMs
					: 0,
		},
	};
	return compareToLastGreen(currentEntry, lastGreen);
}

function readCrashLoopEvents(basePath) {
	if (!basePath) return [];
	try {
		const parsed = JSON.parse(
			readFileSync(join(basePath, EVENTS_FILE), "utf8"),
		);
		return Array.isArray(parsed) ? parsed : [];
	} catch {
		return [];
	}
}

function appendCrashLoopEvent(basePath, previousEvents, event, options) {
	const events = previousEvents.concat(event).slice(-MAX_EVENTS);
	if (!basePath || options.persist === false) return events;
	try {
		const runtimeDir = join(basePath, ".sf", "runtime");
		mkdirSync(runtimeDir, { recursive: true });
		const finalPath = join(runtimeDir, "crash-loop-events.json");
		const tmpPath = join(runtimeDir, `crash-loop-events.${process.pid}.tmp`);
		writeFileSync(tmpPath, JSON.stringify(events, null, 2), "utf8");
		renameSync(tmpPath, finalPath);
	} catch {
		// Detector persistence is best-effort. The in-memory result still matters.
	}
	return events;
}

function stringValue(value) {
	return typeof value === "string" && value.trim() ? value : null;
}
