import { execFileSync } from "node:child_process";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import {
	clearAutonomousSolverState,
	readAutonomousSolverState,
} from "../autonomous-solver.js";
import { isLockProcessAlive, readCrashLock } from "../crash-recovery.js";
import { sfRoot } from "../paths.js";
import { getUokRuns, isDbAvailable } from "../sf-db.js";
import { MessageBus } from "./message-bus.js";
import { summarizeParityHealth, writeParityReport } from "./parity-report.js";
import {
	clearUnitRuntimeRecord,
	decideUnitRuntimeDispatch,
	getUnitRuntimeState,
	isTerminalUnitRuntimeStatus,
	listUnitRuntimeRecords,
} from "./unit-runtime.js";

const DEFAULT_STALE_MS = 2 * 60 * 1000;

function diagnosticsPath(basePath) {
	return join(sfRoot(basePath), "runtime", "uok-diagnostics.json");
}

function parsePsRows(raw) {
	return String(raw ?? "")
		.split(/\r?\n/)
		.map((line) => line.trim())
		.filter(Boolean)
		.map((line) => {
			const match = line.match(/^(\d+)\s+(\d+)\s+(\S+)\s+(.+)$/);
			if (!match) return null;
			return {
				pid: Number(match[1]),
				ppid: Number(match[2]),
				stat: match[3],
				command: match[4],
			};
		})
		.filter(Boolean);
}

function readProcessRows() {
	try {
		return parsePsRows(
			execFileSync("ps", ["-eo", "pid=,ppid=,stat=,args="], {
				encoding: "utf-8",
				stdio: ["ignore", "pipe", "ignore"],
			}),
		);
	} catch {
		return [];
	}
}

function descendantPids(rows, pid) {
	const children = new Map();
	for (const row of rows) {
		const list = children.get(row.ppid) ?? [];
		list.push(row.pid);
		children.set(row.ppid, list);
	}
	const result = [];
	const queue = [...(children.get(pid) ?? [])];
	while (queue.length > 0) {
		const next = queue.shift();
		result.push(next);
		queue.push(...(children.get(next) ?? []));
	}
	return result;
}

function classifyRuntimeRecord(record, hasLiveLock, nowMs, staleMs) {
	const state = getUnitRuntimeState(record);
	let status = state.status;
	const terminal = isTerminalUnitRuntimeStatus(status);
	if (!hasLiveLock && !terminal) status = "stale";
	const lastProgressAt =
		state.lastProgressAt ?? record.updatedAt ?? record.startedAt;
	const lastHeartbeatAt =
		state.lastHeartbeatAt ?? record.updatedAt ?? record.startedAt;
	const lastSignalAt = Math.max(
		Number(lastProgressAt) || 0,
		Number(lastHeartbeatAt) || 0,
		Number(record.updatedAt) || 0,
	);
	const ageMs = lastSignalAt > 0 ? nowMs - lastSignalAt : null;
	let classification = "unknown";
	if (terminal) classification = "terminal";
	else if (!hasLiveLock) classification = "stale";
	else if (ageMs !== null && ageMs > staleMs)
		classification = "quiet-but-healthy";
	else classification = "running";
	return {
		unitType: String(record.unitType ?? ""),
		unitId: String(record.unitId ?? ""),
		status,
		phase: String(record.phase ?? "dispatched"),
		classification,
		projectionActive: !terminal,
		promptPath: record.promptPath ?? record.promptFile ?? null,
		promptHash: record.promptHash ?? null,
		toolSpansPath: record.toolSpansPath ?? record.tracePath ?? null,
		openToolSpans: Array.isArray(record.openToolSpans)
			? record.openToolSpans
			: [],
		lastHeartbeatAt: state.lastHeartbeatAt ?? null,
		lastProgressAt: state.lastProgressAt ?? null,
		lastOutputAt: state.lastOutputAt ?? null,
		outputPath: state.outputPath ?? null,
		watchdogReason: state.watchdogReason ?? null,
		dispatchDecision: decideUnitRuntimeDispatch(record),
	};
}

function normalizeExpectedUnit(expectedNext) {
	if (!expectedNext || expectedNext.action !== "dispatch") return null;
	if (!expectedNext.unitType || !expectedNext.unitId) return null;
	return {
		unitType: String(expectedNext.unitType),
		unitId: String(expectedNext.unitId),
	};
}

function latestEndedRun(runs) {
	return runs.find((run) => run.endedAt && run.status !== "started") ?? null;
}

/**
 * Build a durable UOK health verdict from process, parity, ledger, and runtime projections.
 *
 * Purpose: give headless/status/doctor/widget one operator-facing diagnosis instead of
 * requiring humans to correlate ps, auto.lock, UOK parity, DB rows, and runtime files.
 *
 * Consumer: UOK kernel startup, headless query, /uok status, doctor, and progress widget.
 */
export function synthesizeUokDiagnostics(basePath, options = {}) {
	const nowMs = options.nowMs ?? Date.now();
	const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
	const nowIso = new Date(nowMs).toISOString();
	const processRows = options.processRows ?? readProcessRows();
	const lock =
		options.lock === undefined ? readCrashLock(basePath) : options.lock;
	const lockAlive = lock ? isLockProcessAlive(lock) : false;
	const childPids = lockAlive
		? descendantPids(processRows, Number(lock.pid))
		: [];
	let records = listUnitRuntimeRecords(basePath);
	let runtimeUnits = records.map((record) =>
		classifyRuntimeRecord(record, lockAlive, nowMs, staleMs),
	);
	let activeRuntimeUnits = runtimeUnits.filter((unit) => unit.projectionActive);
	const preParityRuns = isDbAvailable() ? getUokRuns(20) : [];
	const preParityOpenRuns = preParityRuns.filter(
		(run) => run.status === "started" || !run.endedAt,
	);
	let report = null;
	let parityHealth = null;
	try {
		report = writeParityReport(basePath, nowMs);
		parityHealth = summarizeParityHealth(report);
	} catch {
		parityHealth = null;
	}
	const runs = isDbAvailable() ? getUokRuns(20) : preParityRuns;
	const openRuns = runs.filter(
		(run) => run.status === "started" || !run.endedAt,
	);
	const lastRun = runs[0] ?? null;
	const lastEnded = latestEndedRun(runs);
	const expectedUnit = normalizeExpectedUnit(options.expectedNext);
	if (options.repairStaleRuntimeProjection && activeRuntimeUnits.length > 0) {
		const mismatchedRecords = records.filter((record) => {
			const unit = classifyRuntimeRecord(record, lockAlive, nowMs, staleMs);
			if (!unit.projectionActive) return false;
			if (!expectedUnit) return !lockAlive;
			return (
				unit.unitType !== expectedUnit.unitType ||
				unit.unitId !== expectedUnit.unitId
			);
		});
		for (const record of mismatchedRecords) {
			clearUnitRuntimeRecord(basePath, record.unitType, record.unitId);
		}
		if (mismatchedRecords.length > 0) {
			records = listUnitRuntimeRecords(basePath);
			runtimeUnits = records.map((record) =>
				classifyRuntimeRecord(record, lockAlive, nowMs, staleMs),
			);
			activeRuntimeUnits = runtimeUnits.filter((unit) => unit.projectionActive);
		}
	}
	if (options.repairStaleRuntimeProjection && !lockAlive) {
		const solverState = readAutonomousSolverState(basePath);
		if (solverState?.status === "running") {
			clearAutonomousSolverState(basePath);
		}
	}
	const currentRuntimeUnit = lock
		? runtimeUnits.find(
				(unit) =>
					unit.unitType === lock.unitType && unit.unitId === lock.unitId,
			)
		: null;
	const issues = [];
	const recommendations = [];
	const signals = {
		lock: lock ? (lockAlive ? "active" : "stale") : "missing",
		parity: parityHealth?.status ?? "unknown",
		ledger: openRuns.length === 0 ? "consistent" : "open-runs",
		runtimeProjection: "ok",
		wrapper: "unknown",
	};

	if (lock && !lockAlive) {
		issues.push({
			code: "stale-lock",
			severity: "error",
			message: `Stale auto.lock detected for PID ${lock.pid}.`,
			evidence: { lock },
		});
		recommendations.push("Clear stale auto.lock before dispatch.");
	}
	if (parityHealth && !parityHealth.ok) {
		issues.push({
			code: "uok-parity-degraded",
			severity: "warning",
			message: `UOK parity degraded: ${parityHealth.current.criticalMismatches} critical mismatch(es), ${parityHealth.current.missingExitEvents} missing exit(s).`,
			evidence: parityHealth.current,
		});
		recommendations.push("Reconcile UOK parity before mutating git state.");
	}
	if (openRuns.length > 0 && !lockAlive) {
		issues.push({
			code: "open-ledger-without-live-lock",
			severity: "error",
			message: `UOK ledger has ${openRuns.length} started run(s) without a live auto.lock owner.`,
			evidence: {
				runIds: openRuns.map((run) => run.runId),
				recoveredBeforeDiagnostics: preParityOpenRuns.length > openRuns.length,
			},
		});
		recommendations.push(
			"Mark orphaned UOK runs recovered or restart from lock owner.",
		);
	}
	if (!lockAlive && activeRuntimeUnits.length > 0) {
		signals.runtimeProjection = "stale";
		issues.push({
			code: "stale-runtime-projection",
			severity: "error",
			message: `${activeRuntimeUnits.length} active runtime projection(s) exist without a live auto.lock owner.`,
			evidence: {
				units: activeRuntimeUnits.map(
					(unit) => `${unit.unitType} ${unit.unitId}`,
				),
			},
		});
		recommendations.push("Regenerate or clear stale runtime unit projections.");
	}
	if (expectedUnit && activeRuntimeUnits.length > 0) {
		const matchesExpected = activeRuntimeUnits.some(
			(unit) =>
				unit.unitType === expectedUnit.unitType &&
				unit.unitId === expectedUnit.unitId,
		);
		if (!matchesExpected) {
			signals.runtimeProjection = "mismatch";
			issues.push({
				code: "db-projection-unit-mismatch",
				severity: "warning",
				message: `DB dispatch preview is ${expectedUnit.unitType} ${expectedUnit.unitId}, but runtime projection shows ${activeRuntimeUnits.map((unit) => `${unit.unitType} ${unit.unitId}`).join(", ")}.`,
				evidence: { expectedUnit, activeRuntimeUnits },
			});
			recommendations.push(
				"Prefer DB dispatch state and repair runtime projection drift.",
			);
		}
	}
	if (!lock && lastEnded?.status === "ok") {
		const liveSfProcesses = processRows.filter(
			(row) =>
				row.pid !== process.pid &&
				/(^|\s)(sf\s+autonomous|sf\s+auto|node\s+.*dist\/loader\.js\s+autonomous)($|\s)/.test(
					row.command,
				) &&
				!row.command.includes("headless --output-format json query"),
		);
		if (liveSfProcesses.length > 0) {
			signals.wrapper = "maybe-live-after-kernel-exit";
			issues.push({
				code: "kernel-exited-wrapper-maybe-live",
				severity: "warning",
				message:
					"Latest UOK kernel run exited ok and no auto.lock exists, but sf process(es) are still alive.",
				evidence: { pids: liveSfProcesses.slice(0, 8).map((row) => row.pid) },
			});
			recommendations.push(
				"Classify live sf wrapper processes before assuming autonomous is active.",
			);
		} else {
			signals.wrapper = "clear";
		}
	}

	let classification = "healthy";
	if (issues.some((issue) => issue.severity === "error")) {
		classification = "needs-repair";
	} else if (issues.length > 0) {
		classification = "degraded";
	} else if (
		lockAlive &&
		runtimeUnits.some((unit) => unit.classification === "quiet-but-healthy")
	) {
		classification = "quiet-but-healthy";
	} else if (lockAlive) {
		classification = "running";
	}
	const reportPath = diagnosticsPath(basePath);
	return {
		schemaVersion: 1,
		generatedAt: nowIso,
		verdict: issues.some((issue) => issue.severity === "error")
			? "degraded"
			: issues.length > 0
				? "attention"
				: "clear",
		classification,
		signals,
		currentUnit: lock
			? {
					unitType: lock.unitType ?? null,
					unitId: lock.unitId ?? null,
					pid: lock.pid ?? null,
					sessionFile: lock.sessionFile ?? null,
					promptPath:
						lock.promptPath ??
						lock.promptFile ??
						currentRuntimeUnit?.promptPath ??
						null,
					promptHash: lock.promptHash ?? currentRuntimeUnit?.promptHash ?? null,
					toolSpansPath:
						lock.toolSpansPath ??
						lock.tracePath ??
						currentRuntimeUnit?.toolSpansPath ??
						null,
					openToolSpans: currentRuntimeUnit?.openToolSpans ?? [],
					startedAt: lock.startedAt ?? null,
					unitStartedAt: lock.unitStartedAt ?? null,
					childPids,
				}
			: null,
		latestRun: lastRun
			? {
					runId: lastRun.runId,
					sessionId: lastRun.sessionId,
					status: lastRun.status,
					startedAt: lastRun.startedAt,
					endedAt: lastRun.endedAt,
					path: lastRun.path,
					error: lastRun.error,
				}
			: null,
		runtimeUnits,
		issues,
		recommendations,
		reportPath,
	};
}

export function writeUokDiagnostics(basePath, options = {}) {
	const diagnostics = synthesizeUokDiagnostics(basePath, options);
	const path = diagnosticsPath(basePath);
	mkdirSync(join(sfRoot(basePath), "runtime"), { recursive: true });
	writeFileSync(path, `${JSON.stringify(diagnostics, null, 2)}\n`, "utf-8");
	publishDiagnosticsToObserver(basePath, diagnostics);
	return diagnostics;
}

function publishDiagnosticsToObserver(basePath, diagnostics) {
	if (!isDbAvailable() || diagnostics.verdict === "clear") return;
	try {
		const issueCodes = diagnostics.issues.map((issue) => issue.code).join(",");
		const unit = diagnostics.currentUnit
			? `${diagnostics.currentUnit.unitType ?? "unknown"}:${diagnostics.currentUnit.unitId ?? "unknown"}`
			: "no-unit";
		const bus = new MessageBus(basePath);
		bus.sendOnce(
			"uok-diagnostics",
			"uok-observer",
			`UOK diagnostics ${diagnostics.verdict}/${diagnostics.classification}: ${issueCodes || "attention"}`,
			{
				verdict: diagnostics.verdict,
				classification: diagnostics.classification,
				issueCodes: diagnostics.issues.map((issue) => issue.code),
				reportPath: diagnostics.reportPath,
				generatedAt: diagnostics.generatedAt,
				currentUnit: diagnostics.currentUnit,
			},
			`uok-diagnostics:${diagnostics.verdict}:${diagnostics.classification}:${unit}:${issueCodes}`,
		);
	} catch {
		/* observer messages are diagnostic-only; never mask status/doctor output */
	}
}

export function readUokDiagnostics(basePath) {
	const path = diagnosticsPath(basePath);
	if (!existsSync(path)) return null;
	try {
		return JSON.parse(readFileSync(path, "utf-8"));
	} catch {
		return null;
	}
}
