/**
 * last-green.js — Last-green dispatch ledger for the smoke fixture regression firewall.
 *
 * Purpose: durable, comparable record of the last successful smoke fixture run.
 * S03 (crash-loop classifier) reads this ledger; S04 (auto-rollback) writes it.
 * Without this slice the regression firewall has no memory of what "green" looks like.
 *
 * Consumer: run-unit-inline.js (write), S03 crash-loop classifier (read),
 *   S04 rollback controller (read + write flag).
 */
import {
	constants as fsConstants,
	mkdirSync,
	readFileSync,
	renameSync,
	unlinkSync,
	writeFileSync,
} from "node:fs";
import { dirname, join } from "node:path";
import { createHash } from "node:crypto";

import { getExperimentalFlag } from "./experimental.js";

/** Ledger file path relative to basePath. */
const LEDGER_FILE = ".sf/runtime/last-green.json";

/** Maximum ledger entries to retain (newest first). */
const MAX_ENTRIES = 50;

/** Ledger file version for forward-compatibility. */
const LEDGER_VERSION = 1;

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/** Derive a structural checksum over ok, exitCode, and a hash of output. */
function makeChecksum(result) {
	const h = createHash("sha256");
	h.update(String(result.ok));
	h.update(String(result.exitCode ?? 0));
	h.update(result.output ?? "");
	return h.digest("hex").slice(0, 16);
}

/** Load the runtime directory, creating it if absent. */
function ensureRuntimeDir(basePath) {
	const dir = join(basePath, ".sf", "runtime");
	mkdirSync(dir, { recursive: true });
	return dir;
}

/** Read the ledger, returning [] if absent or unreadable. */
function readLedger(basePath) {
	const filePath = join(basePath, LEDGER_FILE);
	try {
		return JSON.parse(readFileSync(filePath, "utf-8"));
	} catch {
		return [];
	}
}

/** Write the ledger atomically (rename is atomic on POSIX). */
function writeLedger(basePath, ledger) {
	const dir = ensureRuntimeDir(basePath);
	const filePath = join(dir, "last-green.json");
	const tmpPath = join(dir, `last-green.${process.pid}.tmp`);
	writeFileSync(tmpPath, JSON.stringify(ledger, null, 2), "utf-8");
	try {
		// overwrite: false would throw on pre-existing file — we want to replace.
		// Use the synchronous rename which is atomic on POSIX.
		renameSync(tmpPath, filePath);
	} catch (err) {
		// Fallback: direct write if rename fails (e.g. cross-device).
		writeFileSync(filePath, JSON.stringify(ledger, null, 2), "utf-8");
	}
}

// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------

/**
 * Write a last-green entry after a successful smoke fixture dispatch.
 * Guards on the `smoke_gate` experimental flag — no-op if the flag is false.
 *
 * Purpose: persist a durable record of the last green run so S03 can detect
 * regressions and S04 can roll back to a known-good state.
 *
 * Consumer: run-unit-inline.js after successful subagent call for SMOKE_FIXTURE_MILESTONE_ID.
 *
 * @param {string} basePath - Project root (contains .sf/).
 * @param {object} result - Dispatch result with ok, exitCode, output, elapsedMs, dispatchCount.
 * @param {object} [metadata] - Optional fixture metadata: milestoneId, unitType, unitId, runId, traceId.
 * @returns {boolean} true if the entry was written; false if suppressed or errored.
 */
export function writeLastGreenRecord(basePath, result, metadata = {}) {
	if (getExperimentalFlag("smoke_gate") !== true) {
		return false;
	}

	const ledger = readLedger(basePath);

	const entry = {
		version: LEDGER_VERSION,
		fixture: {
			milestoneId: metadata.milestoneId ?? null,
			unitType: metadata.unitType ?? null,
			unitId: metadata.unitId ?? null,
			runId: metadata.runId ?? null,
			traceId: metadata.traceId ?? null,
		},
		result: {
			ok: result.ok,
			exitCode: result.exitCode ?? 0,
			outputLength: (result.output ?? "").length,
			outputPreview: ((result.output ?? "").slice(0, 100)),
			elapsedMs: result.elapsedMs ?? 0,
			dispatchCount: result.dispatchCount ?? 0,
		},
		checksum: makeChecksum(result),
		timestamp: new Date().toISOString(),
	};

	ledger.unshift(entry); // newest first
	if (ledger.length > MAX_ENTRIES) {
		ledger.length = MAX_ENTRIES;
	}

	try {
		writeLedger(basePath, ledger);
		return true;
	} catch (err) {
		// Write failure is non-fatal — the dispatch result is more important.
		return false;
	}
}

/**
 * Read and parse the full ledger array.
 *
 * Purpose: S03 crash-loop classifier reads the full ledger to analyse trends.
 *
 * Consumer: S03 crash-loop classifier (read-only).
 *
 * @param {string} basePath
 * @returns {Array|null} Parsed ledger array, or null on file-not-found / parse error.
 */
export function readLastGreenRecord(basePath) {
	const filePath = join(basePath, LEDGER_FILE);
	try {
		return JSON.parse(readFileSync(filePath, "utf-8"));
	} catch {
		return null;
	}
}

/**
 * Return the most-recent ledger entry (index 0) or null.
 *
 * Purpose: convenience accessor so callers don't have to [0]-into the array;
 * avoids the array-vs-entry footgun surfaced in the adversarial review.
 *
 * Consumer: S04 rollback controller; S03 crash-loop detection.
 *
 * @param {string} basePath
 * @returns {object|null} Latest entry, or null if ledger is empty or absent.
 */
export function getLastGreenEntry(basePath) {
	const ledger = readLastGreenRecord(basePath);
	if (!ledger || ledger.length === 0) return null;
	return ledger[0];
}

/**
 * Compare two last-green entries and report whether they match within tolerance.
 *
 * Purpose: detect regressions after a new dispatch run against the last known
 * green baseline. ok and exitCode are strict; output and elapsedMs tolerate
 * bounded variance (model version, system load).
 *
 * Consumer: S03 crash-loop classifier; manual operator diff.
 *
 * @param {object} current - Entry from the most recent run.
 * @param {object} last - Entry from the last-green baseline.
 * @returns {{ match: boolean, delta: object }} match is false if any delta exceeds tolerance.
 */
export function compareToLastGreen(current, last) {
	if (!current || !last) {
		return { match: false, delta: {} };
	}

	const cr = current.result ?? {};
	const lr = last.result ?? {};

	const ok_delta = cr.ok === lr.ok;
	const exitcode_delta = cr.exitCode === lr.exitCode;

	// outputLength ±10%
	const output_delta =
		cr.outputLength !== undefined && lr.outputLength !== undefined
			? Math.abs(cr.outputLength - lr.outputLength) / Math.max(lr.outputLength, 1) <= 0.1
			: true;

	// elapsedMs ±20%
	const elapsed_delta =
		cr.elapsedMs !== undefined && lr.elapsedMs !== undefined
			? Math.abs(cr.elapsedMs - lr.elapsedMs) / Math.max(lr.elapsedMs, 1) <= 0.2
			: true;

	const match = ok_delta && exitcode_delta && output_delta && elapsed_delta;

	return {
		match,
		delta: { ok_delta, exitcode_delta, output_delta, elapsed_delta },
	};
}

/**
 * Delete the ledger file.
 *
 * Purpose: reset the regression baseline when the milestone definition changes
 * or a new green baseline must be established.
 *
 * Consumer: S04 rollback controller; `sf doctor` cleanup.
 *
 * @param {string} basePath
 * @returns {boolean} true if the file was deleted or did not exist.
 */
export function clearLastGreen(basePath) {
	const filePath = join(basePath, LEDGER_FILE);
	try {
		unlinkSync(filePath);
		return true;
	} catch (err) {
		if (err.code === "ENOENT") return true;
		return false;
	}
}