import { createHash, randomUUID } from "node:crypto";
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { atomicWriteSync } from "./atomic-write.js";
import { withFileLockSync } from "./file-lock.js";
import { sfRuntimeRoot } from "./paths.js";
import { logWarning } from "./workflow-logger.js";

const WORKFLOW_EVENT_SCHEMA_VERSION = 1;

// ─── Session ID ───────────────────────────────────────────────────────────
/**
 * Engine-generated session ID — stable for the lifetime of this process.
 * Agents can reference this to correlate all events from one run.
 */
const ENGINE_SESSION_ID = randomUUID();
export function getSessionId() {
	return ENGINE_SESSION_ID;
}
// ─── appendEvent ─────────────────────────────────────────────────────────
/**
 * Append one event to .sf/event-log.jsonl.
 * Computes a content hash from cmd+params (deterministic, independent of ts/actor/session).
 * Creates .sf directory if needed.
 */
export function appendEvent(basePath, event) {
	const hash = createHash("sha256")
		.update(JSON.stringify({ cmd: event.cmd, params: event.params }))
		.digest("hex")
		.slice(0, 16);
	const fullEvent = {
		schemaVersion: WORKFLOW_EVENT_SCHEMA_VERSION,
		v: 2,
		...event,
		hash,
		session_id: ENGINE_SESSION_ID,
	};
	const dir = sfRuntimeRoot(basePath);
	mkdirSync(dir, { recursive: true });
	appendFileSync(
		join(dir, "event-log.jsonl"),
		JSON.stringify(fullEvent) + "\n",
		"utf-8",
	);
}
// ─── readEvents ──────────────────────────────────────────────────────────
/**
 * Read all events from a JSONL file.
 * Returns empty array if file doesn't exist.
 * Corrupted lines are skipped with stderr warning.
 */
export function readEvents(logPath) {
	if (!existsSync(logPath)) {
		return [];
	}
	const content = readFileSync(logPath, "utf-8");
	const lines = content.split("\n").filter((l) => l.length > 0);
	const events = [];
	let corruptCount = 0;
	for (let i = 0; i < lines.length; i++) {
		const line = lines[i];
		try {
			const event = normalizeWorkflowEvent(JSON.parse(line));
			if (event) events.push(event);
		} catch (_err) {
			corruptCount++;
			const snippet = line.slice(0, 80);
			logWarning(
				"event-log",
				`skipping corrupted event at ${logPath}:${i + 1} (${line.length} bytes): ${snippet}${line.length > 80 ? "..." : ""}`,
			);
		}
	}
	// Threshold check: warn at 1% corruption, fail at 10% corruption.
	// Systematic corruption (e.g. crash mid-write) silently loses events;
	// failing fast forces recovery from manifest backup rather than divergence.
	const corruptRatio = lines.length > 0 ? corruptCount / lines.length : 0;
	if (corruptRatio >= 0.1) {
		throw new Error(
			`Event log corruption rate ${(corruptRatio * 100).toFixed(1)}% exceeds fatal threshold (10%). ` +
				`Corrupted lines: ${corruptCount}/${lines.length}. ` +
				`Consider recovery from manifest backup.`,
		);
	}
	if (corruptRatio >= 0.01) {
		logWarning(
			"event-log",
			`Elevated corruption rate: ${(corruptRatio * 100).toFixed(1)}% of ${lines.length} lines unreadable. Consider event-log recovery.`,
		);
	}
	return events;
}

function normalizeWorkflowEvent(event) {
	if (!event || typeof event !== "object" || Array.isArray(event)) return null;
	const schemaVersion = event.schemaVersion ?? WORKFLOW_EVENT_SCHEMA_VERSION;
	if (schemaVersion !== WORKFLOW_EVENT_SCHEMA_VERSION) return null;
	return {
		...event,
		schemaVersion,
	};
}
// ─── findForkPoint ───────────────────────────────────────────────────────
/**
 * Find the index of the last common event between two logs by comparing hashes.
 * Returns -1 if the first events differ (completely diverged).
 * If one log is a prefix of the other, returns length of shorter - 1.
 */
export function findForkPoint(logA, logB) {
	const minLen = Math.min(logA.length, logB.length);
	let lastCommon = -1;
	for (let i = 0; i < minLen; i++) {
		if (logA[i].hash === logB[i].hash) {
			lastCommon = i;
		} else {
			break;
		}
	}
	return lastCommon;
}
// ─── compactMilestoneEvents ─────────────────────────────────────────────────
/**
 * Archive a milestone's events from the active log to a separate file.
 * Active log retains only events from other milestones.
 * Archived file is kept on disk for forensics.
 *
 * @param basePath - Project root (parent of .sf/)
 * @param milestoneId - The milestone whose events should be archived
 * @returns { archived: number } — count of events moved to archive
 */
export function compactMilestoneEvents(basePath, milestoneId) {
	const runtimeDir = sfRuntimeRoot(basePath);
	const logPath = join(runtimeDir, "event-log.jsonl");
	const archivePath = join(
		runtimeDir,
		`event-log-${milestoneId}.jsonl.archived`,
	);
	return withFileLockSync(logPath, () => {
		const allEvents = readEvents(logPath);
		// Single-pass partition to halve the work (per reviewer agent)
		const toArchive = [];
		const remaining = [];
		for (const e of allEvents) {
			if (e.params.milestoneId === milestoneId) {
				toArchive.push(e);
			} else {
				remaining.push(e);
			}
		}
		if (toArchive.length === 0) {
			return { archived: 0 };
		}
		// Write archived events to .jsonl.archived file (crash-safe)
		atomicWriteSync(
			archivePath,
			toArchive.map((e) => JSON.stringify(e)).join("\n") + "\n",
		);
		// Truncate active log to remaining events only
		atomicWriteSync(
			logPath,
			remaining.length > 0
				? remaining.map((e) => JSON.stringify(e)).join("\n") + "\n"
				: "",
		);
		return { archived: toArchive.length };
	});
}
