import {
	existsSync,
	readdirSync,
	realpathSync,
	rmSync,
	statSync,
} from "node:fs";
import { join, sep } from "node:path";
import { loadFile } from "./files.js";
import { abortAndReset } from "./git-self-heal.js";
import {
	RUNTIME_EXCLUSION_PATHS,
	resolveMilestoneIntegrationBranch,
	writeIntegrationBranch,
} from "./git-service.js";
import {
	nativeAddTracked,
	nativeBranchDelete,
	nativeBranchList,
	nativeCommit,
	nativeGetCurrentBranch,
	nativeHasChanges,
	nativeIsRepo,
	nativeLastCommitEpoch,
	nativeLsFiles,
	nativeRmCached,
	nativeWorktreeList,
	nativeWorktreeRemove,
} from "./native-git-bridge.js";
import { parseRoadmap } from "./parsers.js";
import { resolveMilestoneFile } from "./paths.js";
import { loadEffectiveSFPreferences } from "./preferences.js";
import { getMilestoneSlices, isDbAvailable } from "./sf-db.js";
import {
	formatProtectedSnapshotDeletionMessage,
	listProtectedSnapshotDeletions,
} from "./snapshot-safety.js";
import { deriveState, isMilestoneComplete } from "./state.js";
import { getAllWorktreeHealth } from "./worktree-health.js";
import {
	listWorktrees,
	resolveGitDir,
	worktreesDir,
} from "./worktree-manager.js";

/**
 * Returns true if the directory contains only doctor artifacts
 * (e.g. `.sf/doctor-history.jsonl`). These dirs are created by
 * appendDoctorHistory() writing to worktree-scoped paths during the audit
 * and should not be flagged as orphaned worktrees (#3105).
 */
function isDoctorArtifactOnly(dirPath) {
	try {
		const entries = readdirSync(dirPath);
		// Empty dir — not a doctor artifact, still orphaned
		if (entries.length === 0) return false;
		// Only a .sf subdirectory
		if (entries.length === 1 && entries[0] === ".sf") {
			const sfEntries = readdirSync(join(dirPath, ".sf"));
			return (
				sfEntries.length <= 1 &&
				sfEntries.every((e) => e === "doctor-history.jsonl")
			);
		}
		return false;
	} catch {
		return false;
	}
}
export async function checkGitHealth(
	basePath,
	issues,
	fixesApplied,
	shouldFix,
	isolationMode = "none",
) {
	// Degrade gracefully if not a git repo
	if (!nativeIsRepo(basePath)) {
		return; // Not a git repo — skip all git health checks
	}
	const gitDir = resolveGitDir(basePath);
	// ── Orphaned auto-worktrees & Stale milestone branches ────────────────
	// These checks only apply in worktree/branch modes — skip in none mode
	// where no milestone worktrees or branches are created.
	if (isolationMode !== "none") {
		try {
			const worktrees = listWorktrees(basePath);
			const milestoneWorktrees = worktrees.filter((wt) =>
				wt.branch.startsWith("milestone/"),
			);
			// Load roadmap state once for cross-referencing
			const state = await deriveState(basePath);
			for (const wt of milestoneWorktrees) {
				// Extract milestone ID from branch name "milestone/M001" → "M001"
				const milestoneId = wt.branch.replace(/^milestone\//, "");
				const milestoneEntry = state.registry.find((m) => m.id === milestoneId);
				// Check if milestone is complete via roadmap
				let isComplete = false;
				if (milestoneEntry) {
					if (isDbAvailable()) {
						const dbSlices = getMilestoneSlices(milestoneId);
						isComplete =
							dbSlices.length > 0 &&
							dbSlices.every((s) => s.status === "complete");
					} else {
						const roadmapPath = resolveMilestoneFile(
							basePath,
							milestoneId,
							"ROADMAP",
						);
						const roadmapContent = roadmapPath
							? await loadFile(roadmapPath)
							: null;
						if (roadmapContent) {
							const roadmap = parseRoadmap(roadmapContent);
							isComplete = isMilestoneComplete(roadmap);
						}
					}
					// When DB unavailable and no roadmap, isComplete stays false
				}
				if (isComplete) {
					issues.push({
						severity: "warning",
						code: "orphaned_auto_worktree",
						scope: "milestone",
						unitId: milestoneId,
						message: `Worktree for completed milestone ${milestoneId} still exists at ${wt.path}`,
						fixable: true,
					});
					if (shouldFix("orphaned_auto_worktree")) {
						// If cwd is inside the worktree, chdir out first — matching the
						// pattern in removeWorktree() (#1946). Without this, git cannot
						// remove the worktree and the doctor enters a deadlock where it
						// detects the orphan every run but never cleans it up.
						const cwd = process.cwd();
						if (wt.path === cwd || cwd.startsWith(wt.path + sep)) {
							try {
								process.chdir(basePath);
							} catch {
								fixesApplied.push(
									`skipped removing worktree at ${wt.path} (cannot chdir to basePath)`,
								);
								continue;
							}
						}
						try {
							nativeWorktreeRemove(basePath, wt.path, true);
							fixesApplied.push(`removed orphaned worktree ${wt.path}`);
						} catch {
							fixesApplied.push(`failed to remove worktree ${wt.path}`);
						}
					}
				}
			}
			// ── Stale milestone branches ─────────────────────────────────────────
			try {
				const branches = nativeBranchList(basePath, "milestone/*");
				if (branches.length > 0) {
					const worktreeBranches = new Set(
						milestoneWorktrees.map((wt) => wt.branch),
					);
					for (const branch of branches) {
						// Skip branches that have a worktree (handled above)
						if (worktreeBranches.has(branch)) continue;
						const milestoneId = branch.replace(/^milestone\//, "");
						const roadmapPath = resolveMilestoneFile(
							basePath,
							milestoneId,
							"ROADMAP",
						);
						let branchMilestoneComplete = false;
						if (isDbAvailable()) {
							const dbSlices = getMilestoneSlices(milestoneId);
							branchMilestoneComplete =
								dbSlices.length > 0 &&
								dbSlices.every((s) => s.status === "complete");
						} else {
							const roadmapContent = roadmapPath
								? await loadFile(roadmapPath)
								: null;
							if (!roadmapContent) continue;
							const roadmap = parseRoadmap(roadmapContent);
							branchMilestoneComplete = isMilestoneComplete(roadmap);
						}
						if (branchMilestoneComplete) {
							issues.push({
								severity: "info",
								code: "stale_milestone_branch",
								scope: "milestone",
								unitId: milestoneId,
								message: `Branch ${branch} exists for completed milestone ${milestoneId}`,
								fixable: true,
							});
							if (shouldFix("stale_milestone_branch")) {
								try {
									nativeBranchDelete(basePath, branch, true);
									fixesApplied.push(`deleted stale branch ${branch}`);
								} catch {
									fixesApplied.push(`failed to delete branch ${branch}`);
								}
							}
						}
					}
				}
			} catch {
				// git branch list failed — skip stale branch check
			}
		} catch {
			// listWorktrees or deriveState failed — skip worktree/branch checks
		}
	} // end isolationMode !== "none"
	// ── Corrupt merge state ────────────────────────────────────────────────
	try {
		const mergeStateFiles = ["MERGE_HEAD", "SQUASH_MSG"];
		const mergeStateDirs = ["rebase-apply", "rebase-merge"];
		const found = [];
		for (const f of mergeStateFiles) {
			if (existsSync(join(gitDir, f))) found.push(f);
		}
		for (const d of mergeStateDirs) {
			if (existsSync(join(gitDir, d))) found.push(d);
		}
		if (found.length > 0) {
			issues.push({
				severity: "error",
				code: "corrupt_merge_state",
				scope: "project",
				unitId: "project",
				message: `Corrupt merge/rebase state detected: ${found.join(", ")}`,
				fixable: true,
			});
			if (shouldFix("corrupt_merge_state")) {
				const result = abortAndReset(basePath);
				fixesApplied.push(`cleaned merge state: ${result.cleaned.join(", ")}`);
			}
		}
	} catch {
		// Can't check .git dir — skip
	}
	// ── Tracked runtime files ──────────────────────────────────────────────
	try {
		const trackedPaths = [];
		for (const exclusion of RUNTIME_EXCLUSION_PATHS) {
			try {
				const files = nativeLsFiles(basePath, exclusion);
				if (files.length > 0) {
					trackedPaths.push(...files);
				}
			} catch {
				// Individual ls-files can fail — continue
			}
		}
		if (trackedPaths.length > 0) {
			issues.push({
				severity: "warning",
				code: "tracked_runtime_files",
				scope: "project",
				unitId: "project",
				message: `${trackedPaths.length} runtime file(s) are tracked by git: ${trackedPaths.slice(0, 5).join(", ")}${trackedPaths.length > 5 ? "..." : ""}`,
				fixable: true,
			});
			if (shouldFix("tracked_runtime_files")) {
				try {
					for (const exclusion of RUNTIME_EXCLUSION_PATHS) {
						nativeRmCached(basePath, [exclusion]);
					}
					fixesApplied.push(`untracked ${trackedPaths.length} runtime file(s)`);
				} catch {
					fixesApplied.push("failed to untrack runtime files");
				}
			}
		}
	} catch {
		// git ls-files failed — skip
	}
	// ── Legacy slice branches ──────────────────────────────────────────────
	try {
		const branchList = nativeBranchList(basePath, "sf/*/*").filter(
			(branch) => !branch.startsWith("sf/quick/"),
		);
		if (branchList.length > 0) {
			issues.push({
				severity: "info",
				code: "legacy_slice_branches",
				scope: "project",
				unitId: "project",
				message: `${branchList.length} legacy slice branch(es) found: ${branchList.slice(0, 3).join(", ")}${branchList.length > 3 ? "..." : ""}. These are no longer used (branchless architecture).`,
				fixable: true,
			});
			if (shouldFix("legacy_slice_branches")) {
				let deleted = 0;
				for (const branch of branchList) {
					try {
						nativeBranchDelete(basePath, branch, true);
						deleted++;
					} catch {
						/* skip branches that can't be deleted */
					}
				}
				if (deleted > 0) {
					fixesApplied.push(`deleted ${deleted} legacy slice branch(es)`);
				}
			}
		}
	} catch {
		// git branch list failed — skip
	}
	// ── Integration branch existence ──────────────────────────────────────
	// For each active (non-complete) milestone, verify the stored integration
	// branch still exists in git. A missing integration branch blocks merge-back
	// and causes the next merge operation to fail silently.
	try {
		const state = await deriveState(basePath);
		const gitPrefs = loadEffectiveSFPreferences()?.preferences?.git ?? {};
		for (const milestone of state.registry) {
			if (milestone.status === "complete") continue;
			const resolution = resolveMilestoneIntegrationBranch(
				basePath,
				milestone.id,
				gitPrefs,
			);
			if (!resolution.recordedBranch) continue; // No stored branch — skip (not yet set)
			if (resolution.status === "fallback" && resolution.effectiveBranch) {
				issues.push({
					severity: "warning",
					code: "integration_branch_missing",
					scope: "milestone",
					unitId: milestone.id,
					message: resolution.reason,
					fixable: true,
				});
				if (shouldFix("integration_branch_missing")) {
					writeIntegrationBranch(
						basePath,
						milestone.id,
						resolution.effectiveBranch,
					);
					fixesApplied.push(
						`updated integration branch for ${milestone.id} to "${resolution.effectiveBranch}"`,
					);
				}
				continue;
			}
			if (resolution.status === "missing") {
				issues.push({
					severity: "error",
					code: "integration_branch_missing",
					scope: "milestone",
					unitId: milestone.id,
					message: resolution.reason,
					fixable: false,
				});
			}
		}
	} catch {
		// Non-fatal — integration branch check failed
	}
	// ── Orphaned worktree directories ────────────────────────────────────
	// Worktree removal can fail after a branch delete, leaving a directory
	// that is no longer registered with git. These orphaned dirs cause
	// "already exists" errors when re-creating the same worktree name.
	try {
		const wtDir = worktreesDir(basePath);
		if (existsSync(wtDir)) {
			// Resolve symlinks and normalize separators so that symlinked .sf
			// paths (e.g. ~/.sf/projects/<hash>/worktrees/…) match the paths
			// returned by `git worktree list`.
			const normalizePath = (p) => {
				try {
					p = realpathSync(p);
				} catch {
					/* path may not exist */
				}
				return p.replaceAll("\\", "/");
			};
			const registeredPaths = new Set(
				nativeWorktreeList(basePath).map((entry) => normalizePath(entry.path)),
			);
			for (const entry of readdirSync(wtDir)) {
				const fullPath = join(wtDir, entry);
				try {
					if (!statSync(fullPath).isDirectory()) continue;
				} catch {
					continue;
				}
				const normalizedFullPath = normalizePath(fullPath);
				if (!registeredPaths.has(normalizedFullPath)) {
					// Skip directories that only contain doctor artifacts (.sf/doctor-history.jsonl).
					// appendDoctorHistory() can recreate these dirs during the audit itself,
					// causing a circular false positive (#3105 Bug 1).
					if (isDoctorArtifactOnly(fullPath)) continue;
					issues.push({
						severity: "warning",
						code: "worktree_directory_orphaned",
						scope: "project",
						unitId: entry,
						message: `Worktree directory ${fullPath} exists on disk but is not registered with git. Run "git worktree prune" or doctor --fix to remove it.`,
						fixable: true,
					});
					if (shouldFix("worktree_directory_orphaned")) {
						try {
							rmSync(fullPath, { recursive: true, force: true });
							fixesApplied.push(
								`removed orphaned worktree directory ${fullPath}`,
							);
						} catch {
							fixesApplied.push(
								`failed to remove orphaned worktree directory ${fullPath}`,
							);
						}
					}
				}
			}
		}
	} catch {
		// Non-fatal — orphaned worktree directory check failed
	}
	// ── Stale uncommitted changes ────────────────────────────────────────────
	// If the working tree has uncommitted changes and the last commit was
	// longer ago than the configured threshold, flag it and optionally
	// auto-commit a safety snapshot so work isn't lost.
	try {
		const prefs = loadEffectiveSFPreferences()?.preferences ?? {};
		const thresholdMinutes = prefs.stale_commit_threshold_minutes ?? 30;
		if (thresholdMinutes > 0) {
			const dirty = nativeHasChanges(basePath);
			if (dirty) {
				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);
					issues.push({
						severity: "warning",
						code: "stale_uncommitted_changes",
						scope: "project",
						unitId: "project",
						message: `Uncommitted changes detected with no commit in ${mins} minute${mins === 1 ? "" : "s"} (threshold: ${thresholdMinutes}m). Snapshotting tracked files.`,
						fixable: true,
					});
					if (shouldFix("stale_uncommitted_changes")) {
						const protectedDeletions = listProtectedSnapshotDeletions(basePath);
						if (protectedDeletions.length > 0) {
							fixesApplied.push(
								formatProtectedSnapshotDeletionMessage(protectedDeletions),
							);
						} else {
							try {
								nativeAddTracked(basePath);
								const commitMsg = `sf snapshot: uncommitted changes after ${mins}m inactivity`;
								const result = nativeCommit(basePath, commitMsg);
								if (result) {
									fixesApplied.push(
										`created sf snapshot after ${mins}m of uncommitted changes`,
									);
								} else {
									fixesApplied.push(
										"sf snapshot skipped — nothing to commit after staging tracked files",
									);
								}
							} catch {
								fixesApplied.push("failed to create sf snapshot commit");
							}
						}
					}
				}
			}
		}
	} catch {
		// Non-fatal — stale commit check failed
	}
	// ── Worktree lifecycle checks ──────────────────────────────────────────
	// Check SF-managed worktrees for: merged branches, stale work, dirty
	// state, and unpushed commits. Only worktrees under .sf/worktrees/.
	try {
		const healthStatuses = getAllWorktreeHealth(basePath);
		const cwd = process.cwd();
		for (const health of healthStatuses) {
			const wt = health.worktree;
			const isCwd = wt.path === cwd || cwd.startsWith(wt.path + sep);
			// Branch fully merged into main — safe to remove
			if (health.mergedIntoMain) {
				issues.push({
					severity: "info",
					code: "worktree_branch_merged",
					scope: "project",
					unitId: wt.name,
					message: `Worktree "${wt.name}" (branch ${wt.branch}) is fully merged into main${health.safeToRemove ? " — safe to remove" : ""}`,
					fixable: health.safeToRemove,
				});
				if (
					health.safeToRemove &&
					shouldFix("worktree_branch_merged") &&
					!isCwd
				) {
					try {
						const { removeWorktree } = await import("./worktree-manager.js");
						removeWorktree(basePath, wt.name, {
							deleteBranch: true,
							branch: wt.branch,
						});
						fixesApplied.push(
							`removed merged worktree "${wt.name}" and deleted branch ${wt.branch}`,
						);
					} catch {
						fixesApplied.push(`failed to remove merged worktree "${wt.name}"`);
					}
				}
				// If merged, skip the stale/dirty/unpushed checks — they're irrelevant
				continue;
			}
			// Stale: no commits in N days, not merged
			if (health.stale) {
				const days = Math.floor(health.lastCommitAgeDays);
				issues.push({
					severity: "warning",
					code: "worktree_stale",
					scope: "project",
					unitId: wt.name,
					message: `Worktree "${wt.name}" has had no commits in ${days} day${days === 1 ? "" : "s"}`,
					fixable: false,
				});
			}
			// Dirty: uncommitted changes in a worktree (only flag on stale worktrees to avoid noise)
			if (health.dirty && health.stale) {
				issues.push({
					severity: "warning",
					code: "worktree_dirty",
					scope: "project",
					unitId: wt.name,
					message: `Worktree "${wt.name}" has ${health.dirtyFileCount} uncommitted file${health.dirtyFileCount === 1 ? "" : "s"} and is stale`,
					fixable: false,
				});
			}
			// Unpushed: commits not on any remote (only flag on stale worktrees to avoid noise)
			if (health.unpushedCommits > 0 && health.stale) {
				issues.push({
					severity: "warning",
					code: "worktree_unpushed",
					scope: "project",
					unitId: wt.name,
					message: `Worktree "${wt.name}" has ${health.unpushedCommits} unpushed commit${health.unpushedCommits === 1 ? "" : "s"}`,
					fixable: false,
				});
			}
		}
	} catch {
		// Non-fatal — worktree lifecycle check failed
	}
}
