/**
 * SF Milestone Actions — Park, Unpark, and Discard operations.
 *
 * Park: Creates a PARKED.md marker file. deriveState() skips parked milestones
 * when finding the active milestone, but keeps them in the registry.
 *
 * Unpark: Removes the PARKED.md marker. The milestone resumes normal state
 * derivation (active/pending depending on position and dependencies).
 *
 * Discard: Permanently removes the milestone directory. Also prunes the
 * DB-backed queue order, with legacy JSON fallback when SQLite is unavailable.
 */
import {
	existsSync,
	readFileSync,
	rmSync,
	unlinkSync,
	writeFileSync,
} from "node:fs";
import { join } from "node:path";
import { invalidateAllCaches } from "./cache.js";
import {
	buildMilestoneFileName,
	resolveMilestoneFile,
	resolveMilestonePath,
} from "./paths.js";
import { loadQueueOrder, saveQueueOrder } from "./queue-order.js";
import {
	deleteMilestone,
	getMilestone,
	isDbAvailable,
	updateMilestoneStatus,
} from "./sf-db.js";
import { logWarning } from "./workflow-logger.js";
import { removeWorktree } from "./worktree-manager.js";
// ─── Park ──────────────────────────────────────────────────────────────────
/**
 * Park a milestone — creates a PARKED.md marker file with reason and timestamp.
 * Parked milestones are skipped during active-milestone discovery but stay on disk.
 * Returns true if successfully parked, false if milestone not found or already parked.
 */
export function parkMilestone(basePath, milestoneId, reason) {
	const mDir = resolveMilestonePath(basePath, milestoneId);
	if (!mDir || !existsSync(mDir)) return false;
	// Guard: do not park a completed milestone — it would corrupt depends_on satisfaction
	const summaryFile = resolveMilestoneFile(basePath, milestoneId, "SUMMARY");
	if (summaryFile) return false;
	const parkedPath = join(mDir, buildMilestoneFileName(milestoneId, "PARKED"));
	if (existsSync(parkedPath)) return false; // already parked
	const content = [
		"---",
		`parked_at: ${new Date().toISOString()}`,
		`reason: "${reason.replace(/"/g, '\\"')}"`,
		"---",
		"",
		`# ${milestoneId} — Parked`,
		"",
		`> ${reason}`,
		"",
	].join("\n");
	writeFileSync(parkedPath, content, "utf-8");
	// Sync DB status so deriveStateFromDb also skips this milestone (#2694)
	if (isDbAvailable()) {
		try {
			updateMilestoneStatus(milestoneId, "parked");
		} catch (err) {
			logWarning(
				"engine",
				`parkMilestone DB sync failed for ${milestoneId}: ${err.message}`,
			);
		}
	}
	invalidateAllCaches();
	return true;
}
// ─── Unpark ────────────────────────────────────────────────────────────────
/**
 * Unpark a milestone — removes the PARKED.md marker file.
 * Returns true if successfully unparked, false if milestone not found or not parked.
 */
export function unparkMilestone(basePath, milestoneId) {
	const mDir = resolveMilestonePath(basePath, milestoneId);
	if (!mDir || !existsSync(mDir)) return false;
	const parkedPath = join(mDir, buildMilestoneFileName(milestoneId, "PARKED"));
	const hadParkedFile = existsSync(parkedPath);
	const dbThinksParked =
		isDbAvailable() && getMilestone(milestoneId)?.status === "parked";
	// Recover the reverse desync too: DB can still say "parked" even when the
	// PARKED marker was lost on disk, and /unpark should repair that state.
	if (!hadParkedFile && !dbThinksParked) return false;
	if (hadParkedFile) {
		unlinkSync(parkedPath);
	}
	// Sync DB status so deriveStateFromDb picks up the unparked milestone (#2694)
	if (isDbAvailable()) {
		try {
			updateMilestoneStatus(milestoneId, "active");
		} catch (err) {
			logWarning(
				"engine",
				`unparkMilestone DB sync failed for ${milestoneId}: ${err.message}`,
			);
		}
	}
	invalidateAllCaches();
	return true;
}
// ─── Discard ───────────────────────────────────────────────────────────────
/**
 * Discard a milestone — permanently removes the milestone directory and
 * prunes it from the queue order if present.
 * Returns true if successfully discarded, false if milestone not found.
 */
export function discardMilestone(basePath, milestoneId) {
	const mDir = resolveMilestonePath(basePath, milestoneId);
	if (!mDir || !existsSync(mDir)) return false;
	try {
		removeWorktree(basePath, milestoneId, {
			branch: `milestone/${milestoneId}`,
			deleteBranch: true,
		});
	} catch (err) {
		logWarning(
			"engine",
			`discardMilestone worktree cleanup failed for ${milestoneId}: ${err.message}`,
		);
	}
	rmSync(mDir, { recursive: true, force: true });
	// Prune from queue order if present.
	const order = loadQueueOrder(basePath);
	if (order && order.includes(milestoneId)) {
		saveQueueOrder(
			basePath,
			order.filter((id) => id !== milestoneId),
		);
	}
	if (isDbAvailable()) {
		try {
			deleteMilestone(milestoneId);
		} catch (err) {
			logWarning(
				"engine",
				`discardMilestone DB cleanup failed for ${milestoneId}: ${err.message}`,
			);
		}
	}
	invalidateAllCaches();
	return true;
}
// ─── Query ─────────────────────────────────────────────────────────────────
/**
 * Check whether a milestone is parked (PARKED.md exists).
 */
export function isParked(basePath, milestoneId) {
	return !!resolveMilestoneFile(basePath, milestoneId, "PARKED");
}
/**
 * Read the park reason from PARKED.md frontmatter.
 * Returns null if the milestone is not parked or the reason can't be extracted.
 */
export function getParkedReason(basePath, milestoneId) {
	const parkedFile = resolveMilestoneFile(basePath, milestoneId, "PARKED");
	if (!parkedFile) return null;
	try {
		const content = readFileSync(parkedFile, "utf-8");
		const match = content.match(/^---\n([\s\S]*?)\n---/);
		if (!match) return null;
		const reasonMatch = match[1].match(/reason:\s*"([^"]*?)"/);
		return reasonMatch ? reasonMatch[1] : null;
	} catch {
		return null;
	}
}
