/**
 * auto/phases-finalize.js — runFinalize 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 } from "../uok/auto-uok-ctx.js";
import { resolveUokFlags } from "../uok/flags.js";
import { UokGateRunner } from "../uok/gate-runner.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 {
	recordLearningOutcomeForUnit,
	shouldSkipArtifactVerification,
} from "./phases-helpers.js";
import { runUnit } from "./run-unit.js";
import {
	BUDGET_THRESHOLDS,
	MAX_FINALIZE_TIMEOUTS,
	MAX_RECOVERY_CHARS,
} from "./types.js";

// ─── runFinalize ──────────────────────────────────────────────────────────────
/**
 * Phase 5: Post-unit finalize — pre/post verification, UAT pause, step-wizard.
 * Returns break/continue/next to control the outer loop.
 */
export async function runFinalize(ic, iterData, loopState, sidecarItem) {
	const { ctx, pi, s, deps } = ic;
	const { pauseAfterUatDispatch } = iterData;
	debugLog("autoLoop", { phase: "finalize", iteration: ic.iteration });
	// Clear unit timeout (unit completed)
	deps.clearUnitTimeout();
	// Post-unit context for pre/post verification
	const postUnitCtx = {
		s,
		ctx,
		pi,
		buildSnapshotOpts: deps.buildSnapshotOpts,
		lockBase: deps.lockBase,
		stopAuto: deps.stopAuto,
		pauseAuto: deps.pauseAuto,
		updateProgressWidget: deps.updateProgressWidget,
	};
	// Pre-verification processing (commit, doctor, state rebuild, etc.)
	// Timeout guard: if postUnitPreVerification hangs (e.g., safety harness
	// deadlock, browser teardown hang, worktree sync stall), force-continue
	// after timeout so the auto-loop is not permanently frozen (#3757).
	//
	// On timeout, null out s.currentUnit so the timed-out task's late async
	// mutations are harmless — postUnitPreVerification guards all side effects
	// behind `if (s.currentUnit)`. The next iteration sets a fresh currentUnit.
	// Sidecar items use lightweight pre-verification opts
	const preVerificationOpts = sidecarItem
		? sidecarItem.kind === "hook"
			? {
					skipSettleDelay: true,
					skipWorktreeSync: true,
					agentEndMessages: s.lastUnitAgentEndMessages ?? undefined,
				}
			: {
					skipSettleDelay: true,
					agentEndMessages: s.lastUnitAgentEndMessages ?? undefined,
				}
		: { agentEndMessages: s.lastUnitAgentEndMessages ?? undefined };
	const _preUnitSnapshot = s.currentUnit
		? {
				type: s.currentUnit.type,
				id: s.currentUnit.id,
				startedAt: s.currentUnit.startedAt,
			}
		: null;
	const preResultGuard = await withTimeout(
		deps.postUnitPreVerification(postUnitCtx, preVerificationOpts),
		FINALIZE_PRE_TIMEOUT_MS,
		"postUnitPreVerification",
	);
	if (preResultGuard.timedOut) {
		// Detach session from the timed-out unit so late async completions
		// cannot mutate state for the next unit (#3757).
		const hadStagedPending = s.stagedPendingCommit;
		const hadCommitted = s.lastGitActionStatus === "ok";
		s.stagedPendingCommit = false; // prevent orphaned deferred commit
		s.currentUnit = null;
		clearCurrentPhase();
		// Drop any logger entries from the timed-out unit so they don't bleed
		// into the next iteration's drain.
		drainLogs();
		loopState.consecutiveFinalizeTimeouts++;
		if (hadStagedPending) {
			ctx.ui.notify(
				"postUnitPreVerification timed out with staged-but-uncommitted changes — staged files will be included in next unit's commit.",
				"warning",
			);
			logWarning(
				"engine",
				"finalize-timeout: staged-pending-commit orphaned — will be absorbed by next unit",
			);
		} else if (hadCommitted) {
			ctx.ui.notify(
				"postUnitPreVerification timed out after git commit — changes are in history but verification was skipped.",
				"warning",
			);
			logWarning(
				"engine",
				"finalize-timeout: git commit completed before timeout — verification was not run",
			);
		}
		debugLog("autoLoop", {
			phase: "pre-verification-timeout",
			iteration: ic.iteration,
			unitType: iterData.unitType,
			unitId: iterData.unitId,
			consecutiveTimeouts: loopState.consecutiveFinalizeTimeouts,
		});
		if (loopState.consecutiveFinalizeTimeouts >= MAX_FINALIZE_TIMEOUTS) {
			ctx.ui.notify(
				`postUnitPreVerification timed out ${loopState.consecutiveFinalizeTimeouts} consecutive times — stopping autonomous mode to prevent budget waste`,
				"error",
			);
			await deps.stopAuto(
				ctx,
				pi,
				`${loopState.consecutiveFinalizeTimeouts} consecutive finalize timeouts`,
			);
			return { action: "break", reason: "finalize-timeout-escalation" };
		}
		ctx.ui.notify(
			`postUnitPreVerification timed out after ${FINALIZE_PRE_TIMEOUT_MS / 1000}s for ${iterData.unitType} ${iterData.unitId} (${loopState.consecutiveFinalizeTimeouts}/${MAX_FINALIZE_TIMEOUTS}) — continuing to next iteration`,
			"warning",
		);
		return { action: "next", data: undefined };
	}
	const preResult = preResultGuard.value;
	if (preResult === "dispatched") {
		const dispatchedReason = s.lastGitActionFailure
			? "git-closeout-failure"
			: "pre-verification-dispatched";
		debugLog("autoLoop", {
			phase: "exit",
			reason: dispatchedReason,
			gitError: s.lastGitActionFailure ?? undefined,
		});
		return { action: "break", reason: dispatchedReason };
	}
	if (preResult === "retry") {
		if (sidecarItem) {
			// Sidecar artifact retries are skipped — just continue
			debugLog("autoLoop", {
				phase: "sidecar-artifact-retry-skipped",
				iteration: ic.iteration,
			});
		} else {
			// s.pendingVerificationRetry was set by postUnitPreVerification.
			// Emit a dedicated journal event so forensics can distinguish bounded
			// verification retries from genuine stuck-loop dispatch repetitions (#4540).
			const retryInfo = s.pendingVerificationRetry;
			deps.emitJournalEvent({
				ts: new Date().toISOString(),
				flowId: ic.flowId,
				seq: ic.nextSeq(),
				eventType: "artifact-verification-retry",
				data: {
					unitType: _preUnitSnapshot?.type,
					unitId: retryInfo?.unitId,
					attempt: retryInfo?.attempt,
				},
			});
			// Continue the loop — next iteration will inject the retry context into the prompt.
			debugLog("autoLoop", {
				phase: "artifact-verification-retry",
				iteration: ic.iteration,
			});
			return { action: "continue" };
		}
	}
	if (pauseAfterUatDispatch) {
		ctx.ui.notify(
			"UAT requires human execution. Autonomous mode will pause after this unit writes the result file.",
			"info",
		);
		await deps.pauseAuto(ctx, pi);
		debugLog("autoLoop", { phase: "exit", reason: "uat-pause" });
		return { action: "break", reason: "uat-pause" };
	}
	// Verification gate
	// Hook sidecar items skip verification entirely.
	// Non-hook sidecar items run verification but skip retries (just continue).
	const skipVerification = sidecarItem?.kind === "hook";
	const uokFlagsFinalize = resolveUokFlags(ic.prefs);
	const runVerifyGate =
		uokFlagsFinalize.gates &&
		iterData.unitType === "execute-task" &&
		!skipVerification;
	if (!skipVerification) {
		if (runVerifyGate) {
			const vgRunner = new UokGateRunner();
			vgRunner.register({
				id: "unit-verification-gate",
				type: "verification",
				execute: async () => {
					const result = await deps.runPostUnitVerification(
						{ s, ctx, pi },
						deps.pauseAuto,
					);
					if (result === "pause") {
						return {
							outcome: "fail",
							failureClass: "manual-attention",
							rationale:
								"Post-unit verification paused — requires human attention",
						};
					}
					if (result === "retry") {
						return {
							outcome: "fail",
							failureClass: "verification",
							rationale: "Post-unit verification failed — retrying unit",
						};
					}
					return {
						outcome: "pass",
						failureClass: "none",
						rationale: "Post-unit verification passed",
					};
				},
			});
			// Schema-v2 run-context: finalize runs after unit dispatch
			// in the autonomous loop, so surface/runControl are
			// "autonomous"; permissionProfile follows session/prefs.
			// sliceId/taskId are pulled from the unit id when the unit
			// is execute-task (M*/S*/T*); other unit types just leave
			// them undefined.
			const parsed = parseUnitId(iterData.unitId);
			const v2Ctx = buildAutonomousUokContext({
				s,
				prefs: ic.prefs,
				traceId: `finalize:${ic.flowId}`,
				parentTrace: ic.flowId,
				unitType: iterData.unitType,
				unitId: iterData.unitId,
				milestoneId: iterData.mid ?? parsed.milestone,
				sliceId: parsed.slice,
				taskId: parsed.task,
			});
			const gateResult = await vgRunner.run("unit-verification-gate", {
				basePath: s.basePath,
				traceId: `finalize:${ic.flowId}`,
				turnId: `iter-${ic.iteration}`,
				milestoneId: iterData.mid ?? undefined,
				unitType: iterData.unitType,
				unitId: iterData.unitId,
				surface: v2Ctx?.surface,
				runControl: v2Ctx?.runControl,
				permissionProfile: v2Ctx?.permissionProfile,
				parentTrace: v2Ctx?.parentTrace,
			});
			if (gateResult.outcome !== "pass") {
				recordLearningOutcomeForUnit(
					ic,
					iterData.unitType,
					iterData.unitId,
					s.currentUnit?.startedAt,
					{
						succeeded: false,
						verificationPassed: false,
					},
				);
				const reason =
					gateResult.failureClass === "manual-attention"
						? "verification-pause"
						: "verification-fail";
				debugLog("autoLoop", { phase: "exit", reason });
				return { action: "break", reason };
			}
		} else {
			const verificationResult = await deps.runPostUnitVerification(
				{ s, ctx, pi },
				deps.pauseAuto,
			);
			if (verificationResult === "pause") {
				recordLearningOutcomeForUnit(
					ic,
					iterData.unitType,
					iterData.unitId,
					s.currentUnit?.startedAt,
					{
						succeeded: false,
						verificationPassed: false,
					},
				);
				debugLog("autoLoop", {
					phase: "exit",
					reason: "verification-pause",
				});
				return { action: "break", reason: "verification-pause" };
			}
			if (verificationResult === "retry") {
				recordLearningOutcomeForUnit(
					ic,
					iterData.unitType,
					iterData.unitId,
					s.currentUnit?.startedAt,
					{
						succeeded: false,
						verificationPassed: false,
					},
				);
				if (sidecarItem) {
					// Sidecar verification retries are skipped — just continue
					debugLog("autoLoop", {
						phase: "sidecar-verification-retry-skipped",
						iteration: ic.iteration,
					});
				} else {
					// s.pendingVerificationRetry was set by runPostUnitVerification.
					// Continue the loop — next iteration will inject the retry context into the prompt.
					debugLog("autoLoop", {
						phase: "verification-retry",
						iteration: ic.iteration,
					});
					return { action: "continue" };
				}
			}
			if (verificationResult === "continue") {
				// Verification was auto-deferred (low-risk findings) — proceed normally.
				// The defer logic in runPostUnitVerification already notified the user
				// and wrote deferred evidence; we just need to avoid the break/retry
				// paths above and fall through to post-verification processing.
				debugLog("autoLoop", {
					phase: "verification-deferred",
					iteration: ic.iteration,
				});
			}
		}
	}
	// Post-verification processing (DB dual-write, hooks, triage, quick-tasks)
	// Timeout guard: if postUnitPostVerification hangs (e.g., module import
	// deadlock, SQLite transaction hang), force-continue after timeout so the
	// auto-loop is not permanently frozen (#2344).
	const postResultGuard = await withTimeout(
		deps.postUnitPostVerification(postUnitCtx),
		FINALIZE_POST_TIMEOUT_MS,
		"postUnitPostVerification",
	);
	if (postResultGuard.timedOut) {
		// Detach session from the timed-out unit so late async completions
		// cannot mutate state for the next unit (#3757).
		s.currentUnit = null;
		clearCurrentPhase();
		// Drop any logger entries from the timed-out unit so they don't bleed
		// into the next iteration's drain.
		drainLogs();
		loopState.consecutiveFinalizeTimeouts++;
		debugLog("autoLoop", {
			phase: "post-verification-timeout",
			iteration: ic.iteration,
			unitType: iterData.unitType,
			unitId: iterData.unitId,
			consecutiveTimeouts: loopState.consecutiveFinalizeTimeouts,
		});
		if (loopState.consecutiveFinalizeTimeouts >= MAX_FINALIZE_TIMEOUTS) {
			ctx.ui.notify(
				`postUnitPostVerification timed out ${loopState.consecutiveFinalizeTimeouts} consecutive times — stopping autonomous mode to prevent budget waste`,
				"error",
			);
			await deps.stopAuto(
				ctx,
				pi,
				`${loopState.consecutiveFinalizeTimeouts} consecutive finalize timeouts`,
			);
			return { action: "break", reason: "finalize-timeout-escalation" };
		}
		ctx.ui.notify(
			`postUnitPostVerification timed out after ${FINALIZE_POST_TIMEOUT_MS / 1000}s for ${iterData.unitType} ${iterData.unitId} (${loopState.consecutiveFinalizeTimeouts}/${MAX_FINALIZE_TIMEOUTS}) — continuing to next iteration`,
			"warning",
		);
		return { action: "next", data: undefined };
	}
	const postResult = postResultGuard.value;
	if (postResult === "stopped") {
		debugLog("autoLoop", {
			phase: "exit",
			reason: "post-verification-stopped",
		});
		return { action: "break", reason: "post-verification-stopped" };
	}
	if (postResult === "step-wizard") {
		// Assisted mode — exit the loop (caller handles wizard)
		debugLog("autoLoop", { phase: "exit", reason: "step-wizard" });
		return { action: "break", reason: "step-wizard" };
	}
	// Both pre and post verification completed without timeout — reset counter
	loopState.consecutiveFinalizeTimeouts = 0;
	// Flush WAL to main DB file now that all unit DB writes are committed.
	// wal_autocheckpoint=0 prevents SQLite from auto-checkpointing at random
	// times — this explicit call at the end of a successful unit is the only
	// point where the WAL is flushed, making crash recovery deterministic.
	checkpointWal();
	// Surface accumulated workflow-logger issues for this unit to the user.
	// Warnings/errors logged during the unit are buffered in the logger and
	// drained here so the user sees a single consolidated post-unit alert.
	const finalizedArtifactVerified =
		shouldSkipArtifactVerification(iterData.unitType) ||
		verifyExpectedArtifact(iterData.unitType, iterData.unitId, s.basePath);
	if (finalizedArtifactVerified) {
		recordLearningOutcomeForUnit(
			ic,
			iterData.unitType,
			iterData.unitId,
			s.currentUnit?.startedAt,
			{
				succeeded: true,
				verificationPassed: iterData.unitType === "execute-task" ? true : null,
			},
		);
		// Clear the runtime unit record so it does not linger as a phantom
		// "dispatched" unit across session restarts (#sf-moqv2k4g-kbg2nq).
		clearUnitRuntimeRecord(s.basePath, iterData.unitType, iterData.unitId);
		// Evict this unit from stuck-state recentUnits so a completed unit
		// does not pollute the sliding window on restart.
		const unitKey = `${iterData.unitType}/${iterData.unitId}`;
		const prevLen = loopState.recentUnits.length;
		loopState.recentUnits = loopState.recentUnits.filter(
			(u) => u.key !== unitKey,
		);
		if (
			loopState.recentUnits.length < prevLen &&
			loopState.stuckRecoveryAttempts > 0
		) {
			loopState.stuckRecoveryAttempts = 0;
		}
	}
	if (hasAnyIssues()) {
		const { logs } = drainAndSummarize();
		if (logs.length > 0) {
			const severity = logs.some((e) => e.severity === "error")
				? "error"
				: "warning";
			ctx.ui.notify(formatForNotification(logs), severity, {
				kind: severity === "error" ? "notice" : "progress",
				source: "workflow-logger",
				dedupe_key: `workflow-issues:${iterData.unitType}:${iterData.unitId}`,
			});
		}
	}
	// PhaseReview 3-pass (gated on uok.phase_review.enabled)
	const uokFlagsForReview = resolveUokFlags(ic.prefs);
	if (uokFlagsForReview.phaseReview) {
		await runPhaseReview(ic, iterData);
	}
	return { action: "next", data: undefined };
}

/**
 * PhaseReview 3-pass: optional post-unit review pipeline.
 *
 * Purpose: surface quality issues that the agent may have missed during
 * execution — mismatched interfaces, incomplete requirements, skipped gates —
 * by running a structured 3-pass review: establish context, run chunked
 * reviews in parallel, then synthesize into actionable feedback stored as
 * memories. Gated on `uok.phase_review.enabled: true` in preferences.
 *
 * Passes:
 *   1. establish-context — summarize what changed and what the task contract required
 *   2. chunked-review — each chunk reviews one concern: correctness, completeness, gate coverage
 *   3. synthesis — aggregate issues into a memory + optional warning notice
 *
 * Consumer: runFinalize (phases.js) after post-unit verification passes.
 */
