import {
	appendFileSync,
	existsSync,
	mkdirSync,
	readFileSync,
	renameSync,
	writeFileSync,
} from "node:fs";
import { join } from "node:path";
import { sfRoot } from "../paths.js";
import { getUokRuns, isDbAvailable, recordUokRunExit } from "../sf-db.js";

const UOK_PARITY_SCHEMA_VERSION = 1;
export const UNMATCHED_RUN_STALE_MS = 30 * 60 * 1000;

function parityLogPath(basePath) {
	return join(sfRoot(basePath), "runtime", "uok-parity.jsonl");
}
function reportPath(basePath) {
	return join(sfRoot(basePath), "runtime", "uok-parity-report.json");
}
function increment(bucket, key) {
	const normalized = key && key.trim().length > 0 ? key : "unknown";
	bucket[normalized] = (bucket[normalized] ?? 0) + 1;
}
function isParityDiffEvent(value) {
	return (
		value !== null &&
		typeof value === "object" &&
		value.kind === "parity-diff" &&
		typeof value.plane === "string"
	);
}
function timestampMs(value) {
	const ms = Date.parse(String(value ?? ""));
	return Number.isFinite(ms) ? ms : undefined;
}
function isFreshTimestamp(value, nowMs, staleMs) {
	const ms = timestampMs(value);
	return ms !== undefined && nowMs - ms <= staleMs;
}
function hasLiveAutoLock(basePath) {
	const lockPath = join(sfRoot(basePath), "auto.lock");
	if (!existsSync(lockPath)) return false;
	try {
		const lock = JSON.parse(readFileSync(lockPath, "utf-8"));
		const pid = Number(lock.pid);
		if (!Number.isFinite(pid) || pid <= 0) return false;
		process.kill(pid, 0);
		return true;
	} catch {
		return false;
	}
}
function recoverOrphanedStartedLedgerRuns(basePath, ledgerRuns, nowIso) {
	if (hasLiveAutoLock(basePath)) return 0;
	let recovered = 0;
	for (const run of ledgerRuns) {
		if (run.status !== "started" || run.endedAt) continue;
		recordUokRunExit({
			runId: run.runId,
			sessionId: run.sessionId,
			path: run.path,
			flags: run.flags ?? {},
			status: "recovered",
			endedAt: nowIso,
			error: "uok recovered missing exit: no live auto.lock owner",
		});
		recovered += 1;
	}
	return recovered;
}
export function parseParityEvents(raw) {
	let malformedCount = 0;
	const result = raw
		.split("\n")
		.filter((line) => line.trim().length > 0)
		.map((line) => {
			try {
				const parsed = normalizeParityEvent(JSON.parse(line));
				if (!parsed) {
					malformedCount++;
					return null;
				}
				if (isParityDiffEvent(parsed)) return parsed;
				return parsed;
			} catch {
				malformedCount++;
				return {
					status: "error",
					error: "invalid parity json line",
				};
			}
		})
		.filter(Boolean);
	if (malformedCount > 0) {
		console.error(
			`[parity-report] Dropped ${malformedCount} malformed parity event(s)`,
		);
	}
	return result;
}
function normalizeParityEvent(event) {
	if (!event || typeof event !== "object" || Array.isArray(event)) return null;
	const schemaVersion = event.schemaVersion ?? UOK_PARITY_SCHEMA_VERSION;
	if (schemaVersion !== UOK_PARITY_SCHEMA_VERSION) return null;
	return {
		...event,
		schemaVersion,
	};
}
export function buildParityReport(
	events,
	sourcePath,
	nowMs = Date.now(),
	staleMs = UNMATCHED_RUN_STALE_MS,
	ledgerRuns = [],
) {
	const paths = {};
	const statuses = {};
	const criticalMismatches = [];
	const historicalCriticalMismatches = [];
	const runs = new Map();
	for (const run of ledgerRuns) {
		runs.set(run.runId, {
			runId: run.runId,
			path: run.path,
			status: run.status,
			error: run.error,
			sessionId: run.sessionId,
			enterEvents: 1,
			exitEvents: run.endedAt ? 1 : 0,
			enteredAt: run.startedAt,
			exitedAt: run.endedAt,
			source: "ledger",
		});
	}
	const hasLedgerRuns = ledgerRuns.length > 0;
	let currentErrorEvents = 0;
	let historicalErrorEvents = 0;
	let enterEvents = 0;
	let exitEvents = 0;
	let legacyEnterEvents = 0;
	let legacyExitEvents = 0;
	let totalDiffs = 0;
	const divergencesByPlane = {
		plan: 0,
		graph: 0,
		"model-policy": 0,
		"audit-envelope": 0,
		gitops: 0,
	};
	for (const event of events) {
		if (isParityDiffEvent(event)) {
			totalDiffs += 1;
			if (!event.match) {
				divergencesByPlane[event.plane] =
					(divergencesByPlane[event.plane] ?? 0) + 1;
				const target = isFreshTimestamp(event.ts, nowMs, staleMs)
					? criticalMismatches
					: historicalCriticalMismatches;
				if (event.divergence) {
					target.push(`[${event.plane}] ${event.divergence}`);
				} else {
					target.push(`[${event.plane}] divergence (turn=${event.turnId})`);
				}
			}
			continue;
		}
		// Kernel heartbeat event
		const heartbeat = event;
		increment(paths, heartbeat.path);
		// Only tally status from exit events — enter events intentionally omit
		// the status field, so counting them inflates the "unknown" bucket.
		if (heartbeat.phase === "exit") {
			increment(statuses, heartbeat.status);
		}
		const runId =
			typeof heartbeat.runId === "string" && heartbeat.runId.trim().length > 0
				? heartbeat.runId
				: undefined;
		if (heartbeat.phase === "enter") {
			enterEvents += 1;
			if (runId && !hasLedgerRuns) {
				const current = runs.get(runId) ?? {
					runId,
					path: heartbeat.path,
					enterEvents: 0,
					exitEvents: 0,
					enteredAt: heartbeat.ts,
					exitedAt: undefined,
				};
				current.enterEvents += 1;
				current.path = current.path ?? heartbeat.path;
				current.enteredAt = current.enteredAt ?? heartbeat.ts;
				runs.set(runId, current);
			} else if (!runId) {
				legacyEnterEvents += 1;
			}
		}
		if (heartbeat.phase === "exit") {
			exitEvents += 1;
			if (runId && !hasLedgerRuns) {
				const current = runs.get(runId) ?? {
					runId,
					path: heartbeat.path,
					enterEvents: 0,
					exitEvents: 0,
					enteredAt: undefined,
					exitedAt: heartbeat.ts,
				};
				current.exitEvents += 1;
				current.path = current.path ?? heartbeat.path;
				current.exitedAt = heartbeat.ts;
				runs.set(runId, current);
			} else if (!runId) {
				legacyExitEvents += 1;
			}
		}
		if (heartbeat.status === "error") {
			if (hasLedgerRuns) continue;
			const message = heartbeat.error ?? "parity event reported error";
			if (isFreshTimestamp(heartbeat.ts, nowMs, staleMs)) {
				currentErrorEvents += 1;
				criticalMismatches.push(message);
			} else {
				historicalErrorEvents += 1;
				historicalCriticalMismatches.push(message);
			}
		}
	}
	const unmatchedRuns = Array.from(runs.values()).filter(
		(run) => run.enterEvents > run.exitEvents,
	);
	const freshUnmatchedRuns = unmatchedRuns.filter((run) =>
		isFreshTimestamp(run.enteredAt, nowMs, staleMs),
	);
	const historicalUnmatchedRuns = unmatchedRuns.filter(
		(run) => !isFreshTimestamp(run.enteredAt, nowMs, staleMs),
	);
	const legacyMissingExitEvents = Math.max(
		0,
		legacyEnterEvents - legacyExitEvents,
	);
	if (hasLedgerRuns) {
		for (const run of ledgerRuns) {
			if (run.status !== "error") continue;
			if (isFreshTimestamp(run.endedAt ?? run.updatedAt, nowMs, staleMs)) {
				currentErrorEvents += 1;
				criticalMismatches.push(run.error ?? "uok run reported error");
			} else {
				historicalErrorEvents += 1;
				historicalCriticalMismatches.push(
					run.error ?? "uok run reported historical error",
				);
			}
		}
	}
	const missingExitEvents = freshUnmatchedRuns.length;
	if (freshUnmatchedRuns.length > 0) {
		const exampleIds = freshUnmatchedRuns
			.slice(0, 3)
			.map((run) => run.runId)
			.join(", ");
		criticalMismatches.push(
			`uok run enter/exit mismatch: ${freshUnmatchedRuns.length} run(s) missing exit${exampleIds ? ` (${exampleIds})` : ""}`,
		);
	}
	return {
		schemaVersion: UOK_PARITY_SCHEMA_VERSION,
		generatedAt: new Date(nowMs).toISOString(),
		sourcePath,
		totalEvents: events.length,
		paths,
		statuses,
		criticalMismatches,
		historicalCriticalMismatches,
		currentErrorEvents,
		historicalErrorEvents,
		enterEvents,
		exitEvents,
		missingExitEvents,
		legacyMissingExitEvents,
		ledgerRunCount: ledgerRuns.length,
		unmatchedRuns,
		freshUnmatchedRuns,
		historicalUnmatchedRuns,
		totalDiffs,
		divergencesByPlane,
	};
}

export function hasCurrentParityWarning(report) {
	const criticalMismatches = report?.criticalMismatches ?? [];
	const currentErrors =
		report?.currentErrorEvents ?? report?.statuses?.error ?? 0;
	return criticalMismatches.length > 0 || currentErrors > 0;
}

export function summarizeParityHealth(report) {
	const currentCriticalMismatches = report?.criticalMismatches ?? [];
	const historicalCriticalMismatches =
		report?.historicalCriticalMismatches ?? [];
	const currentMissingExitEvents = report?.missingExitEvents ?? 0;
	const currentErrorEvents =
		report?.currentErrorEvents ?? report?.statuses?.error ?? 0;
	const legacyMissingExitEvents = report?.legacyMissingExitEvents ?? 0;
	const historicalErrorEvents = report?.historicalErrorEvents ?? 0;
	const historicalUnmatchedRuns = report?.historicalUnmatchedRuns?.length ?? 0;
	const ok =
		currentCriticalMismatches.length === 0 &&
		currentMissingExitEvents === 0 &&
		currentErrorEvents === 0;
	return {
		ok,
		status: ok ? "ok" : "degraded",
		current: {
			criticalMismatches: currentCriticalMismatches.length,
			missingExitEvents: currentMissingExitEvents,
			errorEvents: currentErrorEvents,
		},
		historical: {
			criticalMismatches: historicalCriticalMismatches.length,
			legacyMissingExitEvents,
			errorEvents: historicalErrorEvents,
			unmatchedRuns: historicalUnmatchedRuns,
		},
	};
}

export function writeParityReport(basePath, nowMs = Date.now()) {
	const sourcePath = parityLogPath(basePath);
	const raw = existsSync(sourcePath) ? readFileSync(sourcePath, "utf-8") : "";
	let ledgerRuns = [];
	try {
		ledgerRuns = isDbAvailable() ? getUokRuns() : [];
		if (ledgerRuns.length > 0 && isDbAvailable()) {
			const recovered = recoverOrphanedStartedLedgerRuns(
				basePath,
				ledgerRuns,
				new Date(nowMs).toISOString(),
			);
			if (recovered > 0) ledgerRuns = getUokRuns();
		}
	} catch {
		ledgerRuns = [];
	}
	const report = buildParityReport(
		parseParityEvents(raw),
		sourcePath,
		nowMs,
		UNMATCHED_RUN_STALE_MS,
		ledgerRuns,
	);
	mkdirSync(join(sfRoot(basePath), "runtime"), { recursive: true });
	const finalPath = reportPath(basePath);
	const tmpPath = `${finalPath}.tmp`;
	writeFileSync(tmpPath, JSON.stringify(report, null, 2) + "\n", "utf-8");
	renameSync(tmpPath, finalPath);
	return report;
}
/**
 * Atomically append a ParityDiffEvent to the parity log.
 * Uses appendFileSync (O_APPEND is atomic for small writes at the OS level).
 * Never throws — diff emission must not block or break kernel execution.
 */
export function writeParityDiff(basePath, event) {
	try {
		mkdirSync(join(sfRoot(basePath), "runtime"), { recursive: true });
		const logPath = parityLogPath(basePath);
		appendFileSync(
			logPath,
			`${JSON.stringify({ schemaVersion: UOK_PARITY_SCHEMA_VERSION, ...event })}\n`,
			"utf-8",
		);
	} catch {
		// Best-effort: diff emission must never break orchestration.
	}
}
/**
 * Append a best-effort UOK heartbeat event for runtime diagnostics.
 *
 * Purpose: keep the parity log useful when a turn or plane fails before the
 * aggregate report writer runs.
 *
 * Consumer: kernel and loop-adapter emit enter/exit events that parity-report
 * summarizes into commit-gating signals.
 */
export function writeParityHeartbeat(basePath, event) {
	try {
		mkdirSync(join(sfRoot(basePath), "runtime"), { recursive: true });
		appendFileSync(
			parityLogPath(basePath),
			`${JSON.stringify({ schemaVersion: UOK_PARITY_SCHEMA_VERSION, ...event })}\n`,
			"utf-8",
		);
	} catch {
		// Best-effort: diagnostics must never break orchestration.
	}
}
/**
 * Shallow-equal comparison for plane decisions.
 * Returns true when legacy and uok produce the same serialized shape.
 * Handles null/undefined gracefully: null vs null → true, null vs [] → false.
 */
export function shallowEqualDecisions(legacy, uok) {
	if (legacy === uok) return true;
	if (
		legacy === null ||
		legacy === undefined ||
		uok === null ||
		uok === undefined
	)
		return false;
	const legacyJson = JSON.stringify(legacy);
	const uokJson = JSON.stringify(uok);
	return legacyJson === uokJson;
}
/**
 * Capture a plane diff. If legacy and UOK outputs diverge, builds a human-readable
 * divergence summary. Returns the constructed ParityDiffEvent.
 */
export function captureParityDiff(args) {
	const match = shallowEqualDecisions(args.legacy, args.uok);
	let divergence;
	if (!match) {
		const legacySummary = summarizeDecision(args.legacy);
		const uokSummary = summarizeDecision(args.uok);
		divergence = `legacy=${legacySummary} uok=${uokSummary}`;
	}
	const event = {
		kind: "parity-diff",
		ts: new Date().toISOString(),
		sessionId: args.sessionId,
		turnId: args.turnId,
		plane: args.plane,
		legacy: args.legacy,
		uok: args.uok,
		match,
		divergence,
	};
	writeParityDiff(args.basePath, event);
	return event;
}
function summarizeDecision(value) {
	if (value === null || value === undefined) return String(value);
	if (typeof value === "string") return `"${value.slice(0, 60)}"`;
	if (typeof value === "number" || typeof value === "boolean")
		return String(value);
	try {
		const str = JSON.stringify(value);
		return str.length > 80 ? str.slice(0, 80) + "…" : str;
	} catch {
		return "[unserializable]";
	}
}
