import {
	existsSync,
	lstatSync,
	readdirSync,
	readFileSync,
	realpathSync,
	rmSync,
	statSync,
} from "node:fs";
import { basename, dirname, join } from "node:path";
import {
	clearLock,
	isLockProcessAlive,
	readCrashLock,
} from "./crash-recovery.js";
import { saveFile } from "./files.js";
import {
	ensureGitignore,
	isSfGitignored,
	SF_RUNTIME_PATTERNS,
} from "./gitignore.js";
import { recoverFailedMigration } from "./migrate-external.js";
import {
	nativeForEachRef,
	nativeIsRepo,
	nativeUpdateRef,
} from "./native-git-bridge.js";
import { milestonesDir, resolveSfRootFile, sfRoot } from "./paths.js";
import { cleanNumberedSfVariants } from "./repo-identity.js";
import { detectScaffoldDrift } from "./scaffold-drift.js";
import {
	isSessionStale,
	readAllSessionStatuses,
	removeSessionStatus,
} from "./session-status-io.js";
import { deriveState } from "./state.js";
import { writeUokDiagnostics } from "./uok/diagnostic-synthesis.js";
import { listUnitRuntimeRecords } from "./uok/unit-runtime.js";
import { getAuditEmitFailureCount } from "./workflow-logger.js";

const ACTIVE_UNIT_RUNTIME_STATUSES = new Set([
	"queued",
	"running",
	"progress",
	"repair",
]);
const ACTIVE_UNIT_RUNTIME_PHASES = new Set([
	"dispatched",
	"running",
	"progress",
	"repair-dispatched",
	"runaway-warning-sent",
	"runaway-final-warning-sent",
]);

function isActiveUnitRuntimeRecord(record) {
	return (
		ACTIVE_UNIT_RUNTIME_STATUSES.has(String(record?.status ?? "")) ||
		ACTIVE_UNIT_RUNTIME_PHASES.has(String(record?.phase ?? ""))
	);
}

export async function checkRuntimeHealth(
	basePath,
	issues,
	fixesApplied,
	shouldFix,
) {
	const root = sfRoot(basePath);
	let crashLock = null;
	let crashLockAlive = false;
	// ── Stale crash lock ──────────────────────────────────────────────────
	try {
		crashLock = readCrashLock(basePath);
		if (crashLock) {
			crashLockAlive = isLockProcessAlive(crashLock);
			if (!crashLockAlive) {
				issues.push({
					severity: "error",
					code: "stale_crash_lock",
					scope: "project",
					unitId: "project",
					message: `Stale auto.lock from PID ${crashLock.pid} (started ${crashLock.startedAt}, was executing ${crashLock.unitType} ${crashLock.unitId}) — process is no longer running`,
					file: ".sf/auto.lock",
					fixable: true,
				});
				if (shouldFix("stale_crash_lock")) {
					clearLock(basePath);
					fixesApplied.push("cleared stale auto.lock");
				}
			}
		}
	} catch {
		// Non-fatal — crash lock check failed
	}
	// ── Stale active unit runtime records ─────────────────────────────────
	// auto.lock is the ownership proof for active unit records. If it is absent
	// or points at a dead PID, dispatched/runtime-active unit files are stale
	// leftovers and make query/dispatch believe old units are still claimed.
	try {
		if (!crashLockAlive) {
			const records = listUnitRuntimeRecords(basePath);
			const staleFiles = [];
			for (const record of records) {
				if (isActiveUnitRuntimeRecord(record)) {
					staleFiles.push(record);
				}
			}
			if (staleFiles.length > 0) {
				issues.push({
					severity: "error",
					code: "stale_active_unit_runtime",
					scope: "project",
					unitId: "project",
					message: `${staleFiles.length} active unit runtime record(s) exist without a live auto.lock owner. These stale claims can block autonomous dispatch.`,
					file: ".sf/runtime/units",
					fixable: true,
				});
				if (shouldFix("stale_active_unit_runtime")) {
					for (const stale of staleFiles) {
						const sanitizedUnitType = String(stale.unitType).replace(
							/[/]/g,
							"-",
						);
						const sanitizedUnitId = String(stale.unitId).replace(/[/]/g, "-");
						const abs = join(
							root,
							"runtime",
							"units",
							`${sanitizedUnitType}-${sanitizedUnitId}.json`,
						);
						rmSync(abs, { force: true });
					}
					fixesApplied.push(
						`cleared ${staleFiles.length} stale active unit runtime record(s)`,
					);
				}
			}
		}
	} catch {
		// Non-fatal — runtime unit cleanup should not block doctor.
	}
	// ── UOK self-diagnostics ─────────────────────────────────────────────
	try {
		const diagnostics = writeUokDiagnostics(basePath);
		if (diagnostics.verdict !== "clear") {
			const firstIssue = diagnostics.issues?.[0];
			issues.push({
				severity: diagnostics.verdict === "degraded" ? "error" : "warning",
				code: "uok_diagnostics_degraded",
				scope: "project",
				unitId: diagnostics.currentUnit?.unitId ?? "project",
				message: `UOK diagnostics report ${diagnostics.verdict}/${diagnostics.classification}${firstIssue ? ` (${firstIssue.code})` : ""}. Evidence: ${diagnostics.reportPath}`,
				file: ".sf/runtime/uok-diagnostics.json",
				fixable: false,
			});
		}
	} catch {
		// Non-fatal — doctor should still report direct runtime checks.
	}
	// ── Stranded lock directory ────────────────────────────────────────────
	// proper-lockfile creates a `.sf.lock/` directory as the OS-level lock
	// mechanism. If the process was SIGKILLed or crashed hard, this directory
	// can remain on disk without any live process holding it. The next session
	// fails to acquire the lock until the directory is removed (#1245).
	try {
		const lockDir = join(dirname(root), `${basename(root)}.lock`);
		if (existsSync(lockDir)) {
			const statRes = statSync(lockDir);
			if (statRes.isDirectory()) {
				// Check if any live process actually holds this lock
				const lock = readCrashLock(basePath);
				const lockHolderAlive = lock ? isLockProcessAlive(lock) : false;
				if (!lockHolderAlive) {
					issues.push({
						severity: "error",
						code: "stranded_lock_directory",
						scope: "project",
						unitId: "project",
						message: `Stranded lock directory "${lockDir}" exists but no live process holds the session lock. This blocks new autonomous mode sessions from starting.`,
						file: lockDir,
						fixable: true,
					});
					if (shouldFix("stranded_lock_directory")) {
						try {
							rmSync(lockDir, { recursive: true, force: true });
							fixesApplied.push(`removed stranded lock directory ${lockDir}`);
						} catch {
							fixesApplied.push(
								`failed to remove stranded lock directory ${lockDir}`,
							);
						}
					}
				}
			}
		}
	} catch {
		// Non-fatal — stranded lock directory check failed
	}
	// ── Stale parallel sessions ────────────────────────────────────────────
	try {
		const parallelStatuses = readAllSessionStatuses(basePath);
		for (const status of parallelStatuses) {
			if (isSessionStale(status)) {
				issues.push({
					severity: "warning",
					code: "stale_parallel_session",
					scope: "project",
					unitId: status.milestoneId,
					message: `Stale parallel session for ${status.milestoneId} (PID ${status.pid}, started ${new Date(status.startedAt).toISOString()}, last heartbeat ${new Date(status.lastHeartbeat).toISOString()}) — process is no longer running`,
					file: `.sf/parallel/${status.milestoneId}.status.json`,
					fixable: true,
				});
				if (shouldFix("stale_parallel_session")) {
					removeSessionStatus(basePath, status.milestoneId);
					fixesApplied.push(
						`cleaned up stale parallel session for ${status.milestoneId}`,
					);
				}
			}
		}
	} catch {
		// Non-fatal — parallel session check failed
	}
	// ── Orphaned completed-units keys ─────────────────────────────────────
	try {
		const completedKeysFile = join(root, "completed-units.json");
		if (existsSync(completedKeysFile)) {
			const raw = readFileSync(completedKeysFile, "utf-8");
			const keys = JSON.parse(raw);
			const orphaned = [];
			for (const key of keys) {
				// Key format: "unitType/unitId" e.g. "execute-task/M001/S01/T01"
				// Hook units have compound types: "hook/<hookName>/unitId"
				const { splitCompletedKey } = await import("./forensics.js");
				const parsed = splitCompletedKey(key);
				if (!parsed) continue;
				const { unitType, unitId } = parsed;
				// Only validate artifact-producing unit types
				const { verifyExpectedArtifact } = await import("./auto-recovery.js");
				if (!verifyExpectedArtifact(unitType, unitId, basePath)) {
					orphaned.push(key);
				}
			}
			if (orphaned.length > 0) {
				issues.push({
					severity: "warning",
					code: "orphaned_completed_units",
					scope: "project",
					unitId: "project",
					message: `${orphaned.length} completed-unit key(s) reference missing artifacts: ${orphaned.slice(0, 3).join(", ")}${orphaned.length > 3 ? "..." : ""}`,
					file: ".sf/completed-units.json",
					fixable: true,
				});
				if (shouldFix("orphaned_completed_units")) {
					const orphanedSet = new Set(orphaned);
					const remaining = keys.filter((key) => !orphanedSet.has(key));
					await saveFile(completedKeysFile, JSON.stringify(remaining));
					fixesApplied.push(
						`removed ${orphaned.length} orphaned completed-unit key(s)`,
					);
				}
			}
		}
	} catch {
		// Non-fatal — completed-units check failed
	}
	// ── Stale hook state ──────────────────────────────────────────────────
	try {
		const hookStateFile = join(root, "hook-state.json");
		if (existsSync(hookStateFile)) {
			const raw = readFileSync(hookStateFile, "utf-8");
			const state = JSON.parse(raw);
			const hasCycleCounts =
				state.cycleCounts &&
				typeof state.cycleCounts === "object" &&
				Object.keys(state.cycleCounts).length > 0;
			// Only flag if there are actual cycle counts AND no autonomous mode is running
			if (hasCycleCounts) {
				const lock = readCrashLock(basePath);
				const autoRunning = lock ? isLockProcessAlive(lock) : false;
				if (!autoRunning) {
					issues.push({
						severity: "info",
						code: "stale_hook_state",
						scope: "project",
						unitId: "project",
						message: `hook-state.json has ${Object.keys(state.cycleCounts).length} residual cycle count(s) from a previous session`,
						file: ".sf/hook-state.json",
						fixable: true,
					});
					if (shouldFix("stale_hook_state")) {
						const { clearPersistedHookState } = await import(
							"./post-unit-hooks.js"
						);
						clearPersistedHookState(basePath);
						fixesApplied.push("cleared stale hook-state.json");
					}
				}
			}
		}
	} catch {
		// Non-fatal — hook state check failed
	}
	// ── Activity log bloat ────────────────────────────────────────────────
	try {
		const activityDir = join(root, "activity");
		if (existsSync(activityDir)) {
			const files = readdirSync(activityDir);
			let totalSize = 0;
			for (const f of files) {
				try {
					totalSize += statSync(join(activityDir, f)).size;
				} catch {
					// stat failed — skip
				}
			}
			const totalMB = totalSize / (1024 * 1024);
			const BLOAT_FILE_THRESHOLD = 500;
			const BLOAT_SIZE_MB = 100;
			if (files.length > BLOAT_FILE_THRESHOLD || totalMB > BLOAT_SIZE_MB) {
				issues.push({
					severity: "warning",
					code: "activity_log_bloat",
					scope: "project",
					unitId: "project",
					message: `Activity logs: ${files.length} files, ${totalMB.toFixed(1)}MB (thresholds: ${BLOAT_FILE_THRESHOLD} files / ${BLOAT_SIZE_MB}MB)`,
					file: ".sf/activity/",
					fixable: true,
				});
				if (shouldFix("activity_log_bloat")) {
					const { pruneActivityLogs } = await import("./activity-log.js");
					pruneActivityLogs(activityDir, 7); // 7-day retention
					fixesApplied.push("pruned activity logs (7-day retention)");
				}
			}
		}
	} catch {
		// Non-fatal — activity log check failed
	}
	// ── STATE.md health ───────────────────────────────────────────────────
	// STATE.md is a display-only projection rendered from the DB. No program
	// logic reads it — staleness does not affect correctness, only human
	// readability. The engine rewrites it after every DB-mutating operation.
	// We only fix the missing-file case so the /sf status panel has something
	// to show on first init.
	try {
		const stateFilePath = resolveSfRootFile(basePath, "STATE");
		const milestonesPath = milestonesDir(basePath);
		if (existsSync(milestonesPath)) {
			if (!existsSync(stateFilePath)) {
				issues.push({
					severity: "warning",
					code: "state_file_missing",
					scope: "project",
					unitId: "project",
					message: "STATE.md is missing — state display will not work",
					file: ".sf/STATE.md",
					fixable: true,
				});
				if (shouldFix("state_file_missing")) {
					const state = await deriveState(basePath);
					await saveFile(stateFilePath, buildStateMarkdownForCheck(state));
					fixesApplied.push("created STATE.md from derived state");
				}
			}
		}
	} catch {
		// Non-fatal — STATE.md check failed
	}
	// ── Gitignore drift ───────────────────────────────────────────────────
	try {
		const gitignorePath = join(basePath, ".gitignore");
		const excludePath = join(basePath, ".git", "info", "exclude");
		if (existsSync(gitignorePath) && nativeIsRepo(basePath)) {
			const content = [
				readFileSync(gitignorePath, "utf-8"),
				existsSync(excludePath) ? readFileSync(excludePath, "utf-8") : "",
			].join("\n");
			const existingLines = new Set(
				content
					.split("\n")
					.map((l) => l.trim())
					.filter((l) => l && !l.startsWith("#")),
			);
			// Check for critical runtime patterns that must be present.
			// Use the canonical SF_RUNTIME_PATTERNS list for consistency.
			const criticalPatterns = Array.from(SF_RUNTIME_PATTERNS);
			// If blanket .sf/ or .sf is present, all patterns are covered
			const hasBlanketIgnore =
				existingLines.has(".sf/") || existingLines.has(".sf");
			if (!hasBlanketIgnore) {
				const missing = criticalPatterns.filter((p) => !existingLines.has(p));
				if (missing.length > 0) {
					issues.push({
						severity: "warning",
						code: "gitignore_missing_patterns",
						scope: "project",
						unitId: "project",
						message: `${missing.length} critical SF runtime pattern(s) missing from .gitignore or .git/info/exclude: ${missing.join(", ")}`,
						file: ".git/info/exclude",
						fixable: true,
					});
					if (shouldFix("gitignore_missing_patterns")) {
						ensureGitignore(basePath);
						fixesApplied.push(
							"added missing SF runtime patterns to .gitignore",
						);
					}
				}
			}
		}
	} catch {
		// Non-fatal — gitignore check failed
	}
	// ── Scaffold freshness (ADR-021) ──────────────────────────────────────
	// Surfaces drift between this project's scaffold artifacts and the
	// templates SF currently ships. Non-fatal — automatic sync runs in
	// ensureAgenticDocsScaffold; this check is the user-visible signal.
	try {
		const report = detectScaffoldDrift(basePath);
		const c = report.countsByBucket;
		// Only emit a finding when something is actionable. `current` and
		// `untracked-with-no-archive-match` are non-actionable from SF's POV.
		const actionable = c.missing + c.upgradable + c["editing-drift"];
		if (actionable > 0) {
			const { parts, pendingCount } = formatBucketCountParts(c);
			issues.push({
				severity: "warning",
				code: "scaffold_drift",
				scope: "project",
				unitId: "project",
				message: `Scaffold drift: ${parts.join(", ")}. Auto-sync handles missing+pending; editing-drift needs review.`,
				file: ".sf/scaffold-manifest.json",
				fixable: pendingCount > 0,
			});
			if (shouldFix("scaffold_drift") && c.missing + c.upgradable > 0) {
				const { ensureAgenticDocsScaffold } = await import(
					"./agentic-docs-scaffold.js"
				);
				ensureAgenticDocsScaffold(basePath);
				fixesApplied.push(
					`scaffold sync: created ${c.missing} missing, refreshed ${c.upgradable} pending`,
				);
			}
		}
	} catch {
		// Non-fatal — scaffold drift check failed
	}
	// ── External state symlink health ──────────────────────────────────────
	try {
		const localSf = join(basePath, ".sf");
		if (existsSync(localSf)) {
			const stat = lstatSync(localSf);
			// Check for .sf.migrating (failed migration)
			const migratingPath = join(basePath, ".sf.migrating");
			if (existsSync(migratingPath)) {
				issues.push({
					severity: "error",
					code: "failed_migration",
					scope: "project",
					unitId: "project",
					message:
						"Found .sf.migrating — a previous external state migration failed. State may be incomplete.",
					file: ".sf.migrating",
					fixable: true,
				});
				if (shouldFix("failed_migration")) {
					if (recoverFailedMigration(basePath)) {
						fixesApplied.push(
							"recovered failed migration (.sf.migrating → .sf)",
						);
					}
				}
			}
			// Check symlink target exists
			if (stat.isSymbolicLink()) {
				try {
					realpathSync(localSf);
				} catch {
					issues.push({
						severity: "error",
						code: "broken_symlink",
						scope: "project",
						unitId: "project",
						message:
							".sf symlink target does not exist. External state directory may have been deleted.",
						file: ".sf",
						fixable: false,
					});
				}
				// ── Symlinked .sf without .gitignore entry (#4423) ──
				// When `.sf` is a symlink AND not gitignored, `git add -A -- :!.sf/...`
				// pathspecs fail with "beyond a symbolic link". Without self-heal this
				// silently drops new user files during auto-commit.
				if (nativeIsRepo(basePath) && !isSfGitignored(basePath)) {
					issues.push({
						severity: "warning",
						code: "symlinked_sf_unignored",
						scope: "project",
						unitId: "project",
						message:
							".sf is a symlink to external state but is not listed in .gitignore. This causes git pathspec exclusions to fail and can lead to silently dropped new files during auto-commit. Add `.sf` to .gitignore.",
						file: ".gitignore",
						fixable: true,
					});
					if (shouldFix("symlinked_sf_unignored")) {
						const modified = ensureGitignore(basePath);
						if (modified)
							fixesApplied.push(
								"added .sf to .gitignore (symlinked external state)",
							);
					}
				}
			}
		}
	} catch {
		// Non-fatal — external state check failed
	}
	// ── Numbered .sf collision variants (#2205) ───────────────────────────
	// macOS APFS can create ".sf 2", ".sf 3" etc. when a directory blocks
	// symlink creation. These must be removed so the canonical .sf is used.
	try {
		const variantPattern = /^\.sf \d+$/;
		const entries = readdirSync(basePath);
		const variants = entries.filter((e) => variantPattern.test(e));
		if (variants.length > 0) {
			for (const v of variants) {
				issues.push({
					severity: "warning",
					code: "numbered_sf_variant",
					scope: "project",
					unitId: "project",
					message: `Found macOS collision variant "${v}" — this can cause SF state to appear deleted.`,
					file: v,
					fixable: true,
				});
			}
			if (shouldFix("numbered_sf_variant")) {
				const removed = cleanNumberedSfVariants(basePath);
				for (const name of removed) {
					fixesApplied.push(`removed numbered .sf variant: ${name}`);
				}
			}
		}
	} catch {
		// Non-fatal — variant check failed
	}
	// ── Metrics ledger integrity ───────────────────────────────────────────
	try {
		const metricsPath = join(root, "metrics.json");
		if (existsSync(metricsPath)) {
			try {
				const raw = readFileSync(metricsPath, "utf-8");
				const ledger = JSON.parse(raw);
				if (ledger.version !== 1 || !Array.isArray(ledger.units)) {
					issues.push({
						severity: "warning",
						code: "metrics_ledger_corrupt",
						scope: "project",
						unitId: "project",
						message:
							"metrics.json has an unexpected structure (version !== 1 or units is not an array) — metrics data may be unreliable",
						file: ".sf/metrics.json",
						fixable: false,
					});
				}
			} catch {
				issues.push({
					severity: "warning",
					code: "metrics_ledger_corrupt",
					scope: "project",
					unitId: "project",
					message:
						"metrics.json is not valid JSON — metrics data may be corrupt",
					file: ".sf/metrics.json",
					fixable: false,
				});
			}
		}
	} catch {
		// Non-fatal — metrics check failed
	}
	// ── Metrics ledger bloat ──────────────────────────────────────────────
	// The metrics ledger has no TTL and grows by one entry per completed unit.
	// At 50 units/day a project can accumulate tens of thousands of entries over
	// months of use. Prune to the newest 1500 when the threshold is exceeded.
	try {
		const metricsFilePath = join(root, "metrics.json");
		if (existsSync(metricsFilePath)) {
			try {
				const raw = readFileSync(metricsFilePath, "utf-8");
				const parsed = JSON.parse(raw);
				const BLOAT_UNITS_THRESHOLD = 2000;
				if (
					parsed.version === 1 &&
					Array.isArray(parsed.units) &&
					parsed.units.length > BLOAT_UNITS_THRESHOLD
				) {
					const fileSizeMB = (
						statSync(metricsFilePath).size /
						(1024 * 1024)
					).toFixed(1);
					issues.push({
						severity: "warning",
						code: "metrics_ledger_bloat",
						scope: "project",
						unitId: "project",
						message: `metrics.json has ${parsed.units.length} unit entries (${fileSizeMB}MB) — threshold is ${BLOAT_UNITS_THRESHOLD}. Run /doctor --fix to prune to the newest 1500 entries.`,
						file: ".sf/metrics.json",
						fixable: true,
					});
					if (shouldFix("metrics_ledger_bloat")) {
						const { pruneMetricsLedger } = await import("./metrics.js");
						const removed = pruneMetricsLedger(basePath, 1500);
						fixesApplied.push(
							`pruned metrics ledger: removed ${removed} oldest entries (${parsed.units.length - removed} remain)`,
						);
					}
				}
			} catch {
				// JSON parse failed — already handled by the integrity check above
			}
		}
	} catch {
		// Non-fatal — metrics bloat check failed
	}
	// ── Large planning file detection ──────────────────────────────────────
	// Files over 100KB can cause LLM context pressure. Report the worst offenders.
	try {
		const MAX_FILE_BYTES = 100 * 1024; // 100KB
		const milestonesPath = milestonesDir(basePath);
		if (existsSync(milestonesPath)) {
			const largeFiles = [];
			function scanForLargeFiles(dir, depth = 0) {
				if (depth > 6) return;
				try {
					for (const entry of readdirSync(dir)) {
						const full = join(dir, entry);
						try {
							const s = statSync(full);
							if (s.isDirectory()) {
								scanForLargeFiles(full, depth + 1);
								continue;
							}
							if (entry.endsWith(".md") && s.size > MAX_FILE_BYTES) {
								largeFiles.push({
									path: full.replace(basePath + "/", ""),
									sizeKB: Math.round(s.size / 1024),
								});
							}
						} catch {
							/* skip entry */
						}
					}
				} catch {
					/* skip dir */
				}
			}
			scanForLargeFiles(milestonesPath);
			if (largeFiles.length > 0) {
				largeFiles.sort((a, b) => b.sizeKB - a.sizeKB);
				const worst = largeFiles[0];
				issues.push({
					severity: "warning",
					code: "large_planning_file",
					scope: "project",
					unitId: "project",
					message: `${largeFiles.length} planning file(s) exceed 100KB — largest: ${worst.path} (${worst.sizeKB}KB). Large files cause LLM context pressure.`,
					file: worst.path,
					fixable: false,
				});
			}
		}
	} catch {
		// Non-fatal — large file scan failed
	}
	// ── Snapshot ref bloat ────────────────────────────────────────────────
	// refs/next/snapshots/ accumulate over time. Prune to newest 5 per label
	// when total count exceeds threshold.
	try {
		if (nativeIsRepo(basePath)) {
			const refs = nativeForEachRef(basePath, "refs/next/snapshots/");
			if (refs.length > 50) {
				issues.push({
					severity: "warning",
					code: "snapshot_ref_bloat",
					scope: "project",
					unitId: "project",
					message: `${refs.length} snapshot refs found under refs/next/snapshots/ — pruning to newest 5 per label will reclaim git storage`,
					fixable: true,
				});
				if (shouldFix("snapshot_ref_bloat")) {
					const byLabel = new Map();
					for (const ref of refs) {
						const parts = ref.split("/");
						const label = parts.slice(0, -1).join("/");
						if (!byLabel.has(label)) byLabel.set(label, []);
						byLabel.get(label).push(ref);
					}
					let pruned = 0;
					for (const [, labelRefs] of byLabel) {
						const sorted = labelRefs.sort();
						for (const old of sorted.slice(0, -5)) {
							try {
								nativeUpdateRef(basePath, old);
								pruned++;
							} catch {
								/* skip */
							}
						}
					}
					if (pruned > 0) {
						fixesApplied.push(`pruned ${pruned} old snapshot ref(s)`);
					}
				}
			}
		}
	} catch {
		// Non-fatal — snapshot ref check failed
	}
	// ── Unified audit projection health ───────────────────────────────────
	// If emitUokAuditEvent has failed since the last reset, the unified audit
	// log has diverged from the workflow-logger buffer. Surface the count so
	// operators can investigate without relying on transient stderr lines.
	try {
		const auditFailures = getAuditEmitFailureCount();
		if (auditFailures > 0) {
			issues.push({
				severity: "warning",
				code: "audit_emit_failure",
				scope: "project",
				unitId: "project",
				message: `Unified audit projection diverged: ${auditFailures} emitUokAuditEvent failure(s) since last logger reset. Check stderr logs for details; the audit log may be incomplete for the current session.`,
				fixable: false,
			});
		}
	} catch {
		// Non-fatal — audit emit failure check failed
	}
	// ── Scaffold freshness (ADR-021 Phase C) ──────────────────────────────
	// Visibility into scaffold drift. Phase C runs the silent path
	// automatically on every SF startup, but the doctor finding lets users
	// see what was upgraded and what is still pending review (editing-drift,
	// untracked-without-archive-match). Severity: warning. Never blocks.
	try {
		const finding = checkScaffoldFreshness(basePath);
		if (finding) issues.push(finding);
	} catch {
		// Non-fatal — scaffold freshness check failed
	}
}
/**
/**
 * Format bucket counts into a readable parts array for scaffold drift messages.
 * Shared logic between checkRuntimeHealth and checkScaffoldFreshness.
 */
function formatBucketCountParts(counts) {
	const parts = [];
	if (counts.missing && counts.missing > 0)
		parts.push(`${counts.missing} missing`);
	if (counts.upgradable && counts.upgradable > 0)
		parts.push(`${counts.upgradable} pending upgrade`);
	if (counts["editing-drift"] && counts["editing-drift"] > 0)
		parts.push(`${counts["editing-drift"]} editing-drift`);
	const pendingCount = (counts.missing ?? 0) + (counts.upgradable ?? 0);
	return { parts, pendingCount };
}
/**
 * ADR-021 Phase C: report scaffold drift bucket counts as a doctor finding.
 *
 * Returns `null` when there is nothing actionable (everything is current or
 * intentionally customised). Otherwise returns a single warning summarising the
 * bucket counts. The phrase "Run /scaffold sync" is forward-looking —
 * Phase E adds the command. Phase C runs the silent path automatically on
 * every SF startup, so the user does not need to act on most of these.
 */
export function checkScaffoldFreshness(basePath) {
	let report;
	try {
		report = detectScaffoldDrift(basePath);
	} catch {
		return null;
	}
	const counts = report.countsByBucket;
	const actionable =
		counts.missing + counts.upgradable + counts["editing-drift"];
	if (actionable === 0) return null;
	const { parts, pendingCount } = formatBucketCountParts(counts);
	const summary = parts.join(", ");
	const guidance =
		pendingCount > 0
			? `Run /scaffold sync to refresh ${pendingCount} pending docs`
			: "Run /scaffold sync to inspect drift";
	return {
		severity: "warning",
		code: "scaffold_drift",
		scope: "project",
		unitId: "project",
		message: `Scaffold drift: ${summary}. ${guidance}.`,
		fixable: false,
	};
}
/**
 * Build STATE.md markdown content from derived state.
 * Local helper used by checkRuntimeHealth for STATE.md drift detection and repair.
 */
function buildStateMarkdownForCheck(state) {
	const lines = [];
	lines.push("# SF State", "");
	const activeMilestone = state.activeMilestone
		? `${state.activeMilestone.id}: ${state.activeMilestone.title}`
		: "None";
	const activeSlice = state.activeSlice
		? `${state.activeSlice.id}: ${state.activeSlice.title}`
		: "None";
	lines.push(`**Active Milestone:** ${activeMilestone}`);
	lines.push(`**Active Slice:** ${activeSlice}`);
	lines.push(`**Phase:** ${state.phase}`);
	if (state.requirements) {
		lines.push(
			`**Requirements Status:** ${state.requirements.active} active · ${state.requirements.validated} validated · ${state.requirements.deferred} deferred · ${state.requirements.outOfScope} out of scope`,
		);
	}
	lines.push("");
	lines.push("## Milestone Registry");
	for (const entry of state.registry) {
		const glyph =
			entry.status === "complete"
				? "\u2705"
				: entry.status === "active"
					? "\uD83D\uDD04"
					: entry.status === "parked"
						? "\u23F8\uFE0F"
						: "\u2B1C";
		lines.push(`- ${glyph} **${entry.id}:** ${entry.title}`);
	}
	lines.push("");
	lines.push("## Recent Decisions");
	if (state.recentDecisions.length > 0) {
		for (const decision of state.recentDecisions) lines.push(`- ${decision}`);
	} else {
		lines.push("- None recorded");
	}
	lines.push("");
	lines.push("## Blockers");
	if (state.blockers.length > 0) {
		for (const blocker of state.blockers) lines.push(`- ${blocker}`);
	} else {
		lines.push("- None");
	}
	lines.push("");
	lines.push("## Next Action");
	lines.push(state.nextAction || "None");
	lines.push("");
	return lines.join("\n");
}
