/**
 * Worktree Health — lifecycle status helpers for SF-managed worktrees.
 *
 * Used by doctor-checks.ts for health audits and by worktree-command.ts
 * for the enhanced `/worktree list` display.
 *
 * Only inspects worktrees under .sf/worktrees/ — SF owns what SF creates.
 */
import { existsSync, lstatSync } from "node:fs";
import {
	nativeDetectMainBranch,
	nativeHasChanges,
	nativeIsAncestor,
	nativeLastCommitEpoch,
	nativeUnpushedCount,
	nativeWorkingTreeStatus,
} from "./native-git-bridge.js";
import { listWorktrees } from "./worktree-manager.js";

// ─── Configuration ─────────────────────────────────────────────────────────
/** Default number of days without commits before a worktree is considered stale. */
const DEFAULT_STALE_DAYS = 14;
// ─── Core ──────────────────────────────────────────────────────────────────
/**
 * Compute the health status for a single worktree.
 *
 * @param basePath — the main project root (not the worktree path)
 * @param wt — worktree info from listWorktrees()
 * @param staleDays — days without commits to consider stale (default: 14)
 */
export function getWorktreeHealth(
	basePath,
	wt,
	staleDays = DEFAULT_STALE_DAYS,
) {
	const mainBranch = nativeDetectMainBranch(basePath);
	// Merge status: is the worktree branch fully contained in main?
	let mergedIntoMain = false;
	try {
		mergedIntoMain = nativeIsAncestor(basePath, wt.branch, mainBranch);
	} catch {
		/* default false */
	}
	// Dirty status: check from inside the worktree itself.
	// Use lstatSync (not existsSync) to detect broken symlinks: existsSync follows
	// the symlink and returns false when the target is missing, which is
	// indistinguishable from an absent directory. lstatSync returns successfully
	// for the symlink inode itself, letting us surface the broken-symlink case
	// as { exists: false } rather than silently skipping the dirty check.
	let dirty = false;
	let dirtyFileCount = 0;
	let pathAccessible = false;
	if (wt.exists) {
		try {
			const st = lstatSync(wt.path);
			// If this is a symlink, the target must also exist to be accessible.
			pathAccessible = !st.isSymbolicLink() || existsSync(wt.path);
		} catch {
			// lstatSync throws ENOENT when the path itself is absent — not accessible.
			pathAccessible = false;
		}
	}
	if (pathAccessible) {
		try {
			dirty = nativeHasChanges(wt.path);
			if (dirty) {
				const status = nativeWorkingTreeStatus(wt.path);
				dirtyFileCount = status.split("\n").filter((l) => l.trim()).length;
			}
		} catch {
			/* default clean */
		}
	}
	// Unpushed commits
	let unpushedCommits = 0;
	try {
		const count = nativeUnpushedCount(basePath, wt.branch);
		unpushedCommits = count >= 0 ? count : 0;
	} catch {
		/* default 0 */
	}
	// Last commit age
	let lastCommitEpoch = 0;
	try {
		lastCommitEpoch = nativeLastCommitEpoch(basePath, wt.branch);
	} catch {
		/* default 0 */
	}
	const nowEpoch = Math.floor(Date.now() / 1000);
	const lastCommitAgeDays =
		lastCommitEpoch > 0 ? (nowEpoch - lastCommitEpoch) / 86400 : -1;
	// Stale: old, not merged
	const stale = !mergedIntoMain && lastCommitAgeDays >= staleDays;
	// Safe to remove: merged into main and no dirty files.
	// Unpushed commits don't matter when the branch is merged — the work is already in main.
	const safeToRemove = mergedIntoMain && !dirty;
	return {
		worktree: wt,
		mergedIntoMain,
		dirty,
		dirtyFileCount,
		unpushedCommits,
		lastCommitEpoch,
		lastCommitAgeDays,
		stale,
		safeToRemove,
		pathAccessible,
	};
}
/**
 * Compute health status for all SF-managed worktrees.
 *
 * @param basePath — the main project root
 * @param staleDays — days without commits to consider stale (default: 14)
 */
export function getAllWorktreeHealth(basePath, staleDays = DEFAULT_STALE_DAYS) {
	const worktrees = listWorktrees(basePath);
	return worktrees.map((wt) => getWorktreeHealth(basePath, wt, staleDays));
}
/**
 * Format a human-readable status line for a worktree health entry.
 * Used by `/worktree list` for inline status display.
 */
export function formatWorktreeStatusLine(health) {
	const parts = [];
	if (health.mergedIntoMain) {
		parts.push("✓ merged into main");
		if (health.safeToRemove) {
			parts.push("safe to remove");
		}
	}
	if (health.dirty) {
		parts.push(
			`${health.dirtyFileCount} uncommitted file${health.dirtyFileCount === 1 ? "" : "s"}`,
		);
	}
	if (health.unpushedCommits > 0) {
		parts.push(
			`${health.unpushedCommits} unpushed commit${health.unpushedCommits === 1 ? "" : "s"}`,
		);
	}
	if (health.stale) {
		const days = Math.floor(health.lastCommitAgeDays);
		parts.push(`no commits in ${days} day${days === 1 ? "" : "s"}`);
	} else if (health.lastCommitAgeDays >= 0 && !health.mergedIntoMain) {
		const age = health.lastCommitAgeDays;
		if (age < 1) {
			const hours = Math.floor(age * 24);
			parts.push(`last commit ${hours}h ago`);
		} else {
			const days = Math.floor(age);
			parts.push(`last commit ${days}d ago`);
		}
	}
	if (parts.length === 0) {
		return "clean";
	}
	return parts.join(" · ");
}
