/**
 * auto/phases-guards.js — runGuards phase.
 */
import { basename, dirname, join, parse as parsePath } from "node:path";
import { importExtensionModule } from "@singularity-forge/coding-agent";
import {
	clearCurrentPhase,
	setCurrentPhase,
} from "../../shared/sf-phase-state.js";
import { atomicWriteSync } from "../atomic-write.js";
import { resetCompletionNudgeState } from "../auto-completion-nudge.js";
import {
	isAwaitingUserInput,
	USER_DRIVEN_DEEP_UNITS,
} from "../auto-post-unit.js";
import {
	buildLoopRemediationSteps,
	diagnoseExpectedArtifact,
	verifyExpectedArtifact,
} from "../auto-recovery.js";
import {
	formatToolCallSummary,
	resetToolCallCounts,
} from "../auto-tool-tracking.js";
import {
	appendAutonomousSolverCheckpoint,
	assessAutonomousSolverTurn,
	beginAutonomousSolverIteration,
	buildAutonomousSolverMissingCheckpointRepairPrompt,
	buildAutonomousSolverPromptBlock,
	buildAutonomousSolverSteeringPromptBlock,
	classifyAutonomousSolverMissingCheckpointFailure,
	consumePendingAutonomousSolverSteering,
	getConfiguredAutonomousSolverMaxIterations,
	recordAutonomousSolverMissingCheckpointRetry,
} from "../autonomous-solver.js";
import { resumeAutoAfterProviderDelay } from "../bootstrap/provider-error-resume.js";
import { debugLog } from "../debug-logger.js";
import { PROJECT_FILES } from "../detection.js";
import { getErrorMessage } from "../error-utils.js";
import { MergeConflictError } from "../git-service.js";
import { recordLearnedOutcome } from "../learning/runtime.js";
import { sfRoot } from "../paths.js";
import { resolvePersistModelChanges } from "../preferences.js";
import {
	approveProductionMutationWithLlmPolicy,
	ensureProductionMutationApprovalTemplate,
	readProductionMutationApprovalStatus,
} from "../production-mutation-approval.js";
import { pauseAutoForProviderError } from "../provider-error-pause.js";
import {
	buildReasoningAssistPrompt,
	injectReasoningGuidance,
	isReasoningAssistEnabled,
} from "../reasoning-assist.js";
import {
	loadEvidenceFromDisk,
	resetEvidence,
} from "../safety/evidence-collector.js";
import { getDirtyFiles } from "../safety/file-change-validator.js";
import {
	cleanupCheckpoint,
	createCheckpoint,
	rollbackToCheckpoint,
} from "../safety/git-checkpoint.js";
import { resolveSafetyHarnessConfig } from "../safety/safety-harness.js";
import { recordSelfFeedback } from "../self-feedback.js";
import {
	checkpointWal,
	getMilestoneSlices,
	getSliceTaskCounts,
	getTask,
	isDbAvailable,
} from "../sf-db.js";
import { getEligibleSlices } from "../slice-parallel-eligibility.js";
import { startSliceParallel } from "../slice-parallel-orchestrator.js";
import { handleProductAudit } from "../tools/product-audit-tool.js";
import { parseUnitId } from "../unit-id.js";
import {
	collectSessionTokenUsage,
	collectWorktreeFingerprint,
	countChangedFiles,
	resetRunawayGuardState,
} from "../uok/auto-runaway-guard.js";
import {
	buildAutonomousUokContext,
	emitAutonomousGate,
} from "../uok/auto-uok-ctx.js";
import { resolveUokFlags } from "../uok/flags.js";
import { emitModelAutoResolvedEvent } from "../uok/model-route-evidence.js";
import {
	ensurePlanV2Graph as ensurePlanningFlowGraph,
	isEmptyPlanV2GraphResult,
	isMissingFinalizedContextResult,
} from "../uok/plan.js";
import { buildUokProgressEvent } from "../uok/progress-event.js";
import {
	clearUnitRuntimeRecord,
	writeUnitRuntimeRecord,
} from "../uok/unit-runtime.js";
import {
	_resetLogs,
	drainAndSummarize,
	drainLogs,
	formatForNotification,
	hasAnyIssues,
	logError,
	logWarning,
} from "../workflow-logger.js";
import {
	getRequiredWorkflowToolsForAutoUnit,
	getWorkflowTransportSupportError,
} from "../workflow-tools.js";
import { resolveWorktreeProjectRoot } from "../worktree-root.js";
import { detectStuck } from "./detect-stuck.js";
import {
	FINALIZE_POST_TIMEOUT_MS,
	FINALIZE_PRE_TIMEOUT_MS,
	withTimeout,
} from "./finalize-timeout.js";
import { runUnit } from "./run-unit.js";
import {
	BUDGET_THRESHOLDS,
	MAX_FINALIZE_TIMEOUTS,
	MAX_RECOVERY_CHARS,
} from "./types.js";

export function requiresHumanProductionMutationApproval(text) {
	const normalized = text.toLowerCase();
	const mentionsProduction =
		/\b(production|prod|live|hetzner)\b/.test(normalized) ||
		normalized.includes("centralcloud.com");
	if (!mentionsProduction) return false;
	const mentionsUnifiedFailover =
		normalized.includes("unified_failover") ||
		normalized.includes("unified-failover") ||
		normalized.includes("/action/unified");
	if (!mentionsUnifiedFailover) return false;
	return /\b(post|enqueue|create|insert|command row|pending command)\b/.test(
		normalized,
	);
}

// ─── runGuards ────────────────────────────────────────────────────────────────
/**
 * Phase 2: Guards — stop directives, budget ceiling, context window, secrets re-check.
 * Returns break to exit the loop, or next to proceed to dispatch.
 */
export async function runGuards(ic, mid, unitType, unitId, sliceId) {
	const { ctx, pi, s, deps, prefs } = ic;
	// ── Stop/Backtrack directive guard (#3487) ──
	// Check for unexecuted stop or backtrack captures BEFORE dispatching any unit.
	// This ensures user "halt" directives are honored immediately.
	// IMPORTANT: Fail-closed — any exception during stop handling still breaks the loop
	// to ensure user halt intent is never silently dropped.
	try {
		const { loadStopCaptures, markCaptureExecuted } = await import(
			"../captures.js"
		);
		const stopCaptures = loadStopCaptures(s.basePath);
		if (stopCaptures.length > 0) {
			const first = stopCaptures[0];
			const isBacktrack = first.classification === "backtrack";
			const label = isBacktrack
				? `Backtrack directive: ${first.text}`
				: `Stop directive: ${first.text}`;
			ctx.ui.notify(label, "warning");
			deps.sendDesktopNotification(
				"SF",
				label,
				"warning",
				"stop-directive",
				basename(s.originalBasePath || s.basePath),
			);
			// Pause first — ensures autonomous mode stops even if later steps fail
			await deps.pauseAuto(ctx, pi);
			// For backtrack captures, write the backtrack trigger after pausing
			if (isBacktrack) {
				try {
					const { executeBacktrack } = await import("../triage-resolution.js");
					executeBacktrack(s.basePath, mid, first);
				} catch (e) {
					debugLog("guards", {
						phase: "backtrack-execution-error",
						error: String(e),
					});
				}
			}
			// Mark captures as executed only after successful pause/transition
			for (const cap of stopCaptures) {
				markCaptureExecuted(s.basePath, cap.id);
			}
			debugLog("autoLoop", {
				phase: "exit",
				reason: isBacktrack ? "user-backtrack" : "user-stop",
			});
			return {
				action: "break",
				reason: isBacktrack ? "user-backtrack" : "user-stop",
			};
		}
	} catch (e) {
		// Fail-closed: if anything in the stop guard throws, break the loop
		// rather than silently continuing and dropping user halt intent
		debugLog("guards", { phase: "stop-guard-error", error: String(e) });
		return { action: "break", reason: "stop-guard-error" };
	}
	// Production mutation guard — headless autonomous must not enqueue live failover
	// commands without a human-provided safe target and cleanup plan.
	try {
		if (isDbAvailable()) {
			const state = await deps.deriveState(s.basePath);
			const activeTask = state.activeTask;
			const activeSlice = state.activeSlice;
			const activeMilestone = state.activeMilestone;
			if (activeMilestone?.id && activeSlice?.id && activeTask?.id) {
				const task = getTask(activeMilestone.id, activeSlice.id, activeTask.id);
				if (task) {
					const taskText = [
						task.title,
						task.description,
						task.verify,
						...task.inputs,
						...task.expected_output,
					].join("\n");
					if (requiresHumanProductionMutationApproval(taskText)) {
						const approvalUnit = {
							milestoneId: activeMilestone.id,
							sliceId: activeSlice.id,
							taskId: activeTask.id,
							taskTitle: task.title,
							taskText,
						};
						const approvalBasePath = s.originalBasePath || s.basePath;
						const approval = readProductionMutationApprovalStatus(
							approvalBasePath,
							approvalUnit,
						);
						if (approval.approved) {
							ctx.ui.notify(
								`Production mutation approval accepted for ${approvalUnit.milestoneId}/${approvalUnit.sliceId}/${approvalUnit.taskId}: ${approval.path}`,
								"warning",
							);
						} else {
							const llmApproval = approveProductionMutationWithLlmPolicy(
								approvalBasePath,
								approvalUnit,
							);
							if (llmApproval.approved) {
								ctx.ui.notify(
									`Production mutation LLM approval accepted for pending-command-only smoke test ${approvalUnit.milestoneId}/${approvalUnit.sliceId}/${approvalUnit.taskId}: ${llmApproval.path}`,
									"warning",
								);
							} else {
								const template = ensureProductionMutationApprovalTemplate(
									approvalBasePath,
									approvalUnit,
								);
								const blockerReasons = [
									...approval.reasons,
									...llmApproval.reasons.map((reason) => `LLM: ${reason}`),
								];
								const reasons = blockerReasons.length
									? ` Missing/invalid fields: ${blockerReasons.join("; ")}.`
									: "";
								const msg =
									`Production mutation guard: ${activeMilestone.id}/${activeSlice.id}/${activeTask.id} asks to POST unified failover against production. ` +
									`${template.created ? "Created" : "Reusing"} approval gate at ${template.path}. ` +
									`Fill it with an explicit safe server/VM target, cleanup/rollback path, and human or LLM approval, then rerun sf headless autonomous.${reasons}`;
								ctx.ui.notify(msg, "error");
								deps.sendDesktopNotification(
									"SF",
									"Production mutation guard paused autonomous mode",
									"warning",
									"safety",
									basename(s.originalBasePath || s.basePath),
								);
								await deps.pauseAuto(ctx, pi);
								return {
									action: "break",
									reason: "production-mutation-guard",
								};
							}
						}
					}
				}
			}
		}
	} catch (e) {
		debugLog("guards", {
			phase: "production-mutation-guard-error",
			error: String(e),
		});
	}
	// Budget ceiling guard
	const budgetCeiling = prefs?.budget_ceiling;
	if (budgetCeiling !== undefined && budgetCeiling > 0) {
		const currentLedger = deps.getLedger();
		// In parallel worker mode, only count cost from the current autonomous mode session
		// to avoid hitting the ceiling due to historical project-wide spend (#2184).
		let costUnits = currentLedger?.units;
		if (
			process.env.SF_PARALLEL_WORKER &&
			s.autoStartTime &&
			Array.isArray(costUnits)
		) {
			const sessionStartISO = new Date(s.autoStartTime).toISOString();
			costUnits = costUnits.filter(
				(u) => u.startedAt != null && u.startedAt >= sessionStartISO,
			);
		}
		const totalCost = costUnits ? deps.getProjectTotals(costUnits).cost : 0;
		const budgetPct = totalCost / budgetCeiling;
		const budgetAlertLevel = deps.getBudgetAlertLevel(budgetPct);
		const newBudgetAlertLevel = deps.getNewBudgetAlertLevel(
			s.lastBudgetAlertLevel,
			budgetPct,
		);
		const enforcement = prefs?.budget_enforcement ?? "pause";
		const budgetEnforcementAction = deps.getBudgetEnforcementAction(
			enforcement,
			budgetPct,
		);
		// Data-driven threshold check — loop descending, fire first match
		const threshold = BUDGET_THRESHOLDS.find(
			(t) => newBudgetAlertLevel >= t.pct,
		);
		if (threshold) {
			s.lastBudgetAlertLevel = newBudgetAlertLevel;
			if (threshold.pct === 100 && budgetEnforcementAction !== "none") {
				// 100% — special enforcement logic (halt/pause/warn)
				const msg = `Budget ceiling ${deps.formatCost(budgetCeiling)} reached (spent ${deps.formatCost(totalCost)}).`;
				if (budgetEnforcementAction === "halt") {
					deps.sendDesktopNotification(
						"SF",
						msg,
						"error",
						"budget",
						basename(s.originalBasePath || s.basePath),
					);
					await deps.stopAuto(ctx, pi, "Budget ceiling reached");
					debugLog("autoLoop", { phase: "exit", reason: "budget-halt" });
					return { action: "break", reason: "budget-halt" };
				}
				if (budgetEnforcementAction === "pause") {
					ctx.ui.notify(
						`${msg} Pausing autonomous mode — /autonomous to override and continue.`,
						"warning",
					);
					deps.sendDesktopNotification(
						"SF",
						msg,
						"warning",
						"budget",
						basename(s.originalBasePath || s.basePath),
					);
					deps.logCmuxEvent(prefs, msg, "warning");
					await deps.pauseAuto(ctx, pi);
					debugLog("autoLoop", { phase: "exit", reason: "budget-pause" });
					return { action: "break", reason: "budget-pause" };
				}
				ctx.ui.notify(`${msg} Continuing (enforcement: warn).`, "warning");
				deps.sendDesktopNotification(
					"SF",
					msg,
					"warning",
					"budget",
					basename(s.originalBasePath || s.basePath),
				);
				deps.logCmuxEvent(prefs, msg, "warning");
			} else if (threshold.pct < 100) {
				// Sub-100% — simple notification
				const msg = `${threshold.label}: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`;
				ctx.ui.notify(msg, threshold.notifyLevel);
				deps.sendDesktopNotification(
					"SF",
					msg,
					threshold.notifyLevel,
					"budget",
					basename(s.originalBasePath || s.basePath),
				);
				deps.logCmuxEvent(prefs, msg, threshold.cmuxLevel);
			}
		} else if (budgetAlertLevel === 0) {
			s.lastBudgetAlertLevel = 0;
		}
	} else {
		s.lastBudgetAlertLevel = 0;
	}
	// ── UOK Plan-gate ──────────────────────────────────────────────────────────
	// Structural validation before the first execute-task unit of a slice:
	// confirms the plan files exist and the slice has ≥1 task.
	// FailureClass "input" → 0 retries (broken plan needs human fix, not
	// an LLM retry). Only fires when uok.gates.enabled is true.
	const uokFlagsGuards = resolveUokFlags(prefs);
	if (uokFlagsGuards.gates && unitType === "execute-task" && mid && sliceId) {
		const taskCounts = getSliceTaskCounts(mid, sliceId);
		const isFirstTaskForSlice = taskCounts.done === 0;
		if (isFirstTaskForSlice) {
			let planGateOutcome = "pass";
			let planGateRationale = "";
			const milestoneSlices = getMilestoneSlices(mid);
			if (!milestoneSlices || milestoneSlices.length === 0) {
				planGateOutcome = "fail";
				planGateRationale = `Milestone ${mid} has no slices in DB (not yet planned)`;
			} else if (taskCounts.total < 1) {
				planGateOutcome = "fail";
				planGateRationale = `Slice ${sliceId} has no tasks defined`;
			}
			// Schema-v2 run-context: this gate runs inside the autonomous
			// loop's guard phase, so surface/runControl are "autonomous" and
			// permissionProfile follows session/prefs. parentTrace points at
			// the iteration's flow id so the gate is linkable back to the
			// loop run that triggered it.
			const v2Ctx = buildAutonomousUokContext({
				s,
				prefs,
				traceId: `guard:${ic.flowId}`,
				parentTrace: ic.flowId,
				unitType,
				unitId,
				milestoneId: mid,
				sliceId,
			});
			const planGateResult = await emitAutonomousGate({
				basePath: s.basePath,
				gateId: "plan-gate",
				gateType: "policy",
				outcome: planGateOutcome,
				failureClass: planGateOutcome === "pass" ? "none" : "input",
				rationale: planGateRationale || "Plan files verified",
				traceId: `guard:${ic.flowId}`,
				turnId: `iter-${ic.iteration}`,
				milestoneId: mid,
				sliceId,
				unitType,
				unitId,
				uokContext: v2Ctx,
			});
			if (planGateResult.outcome !== "pass") {
				ctx.ui.notify(
					`Plan gate failed: ${planGateResult.rationale ?? "invalid plan"}`,
					"warning",
				);
				await deps.pauseAuto(ctx, pi);
				return { action: "break", reason: "plan-gate-failed" };
			}
		}
	}
	// Context window guard
	const contextThreshold = prefs?.context_pause_threshold ?? 0;
	if (contextThreshold > 0 && s.cmdCtx) {
		const contextUsage = s.cmdCtx.getContextUsage();
		if (
			contextUsage &&
			contextUsage.percent !== null &&
			contextUsage.percent >= contextThreshold
		) {
			const msg = `Context window at ${contextUsage.percent}% (threshold: ${contextThreshold}%). Pausing to prevent truncated output.`;
			ctx.ui.notify(
				`${msg} Run /autonomous to continue (will start fresh session).`,
				"warning",
			);
			deps.sendDesktopNotification(
				"SF",
				`Context ${contextUsage.percent}% — paused`,
				"warning",
				"attention",
				basename(s.originalBasePath || s.basePath),
			);
			await deps.pauseAuto(ctx, pi);
			debugLog("autoLoop", { phase: "exit", reason: "context-window" });
			return { action: "break", reason: "context-window" };
		}
	}
	// Secrets re-check gate
	try {
		const manifestStatus = await deps.getManifestStatus(
			s.basePath,
			mid,
			s.originalBasePath,
		);
		if (manifestStatus && manifestStatus.pending.length > 0) {
			const result = await deps.collectSecretsFromManifest(
				s.basePath,
				mid,
				ctx,
			);
			if (
				result &&
				result.applied &&
				result.skipped &&
				result.existingSkipped
			) {
				ctx.ui.notify(
					`Secrets collected: ${result.applied.length} applied, ${result.skipped.length} skipped, ${result.existingSkipped.length} already set.`,
					"info",
				);
			} else {
				ctx.ui.notify("Secrets collection skipped.", "info");
			}
		}
	} catch (err) {
		ctx.ui.notify(
			`Secrets collection error: ${getErrorMessage(err)}. Continuing with next task.`,
			"warning",
		);
	}
	return { action: "next", data: undefined };
}
