import { join } from "node:path";
import { getAutoSession } from "../../auto/session.js";
import { handleCmux } from "../../commands-cmux.js";
import {
	ensurePreferencesFile,
	handlePrefs,
	handlePrefsMode,
	handlePrefsWizard,
} from "../../commands-prefs-wizard.js";
import { runEnvironmentChecks } from "../../doctor-environment.js";
import {
	EXPERIMENTAL_FLAGS,
	getAllExperimentalFlags,
	getExperimentalFlag,
	setAllExperimentalFlags,
	setExperimentalFlag,
} from "../../experimental.js";
import { inferPresetName, resolvePreset } from "../../operating-model.js";
import {
	getGlobalSFPreferencesPath,
	getProjectSFPreferencesPath,
} from "../../preferences.js";
import {
	computeProgressScore,
	formatProgressLine,
} from "../../progress-score.js";
import { setSessionModelOverride } from "../../session-model-override.js";
import { sfHome } from "../../sf-home.js";
import { formattedShortcutPair } from "../../shortcut-defs.js";
import { deriveState } from "../../state.js";
import { writeUokDiagnostics } from "../../uok/diagnostic-synthesis.js";
import { PRIMARY_COMMANDS, PUBLIC_TOP_LEVEL_SUBCOMMANDS } from "../catalog.js";
import { projectRoot } from "../context.js";

const HELP_CATEGORY_ORDER = [
	{
		title: "PRIMARY",
		commands: ["next", "autonomous", "discuss", "quick", "help"],
	},
	{
		title: "WORKFLOW",
		commands: [
			"start",
			"templates",
			"workflow",
			"ship",
			"backlog",
			"schedule",
			"triage",
		],
	},
	{
		title: "STATUS",
		commands: ["status", "queue", "visualize", "history", "logs", "forensics"],
	},
	{
		title: "CONTROL",
		commands: ["pause", "stop", "skip", "undo", "park", "unpark", "capture"],
	},
	{
		title: "SETUP",
		commands: [
			"init",
			"setup",
			"doctor",
			"repair",
			"remote",
			"knowledge",
			"config",
			"keys",
			"model",
			"prefs",
			"update",
		],
	},
];

function commandByName() {
	return new Map(
		PUBLIC_TOP_LEVEL_SUBCOMMANDS.map((command) => [command.cmd, command]),
	);
}

function commandLine(command) {
	return `  /${command.cmd.padEnd(12)} ${command.desc ?? ""}`.trimEnd();
}

function groupedPublicHelpLines() {
	const byName = commandByName();
	const emitted = new Set();
	const lines = ["SF — Singularity Forge workflow runtime\n"];
	for (const category of HELP_CATEGORY_ORDER) {
		const categoryLines = category.commands
			.map((name) => byName.get(name))
			.filter(Boolean)
			.map((command) => {
				emitted.add(command.cmd);
				return commandLine(command);
			});
		if (categoryLines.length === 0) continue;
		lines.push(category.title, ...categoryLines, "");
	}
	const other = PUBLIC_TOP_LEVEL_SUBCOMMANDS.filter(
		(command) => !emitted.has(command.cmd),
	);
	if (other.length > 0) {
		lines.push("OTHER", ...other.map(commandLine), "");
	}
	return lines;
}

function keywordHelpLines(keyword) {
	const needle = keyword.toLowerCase();
	const tokens = needle
		.split(/[^a-z0-9-]+/i)
		.map((token) => token.trim())
		.filter(
			(token) =>
				token.length > 2 &&
				!new Set([
					"the",
					"and",
					"for",
					"with",
					"want",
					"show",
					"see",
					"my",
					"how",
					"what",
					"where",
				]).has(token),
		);
	const matches = PUBLIC_TOP_LEVEL_SUBCOMMANDS.filter(
		(command) =>
			command.cmd.toLowerCase().includes(needle) ||
			(command.desc ?? "").toLowerCase().includes(needle) ||
			tokens.some(
				(token) =>
					command.cmd.toLowerCase().includes(token) ||
					(command.desc ?? "").toLowerCase().includes(token),
			),
	);
	if (matches.length === 0) {
		return [
			`No public SF command matched "${keyword}".`,
			"Try /help all, or describe the work in /discuss.",
		];
	}
	return [`SF command matches for "${keyword}"\n`, ...matches.map(commandLine)];
}

export function showHelp(ctx, args = "") {
	const request = args.trim();
	if (request.length > 0 && request.toLowerCase() !== "all") {
		ctx.ui.notify(keywordHelpLines(request).join("\n"), "info");
		return;
	}
	if (request.toLowerCase() === "all") {
		ctx.ui.notify(groupedPublicHelpLines().join("\n"), "info");
		return;
	}
	const primary = PUBLIC_TOP_LEVEL_SUBCOMMANDS.filter((command) =>
		PRIMARY_COMMANDS.has(command.cmd),
	);
	ctx.ui.notify(
		[
			"SF — Singularity Forge workflow runtime\n",
			"PRIMARY",
			...primary.map(commandLine),
			"",
			`Shift+Tab cycles runtime mode. ${formattedShortcutPair("dashboard")} opens /status.`,
			"Type /<letters> to search secondary commands.",
			"Use /help all for the grouped public command reference.",
		].join("\n"),
		"info",
	);
}
export async function handleStatus(ctx) {
	const basePath = projectRoot();
	// Open DB in cold sessions so status uses DB-backed state, not filesystem fallback (#3385)
	const { ensureDbOpen } = await import("../../bootstrap/dynamic-tools.js");
	await ensureDbOpen();
	const state = await deriveState(basePath);
	if (state.registry.length === 0) {
		ctx.ui.notify("No SF milestones found. Run /next to start.", "info");
		return;
	}
	const { SFDashboardOverlay } = await import("../../dashboard-overlay.js");
	const result = await ctx.ui.custom(
		(tui, theme, _kb, done) =>
			new SFDashboardOverlay(tui, theme, () => done(true)),
		{
			overlay: true,
			overlayOptions: {
				width: "90%",
				minWidth: 80,
				maxHeight: "92%",
				anchor: "center",
			},
		},
	);
	if (result === undefined) {
		ctx.ui.notify(formatTextStatus(state), "info");
	}
}
export async function fireStatusViaCommand(ctx) {
	await handleStatus(ctx);
}
export async function handleVisualize(ctx) {
	if (!ctx.hasUI) {
		ctx.ui.notify("Visualizer requires an interactive terminal.", "warning");
		return;
	}
	const { SFVisualizerOverlay } = await import("../../visualizer-overlay.js");
	const result = await ctx.ui.custom(
		(tui, theme, _kb, done) =>
			new SFVisualizerOverlay(tui, theme, () => done(true)),
		{
			overlay: true,
			overlayOptions: {
				width: "80%",
				minWidth: 80,
				maxHeight: "90%",
				anchor: "center",
			},
		},
	);
	if (result === undefined) {
		ctx.ui.notify(
			"Visualizer requires an interactive terminal. Use /status for a text-based overview.",
			"warning",
		);
	}
}
export async function handleSetup(args, ctx) {
	const { detectProjectState, hasGlobalSetup } = await import(
		"../../detection.js"
	);
	const globalConfigured = hasGlobalSetup();
	const detection = detectProjectState(projectRoot());
	const statusLines = ["SF Setup Status\n"];
	statusLines.push(
		`  Global preferences: ${globalConfigured ? "configured" : "not set"}`,
	);
	statusLines.push(`  Project state: ${detection.state}`);
	if (detection.projectSignals.primaryLanguage) {
		statusLines.push(`  Detected: ${detection.projectSignals.primaryLanguage}`);
	}
	if (args === "llm" || args === "auth") {
		ctx.ui.notify("Use /login to configure LLM authentication.", "info");
		return;
	}
	if (args === "search") {
		ctx.ui.notify("Use /search-provider to configure web search.", "info");
		return;
	}
	if (args === "remote") {
		ctx.ui.notify("Use /remote to configure remote questions.", "info");
		return;
	}
	if (args === "keys") {
		const { handleKeys } = await import("../../key-manager.js");
		await handleKeys("", ctx);
		return;
	}
	if (args === "prefs") {
		await ensurePreferencesFile(getGlobalSFPreferencesPath(), ctx, "global");
		await handlePrefsWizard(ctx, "global");
		return;
	}
	ctx.ui.notify(statusLines.join("\n"), "info");
	ctx.ui.notify(
		"Available setup commands:\n" +
			"  /setup llm     — LLM authentication\n" +
			"  /setup search  — Web search provider\n" +
			"  /setup remote  — Remote questions (Discord/Slack/Telegram)\n" +
			"  /setup keys    — Tool API keys\n" +
			"  /setup prefs   — Global preferences wizard",
		"info",
	);
}
function sortModelsForSelection(models, currentModel) {
	return [...models].sort((a, b) => {
		const aCurrent =
			currentModel &&
			a.provider === currentModel.provider &&
			a.id === currentModel.id;
		const bCurrent =
			currentModel &&
			b.provider === currentModel.provider &&
			b.id === currentModel.id;
		if (aCurrent && !bCurrent) return -1;
		if (!aCurrent && bCurrent) return 1;
		const providerCmp = a.provider.localeCompare(b.provider);
		if (providerCmp !== 0) return providerCmp;
		return a.id.localeCompare(b.id);
	});
}
function buildProviderModelGroups(models, currentModel) {
	const byProvider = new Map();
	for (const model of sortModelsForSelection(models, currentModel)) {
		let group = byProvider.get(model.provider);
		if (!group) {
			group = [];
			byProvider.set(model.provider, group);
		}
		group.push(model);
	}
	return byProvider;
}
async function selectModelByProvider(title, models, ctx, currentModel) {
	const byProvider = buildProviderModelGroups(models, currentModel);
	const providerOptions = Array.from(byProvider.entries()).map(
		([provider, group]) =>
			`${provider} (${group.length} model${group.length === 1 ? "" : "s"})`,
	);
	providerOptions.push("(cancel)");
	const providerChoice = await ctx.ui.select(
		`${title} — choose provider:`,
		providerOptions,
	);
	if (
		!providerChoice ||
		typeof providerChoice !== "string" ||
		providerChoice === "(cancel)"
	)
		return undefined;
	const providerName = providerChoice.replace(/ \(\d+ models?\)$/, "");
	const providerModels = byProvider.get(providerName);
	if (!providerModels || providerModels.length === 0) return undefined;
	const optionToModel = new Map();
	const modelOptions = providerModels.map((model) => {
		const isCurrent =
			currentModel &&
			model.provider === currentModel.provider &&
			model.id === currentModel.id;
		const label = `${isCurrent ? "* " : ""}${model.id}`;
		optionToModel.set(label, model);
		return label;
	});
	modelOptions.push("(cancel)");
	const modelChoice = await ctx.ui.select(
		`${title} — ${providerName}:`,
		modelOptions,
	);
	if (
		!modelChoice ||
		typeof modelChoice !== "string" ||
		modelChoice === "(cancel)"
	)
		return undefined;
	return optionToModel.get(modelChoice);
}
async function resolveRequestedModel(query, ctx) {
	const { resolveModelId } = await import("../../auto-model-selection.js");
	const models = ctx.modelRegistry.getAvailable();
	const exact = resolveModelId(query, models, ctx.model?.provider);
	if (exact) return exact;
	const lowerQuery = query.toLowerCase();
	const partialMatches = models.filter(
		(model) =>
			model.id.toLowerCase().includes(lowerQuery) ||
			`${model.provider}/${model.id}`.toLowerCase().includes(lowerQuery),
	);
	if (partialMatches.length === 1) return partialMatches[0];
	if (partialMatches.length === 0 || !ctx.hasUI) return undefined;
	return selectModelByProvider(
		`Multiple models match "${query}"`,
		partialMatches,
		ctx,
		ctx.model,
	);
}
async function handleModel(trimmedArgs, ctx, pi) {
	const availableModels = ctx.modelRegistry.getAvailable();
	if (availableModels.length === 0) {
		ctx.ui.notify(
			"No available models found. Check provider auth and model discovery.",
			"warning",
		);
		return;
	}
	if (!pi) {
		ctx.ui.notify("Model switching is unavailable in this context.", "warning");
		return;
	}
	const trimmed = trimmedArgs.trim();
	let targetModel;
	if (!trimmed) {
		if (!ctx.hasUI) {
			const current = ctx.model
				? `${ctx.model.provider}/${ctx.model.id}`
				: "(none)";
			ctx.ui.notify(
				`Current model: ${current}\nUsage: /model <provider/model|model-id>`,
				"info",
			);
			return;
		}
		targetModel = await selectModelByProvider(
			"Select session model:",
			availableModels,
			ctx,
			ctx.model,
		);
	} else {
		targetModel = await resolveRequestedModel(trimmed, ctx);
	}
	if (!targetModel) {
		ctx.ui.notify(
			`Model "${trimmed}" not found. Use /model with an exact provider/model or a unique model ID.`,
			"warning",
		);
		return;
	}
	const ok = await pi.setModel(targetModel);
	if (!ok) {
		ctx.ui.notify(
			`No API key for ${targetModel.provider}/${targetModel.id}`,
			"warning",
		);
		return;
	}
	// /model is an explicit per-session pin for SF dispatches.
	// This is captured at auto bootstrap so it survives internal session
	// switches during /autonomous and /next runs.
	const sessionId = ctx.sessionManager?.getSessionId?.();
	if (sessionId) {
		setSessionModelOverride(sessionId, {
			provider: targetModel.provider,
			id: targetModel.id,
		});
	}
	ctx.ui.notify(`Model: ${targetModel.provider}/${targetModel.id}`, "info");
}
function formatModeState(mode) {
	if (!mode) return "SF — mode unknown";
	const session = getAutoSession();
	if (session.isYolo()) return "SF — 🚀 YOLO";
	const preset = inferPresetName(mode);
	if (preset) return `SF — ${preset} mode`;
	return `SF — ${mode.workMode} · ${mode.runControl} · ${mode.modelMode}`;
}
function handleModeCommand(args, ctx) {
	const s = getAutoSession();
	const parts = args.trim().split(/\s+/).filter(Boolean);
	if (parts.length === 0) {
		const mode = s.getMode();
		ctx.ui.notify(formatModeState(mode), "info");
		return true;
	}
	const name = parts[0].toLowerCase();
	// "yolo" is a special toggle — not a standard preset
	if (name === "yolo") {
		const enabled = s.toggleYolo();
		if (ctx.settingsManager && ctx.settingsManager.toggleYOLO) {
			ctx.settingsManager.toggleYOLO();
		}
		if (enabled) {
			ctx.ui.notify(
				"🚀 YOLO ON — build · autonomous · deep · unrestricted · no git prompts",
				"success",
			);
		} else {
			ctx.ui.notify("YOLO OFF — mode restored", "info");
		}
		return true;
	}
	const preset = resolvePreset(name);
	if (preset) {
		// If YOLO is active, exit it first so status and safe-git bypass are cleared
		if (s.isYolo()) {
			s.toggleYolo();
			if (ctx.settingsManager && ctx.settingsManager.toggleYOLO) {
				ctx.settingsManager.toggleYOLO();
			}
		}
		const prev = inferPresetName(s.getMode()) ?? s.getMode().workMode;
		s.setMode({
			workMode: preset.workMode,
			runControl: preset.runControl,
			modelMode: preset.modelMode,
			permissionProfile: preset.permissionProfile,
		});
		ctx.ui.notify(`Mode: ${prev} → ${name}  (${preset.description})`, "info");
		return true;
	}
	// Fall back: treat as raw workMode for power users
	const transition = s.setMode({ workMode: name });
	ctx.ui.notify(
		`Mode: ${transition.from.workMode} → ${transition.to.workMode}`,
		"info",
	);
	return true;
}
function handleControlCommand(args, ctx) {
	const s = getAutoSession();
	const runControl = args.trim();
	if (!runControl) {
		const mode = s.getMode();
		ctx.ui.notify(`Run control: ${mode.runControl}`, "info");
		return true;
	}
	const transition = s.setMode({ runControl });
	ctx.ui.notify(
		`Run control: ${transition.from.runControl} → ${transition.to.runControl}`,
		"info",
	);
	return true;
}
function handlePermissionProfileCommand(args, ctx) {
	const s = getAutoSession();
	const permissionProfile = args.trim();
	if (!permissionProfile) {
		const mode = s.getMode();
		ctx.ui.notify(`Permission profile: ${mode.permissionProfile}`, "info");
		return true;
	}
	const transition = s.setMode({ permissionProfile });
	ctx.ui.notify(
		`Permission profile: ${transition.from.permissionProfile} → ${transition.to.permissionProfile}`,
		"info",
	);
	return true;
}
function handleModelModeCommand(args, ctx) {
	const s = getAutoSession();
	const modelMode = args.trim();
	if (!modelMode) {
		const mode = s.getMode();
		ctx.ui.notify(`Model mode: ${mode.modelMode}`, "info");
		return true;
	}
	const transition = s.setMode({ modelMode });
	ctx.ui.notify(
		`Model mode: ${transition.from.modelMode} → ${transition.to.modelMode}`,
		"info",
	);
	return true;
}
export async function handleCoreCommand(trimmed, ctx, pi) {
	if (
		trimmed === "help" ||
		trimmed === "h" ||
		trimmed === "?" ||
		trimmed.startsWith("help ")
	) {
		showHelp(ctx, trimmed.startsWith("help ") ? trimmed.slice(5).trim() : "");
		return true;
	}
	if (trimmed === "status") {
		await handleStatus(ctx);
		return true;
	}
	if (trimmed === "visualize") {
		await handleVisualize(ctx);
		return true;
	}
	if (trimmed === "widget" || trimmed.startsWith("widget ")) {
		const { cycleWidgetMode, setWidgetMode, getWidgetMode } = await import(
			"../../auto-dashboard.js"
		);
		const arg = trimmed.replace(/^widget\s*/, "").trim();
		if (arg === "full" || arg === "small" || arg === "min" || arg === "off") {
			setWidgetMode(arg);
		} else {
			cycleWidgetMode();
		}
		ctx.ui.notify(`Widget: ${getWidgetMode()}`, "info");
		return true;
	}
	if (trimmed === "model" || trimmed.startsWith("model ")) {
		await handleModel(trimmed.replace(/^model\s*/, "").trim(), ctx, pi);
		return true;
	}
	if (trimmed === "mode" || trimmed.startsWith("mode ")) {
		const modeArgs = trimmed.replace(/^mode\s*/, "").trim();
		// If arg is a work mode (chat/plan/build/review/repair/research), use new mode system
		const workModes = [
			"chat",
			"plan",
			"build",
			"review",
			"repair",
			"research",
			"verify",
			"document",
			"challenge",
		];
		if (workModes.includes(modeArgs)) {
			handleModeCommand(modeArgs, ctx);
			return true;
		}
		// Otherwise fall back to old prefs mode (solo/team)
		const scope = modeArgs === "project" ? "project" : "global";
		const path =
			scope === "project"
				? getProjectSFPreferencesPath()
				: getGlobalSFPreferencesPath();
		await ensurePreferencesFile(path, ctx, scope);
		await handlePrefsMode(ctx, scope);
		return true;
	}
	if (trimmed === "control" || trimmed.startsWith("control ")) {
		handleControlCommand(trimmed.replace(/^control\s*/, "").trim(), ctx);
		return true;
	}
	if (
		trimmed === "permission-profile" ||
		trimmed.startsWith("permission-profile ")
	) {
		handlePermissionProfileCommand(
			trimmed.replace(/^permission-profile\s*/, "").trim(),
			ctx,
		);
		return true;
	}
	if (trimmed === "model-mode" || trimmed.startsWith("model-mode ")) {
		handleModelModeCommand(trimmed.replace(/^model-mode\s*/, "").trim(), ctx);
		return true;
	}
	if (trimmed === "prefs" || trimmed.startsWith("prefs ")) {
		await handlePrefs(trimmed.replace(/^prefs\s*/, "").trim(), ctx);
		return true;
	}
	if (trimmed === "tasks" || trimmed.startsWith("tasks ")) {
		const { handleTasks } = await import("./tasks.js");
		await handleTasks(trimmed.replace(/^tasks\s*/, "").trim(), ctx);
		return true;
	}
	if (trimmed === "cmux" || trimmed.startsWith("cmux ")) {
		await handleCmux(trimmed.replace(/^cmux\s*/, "").trim(), ctx);
		return true;
	}
	if (trimmed === "show-config") {
		const { SFConfigOverlay, formatConfigText } = await import(
			"../../config-overlay.js"
		);
		const result = await ctx.ui.custom(
			(tui, theme, _kb, done) =>
				new SFConfigOverlay(tui, theme, () => done(true)),
			{
				overlay: true,
				overlayOptions: {
					width: "65%",
					minWidth: 55,
					maxHeight: "85%",
					anchor: "center",
				},
			},
		);
		if (result === undefined) {
			ctx.ui.notify(formatConfigText(), "info");
		}
		return true;
	}
	if (trimmed === "setup" || trimmed.startsWith("setup ")) {
		await handleSetup(trimmed.replace(/^setup\s*/, "").trim(), ctx);
		return true;
	}
	if (trimmed === "reload") {
		if (process.env.SF_HEADLESS !== "1") {
			ctx.ui.notify(
				"Reloading extensions, skills, prompts, and themes...",
				"info",
			);
			await ctx.reload();
			ctx.ui.notify(
				"Reloaded extensions, skills, prompts, and themes.",
				"info",
			);
			return true;
		}
		ctx.ui.notify(
			"Reloading agent with fresh extension code — session will be resumed...",
			"info",
		);
		const tmpDir = process.env.TEMP ?? "/tmp";
		const sessionIdFile = join(tmpDir, "sf-current-session");
		const sentinelFile = join(tmpDir, "sf-reload-sentinel");
		const { existsSync, readFileSync, unlinkSync, writeFileSync } =
			await import("node:fs");
		if (existsSync(sessionIdFile)) {
			try {
				const sessionId = readFileSync(sessionIdFile, "utf-8").trim();
				if (sessionId) {
					writeFileSync(sentinelFile, sessionId, "utf-8");
				}
			} catch {
				/* non-fatal */
			}
			try {
				unlinkSync(sessionIdFile);
			} catch {
				/* non-fatal */
			}
		}
		// EXIT_RELOAD = 12 — same as kill_agent
		const EXIT_RELOAD = 12; // must match EXIT_RELOAD in src/headless-events.ts
		process.exit(EXIT_RELOAD);
		return true;
	}
	if (trimmed === "skills" || trimmed.startsWith("skills ")) {
		const args = trimmed.replace(/^skills\s*/, "").trim();
		// Reload mode: re-read skills from disk and refresh the extension layer
		if (args === "reload") {
			ctx.ui.notify("Reloading skills from disk...", "info");
			await ctx.reload();
			ctx.ui.notify(
				"Skills reloaded. New and updated skill files are now active.",
				"info",
			);
			return true;
		}
		// Auto-create mode: detect patterns and generate skills
		if (args === "--auto-create" || args === "-a") {
			const {
				detectSkillCandidates,
				isAutoSkillCreationAllowed,
				generateSkill,
			} = await import("../../skills/auto-create.js");
			const basePath = projectRoot();
			if (!isAutoSkillCreationAllowed(basePath)) {
				ctx.ui.notify(
					"Auto-skill creation is on cooldown. Try again tomorrow.",
					"warning",
				);
				return true;
			}
			const candidates = detectSkillCandidates(basePath);
			if (candidates.length === 0) {
				ctx.ui.notify(
					"No skill candidates detected. Need ≥3 occurrences of a pattern.",
					"info",
				);
				return true;
			}
			const lines = ["SF Skill Auto-Creation\n"];
			lines.push(`Detected ${candidates.length} candidate(s):\n`);
			for (const c of candidates) {
				lines.push(`  ${c.name} — ${c.description}`);
			}
			// Generate the first candidate
			const result = generateSkill(candidates[0], basePath);
			if (result.created) {
				lines.push(`\n✓ Created: ${result.path}`);
				ctx.ui.notify(lines.join("\n"), "info");
			} else {
				lines.push(`\n✗ Skipped: ${result.reason}`);
				ctx.ui.notify(lines.join("\n"), "warning");
			}
			return true;
		}
		// Eval mode: run eval cases for a skill
		if (args.startsWith("--eval ") || args.startsWith("-e ")) {
			const skillName = args
				.replace(/^--eval\s+/, "")
				.replace(/^-e\s+/, "")
				.trim();
			const { loadSkills, runSkillEvals } = await import(
				"../../skills/loader.js"
			);
			const { createEvalCase, generateDefaultEvalCase } = await import(
				"../../skills/eval-harness.js"
			);
			const skills = loadSkills(projectRoot());
			const skill = skills.find((s) => s.name === skillName);
			if (!skill) {
				ctx.ui.notify(`Skill "${skillName}" not found.`, "warning");
				return true;
			}
			// Auto-create default eval case if none exist
			const evalDir = join(
				skill.basePath ?? projectRoot(),
				".agents",
				"skills",
				skillName,
				"evals",
			);
			const { existsSync } = await import("node:fs");
			if (!existsSync(evalDir)) {
				const caseDef = generateDefaultEvalCase(skill);
				createEvalCase(
					join(skill.basePath ?? projectRoot(), ".agents", "skills", skillName),
					"default",
					caseDef,
				);
				ctx.ui.notify(
					`Created default eval case for ${skillName}. Run /skills --eval ${skillName} again.`,
					"info",
				);
				return true;
			}
			const result = await runSkillEvals(
				join(skill.basePath ?? projectRoot(), ".agents", "skills", skillName),
				ctx,
			);
			const lines = [`SF Skill Eval: ${skillName}\n`];
			lines.push(
				`Cases: ${result.passedCases}/${result.totalCases} passed · Score: ${(result.totalScore * 100).toFixed(0)}%\n`,
			);
			for (const c of result.cases) {
				const icon = c.passed ? "✓" : "✗";
				lines.push(`${icon} ${c.caseName}: ${c.score.toFixed(2)}`);
				for (const d of c.details.slice(0, 3)) {
					lines.push(`    ${d}`);
				}
			}
			ctx.ui.notify(
				lines.join("\n"),
				result.passedCases === result.totalCases ? "success" : "warning",
			);
			return true;
		}
		// Normal list mode
		const {
			loadSkills,
			getPermittedSkills,
			getModelInvocableSkills,
			getUserInvocableSkills,
		} = await import("../../skills/loader.js");
		const skills = loadSkills(projectRoot(), { includeBundled: true });
		const visibleSkills = getUserInvocableSkills(skills);
		const mode = getAutoSession().getMode();
		const permitted = getPermittedSkills(visibleSkills, mode.permissionProfile);
		const modelInvocable = getModelInvocableSkills(
			visibleSkills,
			mode.workMode,
		);

		const lines = ["SF Skills\n"];
		lines.push(
			`Found ${visibleSkills.length} user-invocable skill(s) · ${permitted.length} permitted · ${modelInvocable.length} model-invocable\n`,
		);

		for (const skill of visibleSkills) {
			const icon = skill.valid ? "✓" : "✗";
			const user = skill.userInvocable ? "U" : " ";
			const model = skill.modelInvocable ? "M" : " ";
			lines.push(
				`${icon} [${user}${model}] ${skill.name} · ${skill.permissionProfile} · ${skill.sideEffects}`,
			);
			if (!skill.valid && skill.errors?.length > 0) {
				lines.push(`    ⚠ ${skill.errors[0]}`);
			}
		}

		lines.push(
			"\nLegend: [U]ser-invocable [M]odel-invocable · R=restricted N=normal T=trusted",
		);
		lines.push(
			"Use /skills --auto-create to detect and generate skills from activity patterns.",
		);
		lines.push("Use /skills --eval <name> to run eval cases for a skill.");
		ctx.ui.notify(lines.join("\n"), "info");
		return true;
	}
	if (trimmed === "experimental" || trimmed.startsWith("experimental ")) {
		await handleExperimentalCommand(
			trimmed.replace(/^experimental\s*/, "").trim(),
			ctx,
		);
		return true;
	}
	if (trimmed === "diff" || trimmed.startsWith("diff ")) {
		await handleDiffCommand(trimmed.replace(/^diff\s*/, "").trim(), ctx);
		return true;
	}
	if (trimmed === "theme" || trimmed.startsWith("theme ")) {
		await handleThemeCommand(trimmed.replace(/^theme\s*/, "").trim(), ctx);
		return true;
	}
	if (trimmed === "rename" || trimmed.startsWith("rename ")) {
		await handleRenameCommand(trimmed.replace(/^rename\s*/, "").trim(), ctx);
		return true;
	}
	if (trimmed === "streamer-mode" || trimmed.startsWith("streamer-mode ")) {
		await handleStreamerModeCommand(
			trimmed.replace(/^streamer-mode\s*/, "").trim(),
			ctx,
		);
		return true;
	}
	if (
		trimmed === "statusline" ||
		trimmed.startsWith("statusline ") ||
		trimmed === "footer-config" ||
		trimmed.startsWith("footer-config ")
	) {
		await handleStatuslineCommand(
			trimmed.replace(/^(?:statusline|footer-config)\s*/, "").trim(),
			ctx,
		);
		return true;
	}
	if (
		trimmed === "search" ||
		trimmed.startsWith("search ") ||
		trimmed === "find" ||
		trimmed.startsWith("find ")
	) {
		const query = trimmed.replace(/^(?:search|find)\s*/, "").trim();
		await handleSearchCommand(query, ctx);
		return true;
	}
	if (trimmed === "chronicle" || trimmed.startsWith("chronicle ")) {
		await handleChronicleCommand(
			trimmed.replace(/^chronicle\s*/, "").trim(),
			ctx,
		);
		return true;
	}
	if (trimmed === "rewind" || trimmed === "undo-turn") {
		await handleRewindCommand(ctx);
		return true;
	}
	if (trimmed === "instructions" || trimmed.startsWith("instructions ")) {
		await handleInstructionsCommand(
			trimmed.replace(/^instructions\s*/, "").trim(),
			ctx,
		);
		return true;
	}
	if (trimmed === "sidekicks" || trimmed.startsWith("sidekicks ")) {
		await handleSidekicksCommand(ctx);
		return true;
	}
	if (trimmed === "ask" || trimmed.startsWith("ask ")) {
		await handleAskCommand(trimmed.replace(/^ask\s*/, "").trim(), ctx);
		return true;
	}
	if (trimmed === "resume" || trimmed.startsWith("resume ")) {
		await handleResumeCommand(trimmed.replace(/^resume\s*/, "").trim(), ctx);
		return true;
	}
	return false;
}

// ─── /experimental ──────────────────────────────────────────────────────────

async function handleExperimentalCommand(args, ctx) {
	const sub = args.trim();

	if (!sub || sub === "show") {
		const current = getAllExperimentalFlags();
		const lines = ["Experimental features\n"];
		for (const [name, desc] of Object.entries(EXPERIMENTAL_FLAGS)) {
			const on = current[name] === true;
			lines.push(`  ${on ? "✓" : "○"} ${name.padEnd(22)} ${desc}`);
		}
		lines.push(
			"\nUsage: /experimental on [flag]  /experimental off [flag]  /experimental on (all)",
		);
		ctx.ui.notify(lines.join("\n"), "info");
		return;
	}

	if (sub === "on") {
		setAllExperimentalFlags(true);
		ctx.ui.notify("All experimental features enabled.", "info");
		return;
	}
	if (sub === "off") {
		setAllExperimentalFlags(false);
		ctx.ui.notify("All experimental features disabled.", "info");
		return;
	}

	const onMatch = sub.match(/^on\s+(\S+)$/);
	if (onMatch) {
		const flag = onMatch[1];
		if (!EXPERIMENTAL_FLAGS[flag]) {
			ctx.ui.notify(
				`Unknown flag "${flag}". Run /experimental show for the list.`,
				"warning",
			);
			return;
		}
		setExperimentalFlag(flag, true);
		ctx.ui.notify(`${flag} enabled.`, "info");
		return;
	}

	const offMatch = sub.match(/^off\s+(\S+)$/);
	if (offMatch) {
		const flag = offMatch[1];
		if (!EXPERIMENTAL_FLAGS[flag]) {
			ctx.ui.notify(
				`Unknown flag "${flag}". Run /experimental show for the list.`,
				"warning",
			);
			return;
		}
		setExperimentalFlag(flag, false);
		ctx.ui.notify(`${flag} disabled.`, "info");
		return;
	}

	// Unknown sub-command — show usage
	ctx.ui.notify(
		"Usage: /experimental [show|on|off|on <flag>|off <flag>]",
		"info",
	);
}

// ─── /diff ──────────────────────────────────────────────────────────────────

async function handleDiffCommand(args, ctx) {
	const { execSync } = await import("node:child_process");
	const staged = args === "--staged" || args === "--cached";
	try {
		const cmd = staged ? "git diff --staged" : "git diff HEAD";
		const output = execSync(cmd, {
			cwd: projectRoot(),
			encoding: "utf-8",
			stdio: ["pipe", "pipe", "pipe"],
		});
		if (!output.trim()) {
			ctx.ui.notify(
				staged ? "No staged changes." : "No changes since last commit.",
				"info",
			);
			return;
		}
		ctx.ui.notify(output.slice(0, 8000), "info");
	} catch (e) {
		ctx.ui.notify(`git diff failed: ${e.message}`, "error");
	}
}

// ─── /theme ─────────────────────────────────────────────────────────────────

function extractBodyAfterFrontmatterLocal(content) {
	const closingIdx = content.indexOf("\n---", content.indexOf("---"));
	if (closingIdx === -1) return null;
	const afterFrontmatter = content.slice(closingIdx + 4);
	return afterFrontmatter.trim() ? afterFrontmatter : null;
}

async function handleThemeCommand(args, ctx) {
	const THEMES = ["dark", "light", "dim", "auto"];
	const { loadEffectiveSFPreferences } = await import("../../preferences.js");
	const { serializePreferencesToFrontmatter } = await import(
		"../../commands-prefs-wizard.js"
	);
	const { readFileSync, writeFileSync, existsSync } = await import("node:fs");

	if (!args) {
		const current = loadEffectiveSFPreferences()?.preferences?.theme ?? "auto";
		ctx.ui.notify(
			`Current theme: ${current}\nOptions: ${THEMES.join(" | ")}\nUsage: /theme <name>`,
			"info",
		);
		return;
	}
	if (!THEMES.includes(args)) {
		ctx.ui.notify(
			`Unknown theme "${args}". Options: ${THEMES.join(", ")}`,
			"warning",
		);
		return;
	}
	const path = getProjectSFPreferencesPath();
	const { loadProjectSFPreferences } = await import("../../preferences.js");
	const existing = loadProjectSFPreferences();
	const prefs = existing?.preferences ? { ...existing.preferences } : {};
	prefs.version = prefs.version || 1;
	prefs.theme = args;
	const frontmatter = serializePreferencesToFrontmatter(prefs);
	let body =
		"\n# SF Preferences\n\nSee `~/.sf/agent/extensions/sf/docs/preferences-reference.md` for full documentation.\n";
	if (existsSync(path)) {
		const preserved = extractBodyAfterFrontmatterLocal(
			readFileSync(path, "utf-8"),
		);
		if (preserved) body = preserved;
	}
	const { mkdirSync } = await import("node:fs");
	const { dirname } = await import("node:path");
	mkdirSync(dirname(path), { recursive: true });
	writeFileSync(path, `---\n${frontmatter}---${body}`, "utf-8");
	ctx.ui.notify(`Theme set to "${args}". Restart SF to apply.`, "info");
}

// ─── /rename ────────────────────────────────────────────────────────────────

async function handleRenameCommand(name, ctx) {
	if (!name) {
		ctx.ui.notify(
			"Usage: /rename <session name>\nProvide a name for the current session.",
			"info",
		);
		return;
	}
	// Write terminal title via OSC 2
	process.stdout.write(`\x1b]2;${name} — SF\x07`);
	// Store in session metadata if auto-session is available
	try {
		const session = getAutoSession();
		if (session?.setName) {
			session.setName(name);
		}
	} catch {
		/* session metadata not available in all modes */
	}
	ctx.ui.notify(`Session renamed to "${name}".`, "info");
}

// ─── /streamer-mode ─────────────────────────────────────────────────────────

async function handleStreamerModeCommand(args, ctx) {
	const isOn = getExperimentalFlag("streamer_mode");
	if (args === "on" || (!args && !isOn)) {
		setExperimentalFlag("streamer_mode", true);
		ctx.ui.notify(
			"Streamer mode ON — model names and quota details are masked.",
			"info",
		);
	} else if (args === "off" || (!args && isOn)) {
		setExperimentalFlag("streamer_mode", false);
		ctx.ui.notify("Streamer mode OFF.", "info");
	} else {
		const state = isOn ? "ON" : "OFF";
		ctx.ui.notify(
			`Streamer mode is ${state}.\nUsage: /streamer-mode [on|off]`,
			"info",
		);
	}
}

// ─── /statusline ────────────────────────────────────────────────────────────

async function handleStatuslineCommand(args, ctx) {
	if (args.startsWith("script ")) {
		const scriptPath = args.slice(7).trim();
		setExperimentalFlag("status_line_script", scriptPath);
		setExperimentalFlag("status_line", true);
		ctx.ui.notify(
			`Status line script set to: ${scriptPath}\nSTATUS_LINE enabled.`,
			"info",
		);
		return;
	}
	if (args === "off") {
		setExperimentalFlag("status_line", false);
		ctx.ui.notify("Status line script disabled.", "info");
		return;
	}
	ctx.ui.notify(
		[
			"Status line configuration",
			"",
			"  /statusline script <path>   Set user-defined script path",
			"  /statusline off             Disable status line script",
			"",
			"The script is run every 5 seconds. Its stdout becomes a footer status chip.",
			`STATUS_LINE flag: ${getExperimentalFlag("status_line") ? "ON" : "OFF"}`,
		].join("\n"),
		"info",
	);
}

// ─── /search /find ──────────────────────────────────────────────────────────

async function handleSearchCommand(query, ctx) {
	// Gracefully degrade when session manager is unavailable (headless mode)
	try {
		const { getSessionManager } = await import(
			"../../session-manager.js"
		).catch(() => ({ getSessionManager: null }));
		if (!getSessionManager) {
			ctx.ui.notify(
				"Session search is not available in headless mode.",
				"info",
			);
			return;
		}
		const entries = getSessionManager?.()?.getEntries?.() ?? [];
		const term = query.toLowerCase();
		const matches = entries
			.filter((e) => {
				const text = (
					typeof e.content === "string"
						? e.content
						: JSON.stringify(e.content ?? "")
				).toLowerCase();
				return term ? text.includes(term) : true;
			})
			.slice(0, 20);
		if (matches.length === 0) {
			ctx.ui.notify(`No results for "${query}".`, "info");
			return;
		}
		const lines = [`Search: "${query}" — ${matches.length} result(s)\n`];
		for (const m of matches) {
			const snippet =
				typeof m.content === "string"
					? m.content.slice(0, 120).replace(/\n/g, " ")
					: JSON.stringify(m.content).slice(0, 120);
			lines.push(`  [${m.type ?? "entry"}] ${snippet}`);
		}
		ctx.ui.notify(lines.join("\n"), "info");
	} catch (e) {
		ctx.ui.notify(`Search failed: ${e.message}`, "error");
	}
}

// ─── /chronicle ─────────────────────────────────────────────────────────────

async function handleChronicleCommand(args, ctx) {
	try {
		const { readSFDb } = await import("../../sf-db.js").catch(() => ({
			readSFDb: null,
		}));
		if (!readSFDb) {
			ctx.ui.notify(
				"Chronicle requires the SF database. Run /init first.",
				"warning",
			);
			return;
		}
		const { execSync } = await import("node:child_process");
		const lines = ["Session Chronicle\n"];
		// Git log for recent activity
		try {
			const gitLog = execSync(
				"git log --oneline --no-merges -20 2>/dev/null || true",
				{ cwd: projectRoot(), encoding: "utf-8" },
			);
			if (gitLog.trim()) {
				lines.push("Recent commits:");
				lines.push(gitLog.trim());
				lines.push("");
			}
		} catch {
			/* non-fatal */
		}

		if (args.startsWith("search ")) {
			const term = args.slice(7).trim();
			ctx.ui.notify(
				`Chronicle search for "${term}" — use /search for session entry search.`,
				"info",
			);
			return;
		}
		lines.push("Tip: /search <query> searches the current session timeline.");
		ctx.ui.notify(lines.join("\n"), "info");
	} catch (e) {
		ctx.ui.notify(`Chronicle failed: ${e.message}`, "error");
	}
}

// ─── /rewind ────────────────────────────────────────────────────────────────

async function handleRewindCommand(ctx) {
	try {
		const reverted = await ctx.rewind?.();
		if (reverted) {
			ctx.ui.notify(
				"Rewound last turn. File changes from that turn have been reverted.",
				"info",
			);
		} else {
			ctx.ui.notify(
				"Rewind is not available in this context. Use /undo to revert the last completed unit.",
				"info",
			);
		}
	} catch {
		ctx.ui.notify(
			"Rewind is not available. Use /undo to revert the last completed unit.",
			"info",
		);
	}
}

// ─── /instructions ──────────────────────────────────────────────────────────

async function handleInstructionsCommand(_args, ctx) {
	const { existsSync } = await import("node:fs");
	const { join: pathJoin } = await import("node:path");
	const { homedir } = await import("node:os");
	const root = projectRoot();
	const home = homedir();

	const candidates = [
		{ label: "AGENTS.md", path: pathJoin(root, "AGENTS.md") },
		{ label: "CLAUDE.md", path: pathJoin(root, "CLAUDE.md") },
		{ label: "GEMINI.md", path: pathJoin(root, "GEMINI.md") },
		{
			label: ".github/copilot-instructions.md",
			path: pathJoin(root, ".github", "copilot-instructions.md"),
		},
		{
			label: "~/.copilot/copilot-instructions.md",
			path: pathJoin(home, ".copilot", "copilot-instructions.md"),
		},
	];

	const lines = ["Instruction files\n"];
	for (const c of candidates) {
		const exists = existsSync(c.path);
		lines.push(
			`  ${exists ? "✓" : "○"} ${c.label}${exists ? "" : " (not found)"}`,
		);
	}
	lines.push(
		"\nAll existing files are loaded automatically. Delete or rename to disable.",
	);
	ctx.ui.notify(lines.join("\n"), "info");
}

export function formatTextStatus(state) {
	const lines = ["SF Status\n"];
	lines.push(formatProgressLine(computeProgressScore()));
	lines.push("");
	lines.push(`Phase: ${state.phase}`);
	if (state.activeMilestone) {
		lines.push(
			`Active milestone: ${state.activeMilestone.id} — ${state.activeMilestone.title}`,
		);
	}
	if (state.activeSlice) {
		lines.push(
			`Active slice: ${state.activeSlice.id} — ${state.activeSlice.title}`,
		);
	}
	if (state.activeTask) {
		lines.push(
			`Active task: ${state.activeTask.id} — ${state.activeTask.title}`,
		);
	}
	if (state.progress) {
		const { milestones, slices, tasks } = state.progress;
		const parts = [`milestones ${milestones.done}/${milestones.total}`];
		if (slices) parts.push(`slices ${slices.done}/${slices.total}`);
		if (tasks) parts.push(`tasks ${tasks.done}/${tasks.total}`);
		lines.push(`Progress: ${parts.join(", ")}`);
	}
	if (state.nextAction) {
		lines.push(`Next: ${state.nextAction}`);
	}
	if (state.blockers.length > 0) {
		lines.push(`Blockers: ${state.blockers.join("; ")}`);
	}
	try {
		const diagnostics = writeUokDiagnostics(projectRoot());
		lines.push(
			`UOK diagnostics: ${diagnostics.verdict} (${diagnostics.classification})`,
		);
		const firstIssue = diagnostics.issues?.[0];
		if (firstIssue) lines.push(`UOK issue: ${firstIssue.code}`);
	} catch {
		// Status text must stay available even when diagnostics cannot be written.
	}
	if (state.registry.length > 0) {
		lines.push("");
		lines.push("Milestones:");
		for (const milestone of state.registry) {
			const icon =
				milestone.status === "complete"
					? "✓"
					: milestone.status === "active"
						? "▶"
						: milestone.status === "parked"
							? "⏸"
							: "○";
			lines.push(
				`  ${icon} ${milestone.id}: ${milestone.title} (${milestone.status})`,
			);
		}
	}
	const envResults = runEnvironmentChecks(projectRoot());
	const envIssues = envResults.filter((result) => result.status !== "ok");
	if (envIssues.length > 0) {
		lines.push("");
		lines.push("Environment:");
		for (const issue of envIssues) {
			lines.push(`  ${issue.status === "error" ? "✗" : "⚠"} ${issue.message}`);
		}
	}
	return lines.join("\n");
}

// ─── /sidekicks ──────────────────────────────────────────────────────────────

async function handleSidekicksCommand(ctx) {
	try {
		const { readdirSync, readFileSync, existsSync } = await import("node:fs");
		const { join: pathJoin } = await import("node:path");
		const root = projectRoot();
		const parallelDir = pathJoin(root, ".sf", "parallel");
		if (!existsSync(parallelDir)) {
			ctx.ui.notify(
				"No background workers running. Start with /parallel start.",
				"info",
			);
			return;
		}
		const entries = readdirSync(parallelDir, { withFileTypes: true })
			.filter((d) => d.isDirectory())
			.map((d) => d.name);
		if (entries.length === 0) {
			ctx.ui.notify("No sidekick workers found.", "info");
			return;
		}
		const lines = [`Sidekicks (${entries.length})\n`];
		for (const name of entries) {
			const statusPath = pathJoin(parallelDir, name, "STATUS");
			const status = existsSync(statusPath)
				? readFileSync(statusPath, "utf-8").trim()
				: "unknown";
			const icon =
				status === "running"
					? "▶"
					: status === "done"
						? "✓"
						: status === "error"
							? "✗"
							: "○";
			lines.push(`  ${icon} ${name.padEnd(16)} ${status}`);
		}
		lines.push("\nUse /parallel status for details.");
		ctx.ui.notify(lines.join("\n"), "info");
	} catch (e) {
		ctx.ui.notify(`Sidekicks error: ${e.message}`, "error");
	}
}

// ─── /ask ────────────────────────────────────────────────────────────────────

async function handleAskCommand(question, ctx) {
	if (!question) {
		ctx.ui.notify(
			"Usage: /ask <question>\nAsk a side question without it becoming part of the main conversation history.\n\nTip: The response appears inline but does not affect the agent's working context.",
			"info",
		);
		return;
	}
	// Use ctx.fork if available (ephemeral branch), otherwise fall back to a
	// labelled notify so the UX is still useful.
	try {
		if (ctx.fork) {
			const ephemeral = await ctx.fork();
			await ephemeral.sendMessage?.(question);
			ctx.ui.notify("(/ask response in ephemeral branch above)", "info");
		} else {
			// Graceful fallback: route as a tagged message
			ctx.ui.notify(
				`/ask question: ${question}\n\n(Note: ctx.fork is unavailable in this build — /ask behaves like a regular message. The response will be in the main conversation.)`,
				"info",
			);
			await ctx.sendMessage?.(question);
		}
	} catch (e) {
		ctx.ui.notify(`/ask failed: ${e.message}`, "error");
	}
}

// ─── /resume ─────────────────────────────────────────────────────────────────

async function handleResumeCommand(args, ctx) {
	try {
		// Try the native session switcher if available
		if (ctx.switchSession) {
			if (args) {
				await ctx.switchSession(args);
				return;
			}
			// No arg — show picker
			const sessions = await ctx.listSessions?.();
			if (sessions?.length) {
				const lines = ["Available sessions — /resume <id> to switch\n"];
				for (const s of sessions.slice(0, 20)) {
					lines.push(
						`  ${s.id ?? s.name}  ${s.summary ?? s.description ?? ""}`.trim(),
					);
				}
				ctx.ui.notify(lines.join("\n"), "info");
			} else {
				ctx.ui.notify("No other sessions found.", "info");
			}
			return;
		}
		// Fallback: show recent SF headless session files
		const { readdirSync, existsSync } = await import("node:fs");
		const { join: pathJoin } = await import("node:path");
		const sfDir = sfHome();
		const sessDir = pathJoin(sfDir, "sessions");
		if (!existsSync(sessDir)) {
			ctx.ui.notify(
				"No session history found. Sessions are tracked in ~/.sf/sessions/",
				"info",
			);
			return;
		}
		const files = readdirSync(sessDir)
			.filter((f) => f.endsWith(".json") || f.endsWith(".jsonl"))
			.slice(-10)
			.reverse();
		if (!files.length) {
			ctx.ui.notify("No session files found in ~/.sf/sessions/", "info");
			return;
		}
		const lines = ["Recent sessions (read-only view)\n"];
		for (const f of files) {
			lines.push(`  ${f}`);
		}
		lines.push(
			"\nBACKGROUND_SESSIONS flag enables live switching (/experimental on background_sessions).",
		);
		ctx.ui.notify(lines.join("\n"), "info");
	} catch (e) {
		ctx.ui.notify(`Resume failed: ${e.message}`, "error");
	}
}
