/**
 * SF Orphan Worktree Sweep
 *
 * Detects orphaned worktree directories left behind by crashed or killed units
 * and either leaves them intact (resumable) or removes them (broken).
 *
 * Called at session_start to ensure stale worktrees from prior crashes don't
 * accumulate indefinitely.
 *
 * Triage logic:
 *   Active    — auto.lock present and PID alive  → leave alone
 *   Resumable — no active lock, but .git file is valid     → leave intact, journal
 *   Broken    — .git missing or unreadable               → prune, journal
 */
import { randomUUID } from "node:crypto";
import { existsSync, lstatSync, readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { getErrorMessage } from "./error-utils.js";
import { emitJournalEvent } from "./journal.js";
import {
	removeWorktree,
	worktreePath,
	worktreesDir,
} from "./worktree-manager.js";

// ─── Internal Helpers ─────────────────────────────────────────────────────────
/**
 * Read the auto.lock file for a worktree and return the PID, or null if absent.
 * The lock lives at <worktreeDir>/.sf/auto.lock (the worktree has its own .sf/).
 */
function readWorktreeLockPid(worktreeDir) {
	const lockPath = join(worktreeDir, ".sf", "auto.lock");
	if (!existsSync(lockPath)) return null;
	try {
		const raw = readFileSync(lockPath, "utf-8");
		const data = JSON.parse(raw);
		return typeof data.pid === "number" ? data.pid : null;
	} catch {
		return null;
	}
}
/**
 * Returns true if the given PID is alive in this OS process table.
 * Uses signal 0 — POSIX / Node standard liveness probe.
 */
function isPidAlive(pid) {
	try {
		process.kill(pid, 0);
		return true;
	} catch {
		return false;
	}
}
/**
 * Returns true if the worktree at `wtPath` has a structurally valid git pointer:
 * a `.git` *file* (not a directory) whose content starts with "gitdir:".
 *
 * A real git worktree always has a .git file. A .git directory means it's a
 * standalone repo nested inside (not a registered worktree). Either way the
 * worktree is not structurally healthy as an SF worktree.
 */
function isStructurallyHealthy(wtPath) {
	const gitPath = join(wtPath, ".git");
	if (!existsSync(gitPath)) return false;
	try {
		const stat = lstatSync(gitPath);
		if (!stat.isFile()) return false;
		const content = readFileSync(gitPath, "utf-8").trim();
		return content.startsWith("gitdir:");
	} catch {
		return false;
	}
}
// ─── Public API ───────────────────────────────────────────────────────────────
/**
 * Sweep `.sf/worktrees/` for orphaned worktrees and triage each one.
 *
 * - Active (PID alive)  → skipped entirely.
 * - Resumable (healthy) → left intact; operator-visible journal event emitted.
 * - Broken (no .git)    → removed via removeWorktree(); journal event emitted.
 *
 * Never throws. Each per-worktree error is accumulated in `errors[]`.
 * Idempotent: running twice with no orphans returns all-empty arrays both times.
 *
 * @param basePath  The project root (not the worktree path).
 */
export function sweepOrphanWorktrees(basePath) {
	const result = { resumed: [], pruned: [], errors: [] };
	const wtDir = worktreesDir(basePath);
	if (!existsSync(wtDir)) return result;
	let entries;
	try {
		entries = readdirSync(wtDir, { withFileTypes: true })
			.filter((d) => d.isDirectory())
			.map((d) => d.name);
	} catch (err) {
		result.errors.push({
			id: "<worktrees-dir>",
			reason: `readdirSync failed: ${getErrorMessage(err)}`,
		});
		return result;
	}
	for (const id of entries) {
		try {
			const wtPath = worktreePath(basePath, id);
			// ── Active check ──────────────────────────────────────────────────
			const pid = readWorktreeLockPid(wtPath);
			if (pid !== null && isPidAlive(pid)) {
				// In-flight unit owns this worktree — leave it completely alone.
				continue;
			}
			// ── Structural health ─────────────────────────────────────────────
			if (isStructurallyHealthy(wtPath)) {
				// Orphan but intact — operator may want to resume or inspect.
				result.resumed.push(id);
				try {
					emitJournalEvent(basePath, {
						ts: new Date().toISOString(),
						flowId: randomUUID(),
						seq: 0,
						eventType: "worktree-orphaned",
						data: {
							milestoneId: id,
							reason: "resumable",
							worktreeDirExists: true,
							detectedAt: new Date().toISOString(),
						},
					});
				} catch {
					// telemetry failure must not abort sweep
				}
			} else {
				// Broken worktree — prune it.
				const detail = !existsSync(join(wtPath, ".git"))
					? "missing .git file"
					: "invalid .git content";
				removeWorktree(basePath, id, { deleteBranch: false, force: true });
				result.pruned.push(id);
				try {
					emitJournalEvent(basePath, {
						ts: new Date().toISOString(),
						flowId: randomUUID(),
						seq: 0,
						eventType: "worktree-orphaned",
						data: {
							milestoneId: id,
							reason: "broken-pruned",
							worktreeDirExists: false,
							detail,
							detectedAt: new Date().toISOString(),
						},
					});
				} catch {
					// telemetry failure must not abort sweep
				}
			}
		} catch (err) {
			result.errors.push({
				id,
				reason: getErrorMessage(err),
			});
		}
	}
	return result;
}
