/**
 * WorktreeResolver — encapsulates worktree path state and merge/exit lifecycle.
 *
 * Replaces scattered `s.basePath`/`s.originalBasePath` mutation and 3 duplicated
 * merge-or-teardown blocks in auto-loop.ts with single method calls. All
 * `s.basePath` mutations (except session.reset() and initial setup) happen
 * through this class.
 *
 * Design: Option A — mutates AutoSession fields directly so existing `s.basePath`
 * reads continue to work everywhere without wiring changes.
 *
 * Key invariant: `createAutoWorktree()` and `enterAutoWorktree()` call
 * `process.chdir()` internally — this class MUST NOT double-chdir.
 */
import { randomUUID } from "node:crypto";
import { existsSync, unlinkSync } from "node:fs";
import { resolve } from "node:path";
import { debugLog } from "./debug-logger.js";
import { getErrorMessage } from "./error-utils.js";
import { MergeConflictError } from "./git-service.js";
import { emitJournalEvent } from "./journal.js";
import { loadEffectiveSFPreferences } from "./preferences.js";
import {
	getCollapseCadence,
	getMilestoneResquash,
	resquashMilestoneOnMain,
} from "./slice-cadence.js";
import { resolveGitDir } from "./worktree-manager.js";
import {
	emitWorktreeCreated,
	emitWorktreeMerged,
} from "./worktree-telemetry.js";
// ─── WorktreeResolver ──────────────────────────────────────────────────────
/**
 * Manages worktree lifecycle for autonomous mode: enter/exit milestone, merge/transition.
 * Encapsulates path state, handles git isolation modes, and emits telemetry (ADR-019).
 */
export class WorktreeResolver {
	s;
	deps;
	constructor(session, deps) {
		this.s = session;
		this.deps = deps;
	}
	// ── Getters ────────────────────────────────────────────────────────────
	/** Current working path — may be worktree or project root. */
	get workPath() {
		return this.s.basePath;
	}
	/** Original project root — always the non-worktree path. */
	get projectRoot() {
		return this.s.originalBasePath || this.s.basePath;
	}
	/** Path for auto.lock file — same as the old lockBase(). */
	get lockPath() {
		return this.s.originalBasePath || this.s.basePath;
	}
	// ── Private Helpers ────────────────────────────────────────────────────
	rebuildGitService() {
		const gitConfig =
			this.deps.loadEffectiveSFPreferences()?.preferences?.git ?? {};
		this.s.gitService = new this.deps.GitService(this.s.basePath, gitConfig);
	}
	/** Restore basePath to originalBasePath and rebuild GitService. */
	restoreToProjectRoot() {
		if (!this.s.originalBasePath) return;
		this.s.basePath = this.s.originalBasePath;
		this.rebuildGitService();
		this.deps.invalidateAllCaches();
	}
	// ── Validation ──────────────────────────────────────────────────────────
	/** Validate milestoneId to prevent path traversal. */
	validateMilestoneId(milestoneId) {
		if (/[/\\]|\.\./.test(milestoneId)) {
			throw new Error(
				`Invalid milestoneId: ${milestoneId} — contains path separators or traversal`,
			);
		}
	}
	// ── Enter Milestone ────────────────────────────────────────────────────
	/**
	 * Enter or create a worktree for the given milestone.
	 *
	 * Only acts if `shouldUseWorktreeIsolation()` returns true.
	 * Delegates to `enterAutoWorktree` (existing) or `createAutoWorktree` (new).
	 * Those functions call `process.chdir()` internally — we do NOT double-chdir.
	 *
	 * Updates `s.basePath` and rebuilds GitService on success.
	 * On failure: notifies a warning and does NOT update `s.basePath`.
	 */
	enterMilestone(milestoneId, ctx) {
		this.validateMilestoneId(milestoneId);
		// Capture projectRoot at function entry — before any basePath mutation — so all
		// emit calls in this function use a stable value regardless of early-return paths.
		const projectRoot = this.s.originalBasePath ?? this.s.basePath;
		// If worktree creation failed earlier this session, skip all future attempts
		if (this.s.isolationDegraded) {
			debugLog("WorktreeResolver", {
				action: "enterMilestone",
				milestoneId,
				skipped: true,
				reason: "isolation-degraded",
			});
			return;
		}
		if (!this.deps.shouldUseWorktreeIsolation()) {
			debugLog("WorktreeResolver", {
				action: "enterMilestone",
				milestoneId,
				skipped: true,
				reason: "isolation-disabled",
			});
			emitJournalEvent(projectRoot, {
				ts: new Date().toISOString(),
				flowId: randomUUID(),
				seq: 0,
				eventType: "worktree-skip",
				data: { milestoneId, reason: "isolation-disabled" },
			});
			return;
		}
		const basePath = this.s.originalBasePath || this.s.basePath;
		debugLog("WorktreeResolver", {
			action: "enterMilestone",
			milestoneId,
			basePath,
		});
		try {
			const existingPath = this.deps.getAutoWorktreePath(basePath, milestoneId);
			let wtPath;
			if (existingPath) {
				wtPath = this.deps.enterAutoWorktree(basePath, milestoneId);
			} else {
				wtPath = this.deps.createAutoWorktree(basePath, milestoneId);
			}
			this.s.basePath = wtPath;
			this.rebuildGitService();
			debugLog("WorktreeResolver", {
				action: "enterMilestone",
				milestoneId,
				result: "success",
				wtPath,
			});
			emitJournalEvent(projectRoot, {
				ts: new Date().toISOString(),
				flowId: randomUUID(),
				seq: 0,
				eventType: "worktree-enter",
				data: { milestoneId, wtPath, created: !existingPath },
			});
			ctx.notify(`Entered worktree for ${milestoneId} at ${wtPath}`, "info");
			// #4764 — record creation/enter as a lifecycle event so the telemetry
			// aggregator can pair it with the eventual worktree-merged event.
			try {
				emitWorktreeCreated(projectRoot, milestoneId, {
					reason: existingPath ? "enter-milestone" : "create-milestone",
				});
			} catch (telemetryErr) {
				debugLog("WorktreeResolver", {
					action: "enterMilestone",
					phase: "telemetry-emit",
					error:
						telemetryErr instanceof Error
							? telemetryErr.message
							: String(telemetryErr),
				});
			}
		} catch (err) {
			const msg = getErrorMessage(err);
			debugLog("WorktreeResolver", {
				action: "enterMilestone",
				milestoneId,
				result: "error",
				error: msg,
			});
			emitJournalEvent(projectRoot, {
				ts: new Date().toISOString(),
				flowId: randomUUID(),
				seq: 0,
				eventType: "worktree-create-failed",
				data: { milestoneId, error: msg, fallback: "project-root" },
			});
			ctx.notify(
				`Auto-worktree creation for ${milestoneId} failed: ${msg}. Continuing in project root.`,
				"warning",
			);
			// Degrade isolation for the rest of this session so mergeAndExit
			// doesn't try to merge a nonexistent worktree branch (#2483)
			this.s.isolationDegraded = true;
			// Do NOT update s.basePath — stay in project root
		}
	}
	// ── Exit Milestone ─────────────────────────────────────────────────────
	/**
	 * Exit the current worktree: auto-commit, teardown, reset basePath.
	 *
	 * Only acts if currently in an auto-worktree (checked via `isInAutoWorktree`).
	 * Resets `s.basePath` to `s.originalBasePath` and rebuilds GitService.
	 */
	exitMilestone(milestoneId, ctx, opts) {
		this.validateMilestoneId(milestoneId);
		if (!this.deps.isInAutoWorktree(this.s.basePath)) {
			debugLog("WorktreeResolver", {
				action: "exitMilestone",
				milestoneId,
				skipped: true,
				reason: "not-in-worktree",
			});
			return;
		}
		debugLog("WorktreeResolver", {
			action: "exitMilestone",
			milestoneId,
			basePath: this.s.basePath,
		});
		try {
			this.deps.autoCommitCurrentBranch(this.s.basePath, "stop", milestoneId);
		} catch (err) {
			debugLog("WorktreeResolver", {
				action: "exitMilestone",
				milestoneId,
				phase: "auto-commit-failed",
				error: getErrorMessage(err),
			});
		}
		try {
			this.deps.teardownAutoWorktree(this.s.originalBasePath, milestoneId, {
				preserveBranch: opts?.preserveBranch ?? false,
			});
		} catch (err) {
			debugLog("WorktreeResolver", {
				action: "exitMilestone",
				milestoneId,
				phase: "teardown-failed",
				error: getErrorMessage(err),
			});
		}
		this.restoreToProjectRoot();
		debugLog("WorktreeResolver", {
			action: "exitMilestone",
			milestoneId,
			result: "done",
			basePath: this.s.basePath,
		});
		ctx.notify(`Exited worktree for ${milestoneId}`, "info");
	}
	// ── Merge and Exit ─────────────────────────────────────────────────────
	/**
	 * Merge the completed milestone branch back to main and exit the worktree.
	 *
	 * Handles all three isolation modes:
	 * - **worktree**: Read roadmap, merge, teardown worktree, reset paths.
	 *   Falls back to bare teardown if no roadmap exists.
	 * - **branch**: Check if on milestone branch, merge if so (no chdir/teardown).
	 * - **none**: No-op.
	 *
	 * Error recovery: on merge failure, always restore `s.basePath` to
	 * `s.originalBasePath` and `process.chdir(s.originalBasePath)`.
	 */
	mergeAndExit(milestoneId, ctx) {
		this.validateMilestoneId(milestoneId);
		// Capture projectRoot before any basePath mutation so all emit calls in this
		// function use a stable value. restoreToProjectRoot() resets basePath, so
		// reading this.s.originalBasePath || this.s.basePath post-mutation is unreliable.
		const projectRoot = this.s.originalBasePath ?? this.s.basePath;
		// If worktree creation failed earlier, skip merge — work is on current branch (#2483)
		if (this.s.isolationDegraded) {
			debugLog("WorktreeResolver", {
				action: "mergeAndExit",
				milestoneId,
				skipped: true,
				reason: "isolation-degraded",
			});
			ctx.notify(
				`Skipping worktree merge for ${milestoneId} — isolation was degraded (worktree creation failed earlier). Work is on the current branch.`,
				"info",
			);
			return;
		}
		const mode = this.deps.getIsolationMode();
		debugLog("WorktreeResolver", {
			action: "mergeAndExit",
			milestoneId,
			mode,
			basePath: this.s.basePath,
		});
		emitJournalEvent(projectRoot, {
			ts: new Date().toISOString(),
			flowId: randomUUID(),
			seq: 0,
			eventType: "worktree-merge-start",
			data: { milestoneId, mode },
		});
		// #2625: If we are physically inside an auto-worktree, we MUST merge
		// regardless of the current isolation config. This prevents data loss when
		// the default isolation mode changes between versions (e.g., "worktree" ->
		// "none"): the worktree branch still holds real commits that need merging.
		const inWorktree =
			this.deps.isInAutoWorktree(this.s.basePath) && this.s.originalBasePath;
		if (mode === "none" && !inWorktree) {
			debugLog("WorktreeResolver", {
				action: "mergeAndExit",
				milestoneId,
				skipped: true,
				reason: "mode-none",
			});
			return;
		}
		// #4764 — telemetry: record start timestamp so we can emit merge duration.
		const mergeStartedAt = new Date().toISOString();
		const mergeStartMs = Date.now();
		let actuallyMerged = false;
		if (mode === "worktree" || inWorktree) {
			actuallyMerged = this._mergeWorktreeMode(milestoneId, ctx);
		} else if (mode === "branch") {
			actuallyMerged = this._mergeBranchMode(milestoneId, ctx);
		}
		// The remainder of this function emits telemetry and runs re-squash.
		// Both are gated on actuallyMerged — if the _merge* helper took a
		// no-merge path (missing originalBase, no roadmap, wrong branch) the
		// milestone branch was intentionally left unmerged and we must not
		// emit a worktree-merged event or collapse commits on main.
		if (!actuallyMerged) {
			// Always clear the start-SHA tracker to avoid leaking across sessions.
			this.s.milestoneStartShas.delete(milestoneId);
			return;
		}
		// #4765 — when collapse_cadence=slice AND milestone_resquash=true, the
		// N per-slice commits on main should be collapsed into one milestone
		// commit. Done AFTER the primary merge-and-teardown so the branch and
		// worktree are already cleaned up; we operate on main directly.
		try {
			const startSha = this.s.milestoneStartShas.get(milestoneId);
			if (startSha) {
				const prefs = loadEffectiveSFPreferences()?.preferences;
				if (
					getCollapseCadence(prefs) === "slice" &&
					getMilestoneResquash(prefs)
				) {
					const result = resquashMilestoneOnMain(
						projectRoot,
						milestoneId,
						startSha,
					);
					if (result.resquashed) {
						ctx.notify(
							`slice-cadence: re-squashed slice commits for ${milestoneId} into a single milestone commit.`,
							"info",
						);
					}
				}
				this.s.milestoneStartShas.delete(milestoneId);
			}
		} catch (err) {
			debugLog("WorktreeResolver", {
				action: "mergeAndExit",
				milestoneId,
				phase: "resquash",
				error: getErrorMessage(err),
			});
		}
		// #4764 — record merge completion. Only reaches here when an actual
		// merge ran; failure paths throw out of _merge* before this point and
		// no-merge paths returned above.
		try {
			emitWorktreeMerged(projectRoot, milestoneId, {
				reason: "milestone-complete",
				startedAt: mergeStartedAt,
				durationMs: Date.now() - mergeStartMs,
			});
		} catch (telemetryErr) {
			debugLog("WorktreeResolver", {
				action: "mergeAndExit",
				phase: "telemetry-emit",
				error:
					telemetryErr instanceof Error
						? telemetryErr.message
						: String(telemetryErr),
			});
		}
	}
	/** Worktree-mode merge: read roadmap, merge, teardown, reset paths.
	 *  Returns true when a squash-merge actually ran (false on skip paths). */
	_mergeWorktreeMode(milestoneId, ctx) {
		const originalBase = this.s.originalBasePath;
		if (!originalBase) {
			debugLog("WorktreeResolver", {
				action: "mergeAndExit",
				milestoneId,
				mode: "worktree",
				skipped: true,
				reason: "missing-original-base",
			});
			return false;
		}
		let merged = false;
		try {
			const { synced } = this.deps.syncWorktreeStateBack(
				originalBase,
				this.s.basePath,
				milestoneId,
			);
			if (synced.length > 0) {
				debugLog("WorktreeResolver", {
					action: "mergeAndExit",
					milestoneId,
					phase: "reverse-sync",
					synced: synced.length,
				});
			}
			// Resolve roadmap — try project root first, then worktree path as fallback.
			// The worktree may hold the only copy when syncWorktreeStateBack fails
			// silently or .sf/ is not symlinked. Without the fallback, a missing
			// roadmap triggers bare teardown which deletes the branch and orphans all
			// milestone commits (#1573).
			let roadmapPath = this.deps.resolveMilestoneFile(
				originalBase,
				milestoneId,
				"ROADMAP",
			);
			if (!roadmapPath && this.s.basePath !== originalBase) {
				roadmapPath = this.deps.resolveMilestoneFile(
					this.s.basePath,
					milestoneId,
					"ROADMAP",
				);
				if (roadmapPath) {
					debugLog("WorktreeResolver", {
						action: "mergeAndExit",
						milestoneId,
						phase: "roadmap-fallback",
						note: "resolved from worktree path",
					});
				}
			}
			if (roadmapPath) {
				const roadmapContent = this.deps.readFileSync(roadmapPath, "utf-8");
				const mergeResult = this.deps.mergeMilestoneToMain(
					originalBase,
					milestoneId,
					roadmapContent,
				);
				merged = true;
				// #2945 Bug 3: mergeMilestoneToMain performs best-effort worktree
				// cleanup internally (step 12), but it can silently fail on Windows
				// or when the worktree directory is locked. Perform a secondary
				// teardown here to ensure the worktree is properly cleaned up.
				// This is idempotent — if the worktree was already removed,
				// teardownAutoWorktree handles the no-op case gracefully.
				try {
					this.deps.teardownAutoWorktree(originalBase, milestoneId);
				} catch {
					// Best-effort — the primary cleanup in mergeMilestoneToMain may
					// have already removed the worktree.
				}
				if (mergeResult.codeFilesChanged) {
					ctx.notify(
						`Milestone ${milestoneId} merged to main.${mergeResult.pushed ? " Pushed to remote." : ""}`,
						"info",
					);
				} else {
					// (#1906) Milestone produced only .sf/ metadata — no actual code was
					// merged. This typically means the LLM wrote planning artifacts
					// (summaries, roadmaps) but never implemented the code. Surface this
					// clearly so the user knows the milestone is not truly complete.
					ctx.notify(
						`WARNING: Milestone ${milestoneId} merged to main but contained NO code changes — only .sf/ metadata files. ` +
							`The milestone summary may describe planned work that was never implemented. ` +
							`Review the milestone output and re-run if code is missing.`,
						"warning",
					);
				}
			} else {
				// No roadmap at either location — teardown but PRESERVE the branch so
				// commits are not orphaned. The user can merge manually later (#1573).
				this.deps.teardownAutoWorktree(originalBase, milestoneId, {
					preserveBranch: true,
				});
				ctx.notify(
					`Exited worktree for ${milestoneId} (no roadmap found — branch preserved for manual merge).`,
					"warning",
				);
			}
		} catch (err) {
			const msg = getErrorMessage(err);
			debugLog("WorktreeResolver", {
				action: "mergeAndExit",
				milestoneId,
				result: "error",
				error: msg,
				fallback: "chdir-to-project-root",
			});
			// originalBase is guaranteed non-null at this point (checked at function entry).
			emitJournalEvent(originalBase, {
				ts: new Date().toISOString(),
				flowId: randomUUID(),
				seq: 0,
				eventType: "worktree-merge-failed",
				data: { milestoneId, error: msg },
			});
			// Surface a clear, actionable error. The worktree and milestone branch are
			// intentionally preserved — nothing has been deleted. The user can retry
			// /dispatch complete-milestone or merge manually once the underlying
			// issue is fixed (e.g. checkout to wrong branch, unresolved conflicts).
			// (#1668, #1891)
			ctx.notify(
				`Milestone merge failed: ${msg}. Your worktree and milestone branch are preserved — retry with \`/dispatch complete-milestone\` or merge manually.`,
				"warning",
			);
			// Clean up stale merge state left by failed squash-merge (#1389)
			// Use resolveGitDir to handle worktrees where .git is a file (gitdir pointer).
			// Use resolve() rather than join() to normalise any backslash separators on
			// Windows and avoid mixed-separator paths in downstream comparisons.
			try {
				const gitDir = resolveGitDir(originalBase || this.s.basePath);
				for (const f of ["SQUASH_MSG", "MERGE_HEAD", "MERGE_MSG"]) {
					const p = resolve(gitDir, f);
					if (existsSync(p)) unlinkSync(p);
				}
			} catch {
				/* best-effort */
			}
			// Error recovery: always restore to project root
			if (originalBase) {
				try {
					process.chdir(originalBase);
				} catch {
					/* best-effort */
				}
			}
			// Re-throw MergeConflictError so the auto loop can detect real code
			// conflicts and stop instead of retrying forever (#2330).
			if (err instanceof MergeConflictError) {
				throw err;
			}
		}
		// Always restore basePath and rebuild — whether merge succeeded or failed
		this.restoreToProjectRoot();
		debugLog("WorktreeResolver", {
			action: "mergeAndExit",
			milestoneId,
			result: "done",
			basePath: this.s.basePath,
		});
		return merged;
	}
	/** Branch-mode merge: check current branch, merge if on milestone branch.
	 *  Returns true when a merge actually ran (false on skip paths). */
	_mergeBranchMode(milestoneId, ctx) {
		try {
			const currentBranch = this.deps.getCurrentBranch(this.s.basePath);
			const milestoneBranch = this.deps.autoWorktreeBranch(milestoneId);
			if (currentBranch !== milestoneBranch) {
				debugLog("WorktreeResolver", {
					action: "mergeAndExit",
					milestoneId,
					mode: "branch",
					skipped: true,
					reason: "not-on-milestone-branch",
					currentBranch,
					milestoneBranch,
				});
				return false;
			}
			const roadmapPath = this.deps.resolveMilestoneFile(
				this.s.basePath,
				milestoneId,
				"ROADMAP",
			);
			if (!roadmapPath) {
				debugLog("WorktreeResolver", {
					action: "mergeAndExit",
					milestoneId,
					mode: "branch",
					skipped: true,
					reason: "no-roadmap",
				});
				return false;
			}
			const roadmapContent = this.deps.readFileSync(roadmapPath, "utf-8");
			const mergeResult = this.deps.mergeMilestoneToMain(
				this.s.basePath,
				milestoneId,
				roadmapContent,
			);
			// Rebuild GitService after merge (branch HEAD changed)
			this.rebuildGitService();
			if (mergeResult.codeFilesChanged) {
				ctx.notify(
					`Milestone ${milestoneId} merged (branch mode).${mergeResult.pushed ? " Pushed to remote." : ""}`,
					"info",
				);
			} else {
				ctx.notify(
					`WARNING: Milestone ${milestoneId} merged (branch mode) but contained NO code changes — only .sf/ metadata. ` +
						`Review the milestone output and re-run if code is missing.`,
					"warning",
				);
			}
			debugLog("WorktreeResolver", {
				action: "mergeAndExit",
				milestoneId,
				mode: "branch",
				result: "success",
			});
			return true;
		} catch (err) {
			const msg = getErrorMessage(err);
			debugLog("WorktreeResolver", {
				action: "mergeAndExit",
				milestoneId,
				mode: "branch",
				result: "error",
				error: msg,
			});
			ctx.notify(`Milestone merge failed (branch mode): ${msg}`, "warning");
			return false;
		}
	}
	// ── Merge and Enter Next ───────────────────────────────────────────────
	/**
	 * Milestone transition: merge the current milestone, then enter the next one.
	 *
	 * This is the pattern used when the loop detects that the active milestone
	 * has changed (e.g., current completed, next one is now active). The caller
	 * is responsible for re-deriving state between the merge and the enter.
	 */
	mergeAndEnterNext(currentMilestoneId, nextMilestoneId, ctx) {
		debugLog("WorktreeResolver", {
			action: "mergeAndEnterNext",
			currentMilestoneId,
			nextMilestoneId,
		});
		this.mergeAndExit(currentMilestoneId, ctx);
		this.enterMilestone(nextMilestoneId, ctx);
	}
}
