/**
 * SF Doctor — Proactive Healing Layer
 *
 * Three mechanisms for automatic health monitoring during autonomous mode:
 *
 * 1. Pre-dispatch health gate: lightweight check before each unit dispatch.
 *    Returns blocking issues that should pause autonomous mode rather than
 *    dispatching into a broken state.
 *
 * 2. Health score tracking: tracks issue counts over time to detect
 *    degradation trends. If health is declining, surfaces a warning.
 *
 * 3. Auto-heal escalation: if deterministic fix can't resolve issues
 *    after N units, escalates to LLM-assisted heal dispatch.
 */
import { existsSync, rmSync } from "node:fs";
import { basename, dirname, join } from "node:path";
import {
	clearLock,
	isLockProcessAlive,
	readCrashLock,
} from "./crash-recovery.js";
import { rebuildState } from "./doctor.js";
import { runEnvironmentChecks } from "./doctor-environment.js";
import { abortAndReset } from "./git-self-heal.js";
import { resolveMilestoneIntegrationBranch } from "./git-service.js";
import {
	nativeAddTracked,
	nativeCommit,
	nativeGetCurrentBranch,
	nativeHasChanges,
	nativeIsRepo,
	nativeLastCommitEpoch,
} from "./native-git-bridge.js";
import { resolveSfRootFile, sfRoot } from "./paths.js";
import { loadEffectiveSFPreferences } from "./preferences.js";
import {
	formatProtectedSnapshotDeletionMessage,
	listProtectedSnapshotDeletions,
} from "./snapshot-safety.js";
import { deriveState } from "./state.js";

/** In-memory health history for the current autonomous mode session. */
let healthHistory = [];
/** Count of consecutive units with unresolved errors. */
let consecutiveErrorUnits = 0;
/** Unit index counter for health tracking. */
let healthUnitIndex = 0;
/** Previous progress level for state transition detection. */
let previousProgressLevel = "green";
/** Callback for state transition notifications. Set by autonomous mode. */
let onLevelChange = null;
/**
 * Register a callback for progress level transitions (green→yellow, yellow→red, etc.).
 * Called once when autonomous mode starts. Pass null to unregister.
 */
export function setLevelChangeCallback(cb) {
	onLevelChange = cb;
	previousProgressLevel = "green";
}
/**
 * Record a health snapshot after a doctor run.
 * Called from the post-unit hook in auto-post-unit.ts.
 */
export function recordHealthSnapshot(
	errors,
	warnings,
	fixesApplied,
	issues,
	fixes,
	scope,
) {
	healthUnitIndex++;
	healthHistory.push({
		timestamp: Date.now(),
		errors,
		warnings,
		fixesApplied,
		unitIndex: healthUnitIndex,
		issues: issues ?? [],
		fixes: fixes ?? [],
		scope,
	});
	// Keep only the last 50 snapshots to bound memory
	if (healthHistory.length > 50) {
		healthHistory = healthHistory.slice(-50);
	}
	if (errors > 0) {
		consecutiveErrorUnits++;
	} else {
		consecutiveErrorUnits = 0;
	}
	// Detect progress level transitions and notify
	if (onLevelChange) {
		const newLevel =
			consecutiveErrorUnits >= 3
				? "red"
				: consecutiveErrorUnits >= 1 || getHealthTrend() === "degrading"
					? "yellow"
					: "green";
		if (newLevel !== previousProgressLevel) {
			const topIssue =
				(issues ?? []).find((i) => i.severity === "error") ?? (issues ?? [])[0];
			const detail = topIssue ? `: ${topIssue.message}` : "";
			onLevelChange(
				previousProgressLevel,
				newLevel,
				`Health ${previousProgressLevel} → ${newLevel}${detail}`,
			);
			previousProgressLevel = newLevel;
		}
	}
}
/**
 * Get the current health trend.
 * Returns "improving", "stable", "degrading", or "unknown" (not enough data).
 */
export function getHealthTrend() {
	if (healthHistory.length < 3) return "unknown";
	const recent = healthHistory.slice(-5);
	const older = healthHistory.slice(-10, -5);
	if (older.length === 0) return "unknown";
	const recentAvg =
		recent.reduce((sum, s) => sum + s.errors + s.warnings, 0) / recent.length;
	const olderAvg =
		older.reduce((sum, s) => sum + s.errors + s.warnings, 0) / older.length;
	const delta = recentAvg - olderAvg;
	if (delta > 1) return "degrading";
	if (delta < -1) return "improving";
	return "stable";
}
/**
 * Get the number of consecutive units with unresolved errors.
 */
export function getConsecutiveErrorUnits() {
	return consecutiveErrorUnits;
}
/**
 * Get health history for display (e.g., dashboard overlay).
 */
export function getHealthHistory() {
	return healthHistory;
}
/**
 * Get the latest health issues from the most recent snapshot.
 * Returns issues from the last snapshot that had any, for real-time visibility.
 */
export function getLatestHealthIssues() {
	for (let i = healthHistory.length - 1; i >= 0; i--) {
		if (healthHistory[i].issues.length > 0) return healthHistory[i].issues;
	}
	return [];
}
/**
 * Get the latest fixes applied from the most recent snapshot.
 */
export function getLatestHealthFixes() {
	for (let i = healthHistory.length - 1; i >= 0; i--) {
		if (healthHistory[i].fixes.length > 0) return healthHistory[i].fixes;
	}
	return [];
}
/**
 * Reset health tracking state. Called on autonomous mode start/stop.
 */
export function resetHealthTracking() {
	healthHistory = [];
	consecutiveErrorUnits = 0;
	healthUnitIndex = 0;
	previousProgressLevel = "green";
}
/**
 * Clear stale auto runtime locks before startup decides whether to resume.
 *
 * Purpose: make background/proactive healing effective for the first auto
 * decision, not only after a unit is already about to dispatch.
 *
 * Consumer: startAuto before assessInterruptedSession reads auto.lock and
 * paused-session state.
 */
export function healAutoStartupRuntime(basePath) {
	const fixesApplied = [];
	try {
		const lock = readCrashLock(basePath);
		if (lock && !isLockProcessAlive(lock)) {
			clearLock(basePath);
			fixesApplied.push("cleared stale auto.lock before auto startup");
		}
	} catch {
		// Non-fatal.
	}
	try {
		const root = sfRoot(basePath);
		const lockDir = join(dirname(root), `${basename(root)}.lock`);
		if (existsSync(lockDir)) {
			const lock = readCrashLock(basePath);
			const lockHolderAlive = lock ? isLockProcessAlive(lock) : false;
			if (!lockHolderAlive) {
				rmSync(lockDir, { recursive: true, force: true });
				fixesApplied.push("removed stranded session lock directory");
			}
		}
	} catch {
		// Non-fatal.
	}
	return fixesApplied;
}
/**
 * Lightweight pre-dispatch health check. Runs fast checks that should
 * block dispatch if they fail — avoids dispatching into a broken state.
 *
 * This is NOT a full doctor run — it only checks critical, fast-to-evaluate
 * conditions that would cause the next unit to fail or corrupt state.
 *
 * Returns { proceed: true } if dispatch should continue.
 */
export async function preDispatchHealthGate(basePath) {
	const issues = [];
	const fixesApplied = [];
	// ── Stale crash lock blocks dispatch ──
	// If a stale lock exists, the crash recovery path should handle it,
	// not a new dispatch. This prevents double-dispatch after crashes.
	try {
		const lock = readCrashLock(basePath);
		if (lock && !isLockProcessAlive(lock)) {
			// Auto-clear it since we're about to dispatch anyway
			clearLock(basePath);
			fixesApplied.push("cleared stale auto.lock before dispatch");
		}
	} catch {
		// Non-fatal
	}
	// ── Corrupt merge/rebase state blocks dispatch ──
	// Dispatching a unit with MERGE_HEAD present will cause git operations to fail.
	try {
		const gitDir = join(basePath, ".git");
		if (existsSync(gitDir)) {
			const blockers = ["MERGE_HEAD", "rebase-apply", "rebase-merge"].filter(
				(f) => existsSync(join(gitDir, f)),
			);
			if (blockers.length > 0) {
				// Try to auto-heal
				try {
					const result = abortAndReset(basePath);
					fixesApplied.push(
						`pre-dispatch: cleaned merge state (${result.cleaned.join(", ")})`,
					);
				} catch {
					issues.push(
						`Corrupt git state: ${blockers.join(", ")}. Run /doctor fix.`,
					);
				}
			}
		}
	} catch {
		// Non-fatal
	}
	// ── STATE.md existence check ──
	// If STATE.md is missing, attempt to rebuild it for the next unit's context.
	// Non-blocking — fresh worktrees won't have it until the first unit completes (#889).
	try {
		const stateFile = resolveSfRootFile(basePath, "STATE");
		const milestonesDir = join(sfRoot(basePath), "milestones");
		if (existsSync(milestonesDir) && !existsSync(stateFile)) {
			try {
				await rebuildState(basePath);
				fixesApplied.push("rebuilt missing STATE.md before dispatch");
			} catch {
				// Rebuild failed — non-blocking, dispatch continues
				fixesApplied.push(
					"STATE.md missing — will rebuild after first unit completes",
				);
			}
		}
	} catch {
		// Non-fatal — dispatch continues without STATE.md if rebuild fails
	}
	// ── Integration branch existence check ──
	// If the active milestone's recorded integration branch no longer exists in
	// git, the merge-back at the end of the milestone will fail. Block dispatch
	// now to surface this before work is lost.
	try {
		if (nativeIsRepo(basePath)) {
			const state = await deriveState(basePath);
			if (state.activeMilestone) {
				const gitPrefs = loadEffectiveSFPreferences()?.preferences?.git ?? {};
				const resolution = resolveMilestoneIntegrationBranch(
					basePath,
					state.activeMilestone.id,
					gitPrefs,
				);
				if (resolution.status === "fallback" && resolution.effectiveBranch) {
					fixesApplied.push(
						`using fallback integration branch "${resolution.effectiveBranch}" for milestone ${state.activeMilestone.id}; recorded "${resolution.recordedBranch}" no longer exists`,
					);
				} else if (
					resolution.recordedBranch &&
					resolution.status === "missing"
				) {
					issues.push(
						`${resolution.reason} Restore the branch or update the integration branch before dispatching. Run /doctor for details.`,
					);
				}
			}
		}
	} catch {
		// Non-fatal — dispatch continues if state/branch check fails
	}
	// ── Stale uncommitted changes — auto-snapshot before dispatch ──
	// If the working tree is dirty and no commit has happened recently,
	// create a safety snapshot so work isn't lost if the next unit crashes.
	try {
		if (nativeIsRepo(basePath)) {
			const prefs = loadEffectiveSFPreferences()?.preferences ?? {};
			const thresholdMinutes = prefs.stale_commit_threshold_minutes ?? 30;
			if (thresholdMinutes > 0 && nativeHasChanges(basePath)) {
				const branch = nativeGetCurrentBranch(basePath);
				const lastEpoch = nativeLastCommitEpoch(basePath, branch || "HEAD");
				const nowEpoch = Math.floor(Date.now() / 1000);
				const minutesSinceCommit =
					lastEpoch > 0 ? (nowEpoch - lastEpoch) / 60 : Infinity;
				if (minutesSinceCommit >= thresholdMinutes) {
					const mins = Math.floor(minutesSinceCommit);
					const protectedDeletions = listProtectedSnapshotDeletions(basePath);
					if (protectedDeletions.length > 0) {
						issues.push(
							formatProtectedSnapshotDeletionMessage(protectedDeletions),
						);
					} else {
						try {
							nativeAddTracked(basePath);
							const commitMsg = `sf snapshot: pre-dispatch, uncommitted changes after ${mins}m inactivity`;
							const result = nativeCommit(basePath, commitMsg);
							if (result) {
								fixesApplied.push(
									`pre-dispatch: created sf snapshot after ${mins}m of uncommitted changes`,
								);
							}
						} catch {
							// Non-blocking — snapshot failed but dispatch can continue
							fixesApplied.push("pre-dispatch: sf snapshot failed");
						}
					}
				}
			}
		}
	} catch {
		// Non-fatal
	}
	// ── Disk space check ──
	// Catches low-disk conditions before dispatch rather than letting the unit
	// fail mid-execution with ENOSPC (which wastes a full LLM turn).
	try {
		const envResults = runEnvironmentChecks(basePath);
		const diskError = envResults.find(
			(r) => r.name === "disk_space" && r.status === "error",
		);
		if (diskError) {
			issues.push(
				`${diskError.message}${diskError.detail ? ` — ${diskError.detail}` : ""}`,
			);
		}
	} catch {
		// Non-fatal — dispatch continues if env check fails
	}
	// If we had critical issues that couldn't be auto-healed, block dispatch
	if (issues.length > 0) {
		return {
			proceed: false,
			reason: `Pre-dispatch health check failed:\n${issues.map((i) => `  - ${i}`).join("\n")}\nRun /doctor fix to resolve.`,
			issues,
			fixesApplied,
		};
	}
	return { proceed: true, issues, fixesApplied };
}
// ── Auto-Heal Escalation ──────────────────────────────────────────────────
/** Threshold: escalate to LLM heal after this many consecutive error units. */
const ESCALATION_THRESHOLD = 5;
/** Whether an escalation has already been triggered this session (prevent spam). */
let escalationTriggered = false;
/**
 * Check whether auto-heal should escalate from deterministic fix to
 * LLM-assisted heal. Called after each post-unit doctor run.
 *
 * Returns the structured issue text for LLM dispatch, or null if
 * escalation is not needed.
 */
export function checkHealEscalation(errors, unresolvedIssues) {
	if (escalationTriggered) {
		return {
			shouldEscalate: false,
			reason: "already escalated this session",
			issues: [],
		};
	}
	if (consecutiveErrorUnits < ESCALATION_THRESHOLD) {
		return {
			shouldEscalate: false,
			reason: `${consecutiveErrorUnits}/${ESCALATION_THRESHOLD} consecutive error units`,
			issues: [],
		};
	}
	if (errors === 0) {
		return {
			shouldEscalate: false,
			reason: "no errors to escalate",
			issues: [],
		};
	}
	const trend = getHealthTrend();
	if (trend === "improving") {
		return {
			shouldEscalate: false,
			reason: "health is improving — deferring escalation",
			issues: [],
		};
	}
	escalationTriggered = true;
	return {
		shouldEscalate: true,
		reason: `${consecutiveErrorUnits} consecutive units with unresolved errors (trend: ${trend})`,
		issues: unresolvedIssues,
	};
}
/**
 * Reset escalation state. Called on autonomous mode start/stop.
 */
export function resetEscalation() {
	escalationTriggered = false;
}
/**
 * Format a health summary for display in the autonomous mode dashboard.
 * Human-readable with full words, not abbreviations.
 */
export function formatHealthSummary() {
	if (healthHistory.length === 0) return "No health data yet.";
	const latest = healthHistory[healthHistory.length - 1];
	const trend = getHealthTrend();
	const trendLabel =
		trend === "improving"
			? "improving"
			: trend === "degrading"
				? "degrading"
				: trend === "stable"
					? "stable"
					: "unknown";
	const totalFixes = healthHistory.reduce((sum, s) => sum + s.fixesApplied, 0);
	const parts = [];
	// Error/warning summary
	if (latest.errors === 0 && latest.warnings === 0) {
		parts.push("No issues");
	} else {
		const counts = [];
		if (latest.errors > 0)
			counts.push(`${latest.errors} error${latest.errors > 1 ? "s" : ""}`);
		if (latest.warnings > 0)
			counts.push(
				`${latest.warnings} warning${latest.warnings > 1 ? "s" : ""}`,
			);
		parts.push(counts.join(", "));
	}
	parts.push(`trend ${trendLabel}`);
	if (totalFixes > 0) {
		parts.push(`${totalFixes} fix${totalFixes > 1 ? "es" : ""} applied`);
	}
	if (consecutiveErrorUnits > 0) {
		parts.push(
			`${consecutiveErrorUnits} of ${ESCALATION_THRESHOLD} consecutive errors before escalation`,
		);
	}
	// Include top issue from latest snapshot
	if (latest.issues.length > 0) {
		const topIssue =
			latest.issues.find((i) => i.severity === "error") ?? latest.issues[0];
		parts.push(`latest: ${topIssue.message}`);
	}
	return parts.join(" · ");
}
/**
 * Reset all proactive healing state. Called on autonomous mode start/stop.
 */
export function resetProactiveHealing() {
	resetHealthTracking();
	resetEscalation();
}
