/**
 * AutoSession — encapsulates all mutable autonomous mode state into a single instance.
 *
 * Replaces ~40 module-level variables scattered across auto.ts with typed
 * properties on a class instance. Benefits:
 *
 * - reset() clears everything in one call (was 25+ manual resets in stopAuto)
 * - toJSON() provides diagnostic snapshots
 * - grep `s.` shows every state access
 * - Constructable for testing
 *
 * MAINTENANCE RULE: All new mutable autonomous mode state MUST be added here as a
 * class property, not as a module-level variable in auto.ts. If the state
 * needs clearing on stop, add it to reset(). Tests in
 * auto-session-encapsulation.test.ts enforce that auto.ts has no module-level
 * `let` or `var` declarations.
 */

import { emitJournalEvent } from "../journal.js";
import { recordCounter } from "../metrics-central.js";
import {
	buildModeState,
	resolveModelMode,
	resolvePermissionProfile,
	resolveRunControlMode,
	resolveWorkMode,
} from "../operating-model.js";
import { loadSessionModeState, saveSessionModeState } from "../sf-db.js";

// ─── Constants ───────────────────────────────────────────────────────────────

/**
 * Detect the current surface from environment variables.
 * Each surface sets a distinct env var in the child process it spawns.
 * This is the only reliable way to identify surface inside the SF extension,
 * because the `ctx.surface` field is not propagated to AutoSession directly.
 */
function detectSurface() {
	if (process.env.SF_HEADLESS === "1") return "headless";
	if (process.env.SF_WEB_BRIDGE_TUI === "1") return "web";
	return "tui";
}
export const MAX_UNIT_DISPATCHES = 3;
export const STUB_RECOVERY_THRESHOLD = 2;
export const MAX_LIFETIME_DISPATCHES = 6;
export const NEW_SESSION_TIMEOUT_MS = 120_000;
// ─── Singleton ───────────────────────────────────────────────────────────────
let _autoSessionInstance = null;
/** Get or create the singleton AutoSession instance. */
export function getAutoSession() {
	if (!_autoSessionInstance) {
		_autoSessionInstance = new AutoSession();
	}
	return _autoSessionInstance;
}
/** Reset the singleton instance (used in tests). */
export function resetAutoSession() {
	_autoSessionInstance = null;
}
// ─── AutoSession ─────────────────────────────────────────────────────────────

/**
 * Update terminal/tmux window title with current mode state.
 *
 * Purpose: make SF mode visible in terminal tabs, tmux window names,
 * and task switchers without requiring TUI focus.
 *
 * Consumer: AutoSession.setMode() after every mode transition.
 */
function updateTerminalTitle(mode) {
	if (
		typeof process === "undefined" ||
		!process.stdout ||
		!process.stdout.isTTY
	)
		return;
	const rcBadge =
		mode.runControl === "autonomous" ? "∞" : mode.runControl.slice(0, 1);
	const yoloBadge = mode.yolo ? "🚀" : "";
	const badge =
		yoloBadge +
		mode.workMode +
		"|" +
		rcBadge +
		"|" +
		mode.permissionProfile.slice(0, 1) +
		"|" +
		mode.modelMode.slice(0, 1);
	const title = "SF[" + badge + "]";
	// OSC 0: set icon name + window title
	process.stdout.write("\x1b]0;" + title + "\x07");
	// Also set process.title for OS task switchers
	process.title = title;
}

export class AutoSession {
	constructor() {
		// Try to load persisted mode state from DB
		this._loadPersistedModeState();
	}

	_loadPersistedModeState() {
		try {
			const persisted = loadSessionModeState();
			if (persisted) {
				this.workMode = resolveWorkMode(persisted.workMode);
				this.runControl = resolveRunControlMode(persisted.runControl);
				this.stepMode = this.runControl === "assisted";
				this.permissionProfile = resolvePermissionProfile(
					persisted.permissionProfile,
				);
				this.modelMode = resolveModelMode(persisted.modelMode);
				this.modeUpdatedAt = persisted.updatedAt;
			}
		} catch {
			// DB may not be open yet — use defaults
		}
		// Always stamp surface from env: persisted value reflects previous launch's
		// surface and should not override the current surface detection.
		this.surface = detectSurface();
		// Auto-activate YOLO when SF_YOLO=1 (set by --yolo / -y CLI flag)
		if (process.env.SF_YOLO === "1" && !this.yolo) {
			this.toggleYolo();
		}
	}

	// ── Lifecycle ────────────────────────────────────────────────────────────
	active = false;
	paused = false;
	stepMode = false;
	/**
	 * When false, the agent is forbidden from calling ask_user_questions.
	 * Assisted mode sets this true; `/autonomous` sets it false.
	 */
	canAskUser = true;
	verbose = false;
	activeEngineId = null;
	activeRunDir = null;
	cmdCtx = null;
	/**
	 * Set to true by stopAuto() so cleanupAfterLoopExit() can avoid sending
	 * a duplicate terminal notification in headless mode.
	 */
	stopAutoCalled = false;
	/**
	 * Last known ExtensionCommandContext that had newSession().
	 *
	 * Purpose: allow autonomous mode to start from non-command contexts (shortcut
	 * handlers, event handlers) by falling back to a previously cached command
	 * context. newSession() is bound at registration time and remains valid across
	 * session switches, so this is safe to reuse after /clear.
	 *
	 * NOT in reset() — intentionally persists across auto-stop/restart cycles so
	 * Ctrl+Y and similar shortcuts work after the loop exits cleanly.
	 */
	lastCommandCtx = null;
	/**
	 * YOLO mode: build + autonomous + deep + unrestricted.
	 * Tracks the local toggle state so the terminal title and status display
	 * can reflect it without going back through settingsManager.
	 */
	yolo = false;
	/** Previous mode snapshot saved before YOLO was activated, for restore on toggle-off. */
	_preYoloMode = null;
	// ── Mode state ───────────────────────────────────────────────────────────
	/**
	 * Current work mode: chat | plan | build | review | repair | research.
	 * Defaults to "chat" for new sessions.
	 */
	workMode = "chat";
	/**
	 * Requested run control: manual | assisted | autonomous.
	 * Active loop state still comes from `active` + `stepMode`; this field keeps
	 * command/UI mode state separate from whether a process is currently running.
	 */
	runControl = "manual";
	/**
	 * Current permission profile: restricted | normal | trusted | unrestricted.
	 * Defaults to "normal" — ask mode should be able to read files and run
	 * safe commands without permission prompts. Use /mode build for unrestricted.
	 */
	permissionProfile = "normal";
	/**
	 * Current model mode: fast | smart | deep.
	 * Defaults to "smart".
	 */
	modelMode = "smart";
	/**
	 * Surface identifier: tui | web | headless | rpc.
	 * Defaults to "tui".
	 */
	surface = "tui";
	/**
	 * ISO timestamp of last mode transition.
	 */
	modeUpdatedAt = null;
	// ── Paths ────────────────────────────────────────────────────────────────
	basePath = "";
	originalBasePath = "";
	previousProjectRootEnv = null;
	hadProjectRootEnv = false;
	projectRootEnvCaptured = false;
	previousMilestoneLockEnv = null;
	hadMilestoneLockEnv = false;
	milestoneLockEnvCaptured = false;
	sessionMilestoneLock = null;
	gitService = null;
	// ── Dispatch counters ────────────────────────────────────────────────────
	unitDispatchCount = new Map();
	unitLifetimeDispatches = new Map();
	unitRecoveryCount = new Map();
	// ── Timers ───────────────────────────────────────────────────────────────
	unitTimeoutHandle = null;
	wrapupWarningHandle = null;
	idleWatchdogHandle = null;
	continueHereHandle = null;
	// ── Current unit ─────────────────────────────────────────────────────────
	currentUnit = null;
	currentTraceId = null;
	currentTurnId = null;
	currentUnitRouting = null;
	currentMilestoneId = null;
	// ── Model state ──────────────────────────────────────────────────────────
	autoModeStartModel = null;
	autoModeStartThinkingLevel = null;
	originalThinkingLevel = null;
	/** Explicit /model pin captured at bootstrap (session-scoped policy override). */
	manualSessionModelOverride = null;
	currentUnitModel = null;
	/** Fully-qualified model ID (provider/id) set after selectAndApplyModel + hook overrides (#2899). */
	currentDispatchedModelId = null;
	/** Per-session, per-unit failed model routes skipped by runtime recovery. */
	modelFailures = [];
	originalModelId = null;
	originalModelProvider = null;
	lastBudgetAlertLevel = 0;
	// ── Recovery ─────────────────────────────────────────────────────────────
	pendingCrashRecovery = null;
	pendingVerificationRetry = null;
	/** Set when stuck detection triggers rethink: injected into next dispatch prompt. */
	pendingRethinkAttempt = null;
	verificationRetryCount = new Map();
	pausedSessionFile = null;
	pausedUnitType = null;
	pausedUnitId = null;
	resourceVersionOnStart = null;
	lastStateRebuildAt = 0;
	// ── Sidecar queue ─────────────────────────────────────────────────────
	sidecarQueue = [];
	// ── Tool invocation errors (#2883) ──────────────────────────────────
	/** Set when a SF tool execution ends with isError due to malformed/truncated
	 *  JSON arguments. Checked by postUnitPreVerification to break retry loops. */
	lastToolInvocationError = null;
	/** Set when turn-level git action fails during closeout. */
	lastGitActionFailure = null;
	/** Last turn-level git action status captured during finalize. */
	lastGitActionStatus = null;
	/**
	 * Last complete_task execution error for the current turn.
	 * Unlike malformed tool invocation errors, these are normal tool execution
	 * failures (for example a transient SUMMARY.md write failure) and should be
	 * retried in-flow instead of pausing autonomous mode.
	 */
	lastTaskCompleteFailure = null;
	/** Per-unit task completion failures to surface in the next execute-task prompt. */
	pendingTaskCompleteFailures = new Map();
	// ── Isolation degradation ────────────────────────────────────────────
	/** Set to true when worktree creation fails; prevents merge of nonexistent branch. */
	isolationDegraded = false;
	// ── Merge guard ──────────────────────────────────────────────────────
	/** Set to true after phases.ts successfully calls mergeAndExit, so that
	 *  stopAuto does not attempt the same merge a second time (#2645). */
	milestoneMergedInPhases = false;
	/** Set to the milestoneId after product audit fires at merge, so the audit
	 *  fires exactly once per milestone (not twice when mergeAndExit is called
	 *  at both the transition point and the terminal complete point). */
	productAuditMilestoneId = null;
	// ── Dispatch circuit breakers ──────────────────────────────────────
	rewriteAttemptCount = 0;
	/** Tracks consecutive bootstrap attempts that found phase === "complete".
	 *  Moved from module-level to per-session so s.reset() clears it (#1348). */
	consecutiveCompleteBootstraps = 0;
	// ── Rate-limiting / session tracking ────────────────────────────────────
	lastRequestTimestamp = 0;
	lastUnitAgentEndMessages = null;
	// ── Metrics ──────────────────────────────────────────────────────────────
	autoStartTime = 0;
	lastPromptCharCount;
	lastBaselineCharCount;
	pendingQuickTasks = [];
	// ── Safety harness ───────────────────────────────────────────────────────
	/** SHA of the pre-unit git checkpoint ref. Cleared on success or rollback. */
	checkpointSha = null;
	/** Dirty files captured before the current execute-task unit starts. */
	preUnitDirtyFiles = [];
	// ── Deferred commit (Fix 1) ──────────────────────────────────────────────
	/**
	 * True when postUnitPreVerification has staged files but deferred the git
	 * commit until after verification passes (Fix 1 deferral pattern).
	 *
	 * postUnitPostVerification reads this flag and calls git.commitStaged()
	 * before DB writes when it is set, then clears it.
	 *
	 * The timeout handler in phases.ts clears this flag and emits a diagnostic
	 * warning when postUnitPreVerification times out with staged-but-uncommitted
	 * changes (Fix 4).
	 */
	stagedPendingCommit = false;
	/**
	 * Task commit context stashed alongside stagedPendingCommit so that
	 * postUnitPostVerification can build a proper conventional commit message
	 * (with one-liner, key files, SF-Task trailer) rather than a fallback stub.
	 *
	 * Set when stagedPendingCommit is set; cleared together with it.
	 */
	pendingCommitTaskContext = null;
	// ── Slice-cadence start SHAs (#4765) ────────────────────────────────────
	// #4765 — slice-cadence collapse: main-branch SHAs at the moment each
	// milestone's first slice merge began. Used by resquashMilestoneOnMain at
	// milestone completion to collapse N slice commits into one. Cleared when
	// the milestone finishes (or resquash runs).
	milestoneStartShas = new Map();
	// ── Research unit terminal transition ──────────────────────────────────
	/**
	 * Set to true when a research unit (research-slice/research-milestone)
	 * successfully saves its RESEARCH artifact via save_summary.
	 * Subsequent planning tool calls are blocked to prevent post-artifact drift
	 * where the agent continues into milestone/slice/task planning.
	 */
	researchTerminalTransition = false;
	// ── Signal handler ───────────────────────────────────────────────────────
	sigtermHandler = null;
	// ── Loop promise state ──────────────────────────────────────────────────
	// Per-unit resolve function and session-switch guard live at module level
	// in auto-loop.ts (_currentResolve, _sessionSwitchInFlight).
	// ── Methods ──────────────────────────────────────────────────────────────
	clearTimers() {
		if (this.unitTimeoutHandle) {
			clearTimeout(this.unitTimeoutHandle);
			this.unitTimeoutHandle = null;
		}
		if (this.wrapupWarningHandle) {
			clearTimeout(this.wrapupWarningHandle);
			this.wrapupWarningHandle = null;
		}
		if (this.idleWatchdogHandle) {
			clearInterval(this.idleWatchdogHandle);
			this.idleWatchdogHandle = null;
		}
		if (this.continueHereHandle) {
			clearInterval(this.continueHereHandle);
			this.continueHereHandle = null;
		}
	}
	resetDispatchCounters() {
		this.unitDispatchCount.clear();
		this.unitLifetimeDispatches.clear();
	}
	get lockBasePath() {
		return this.originalBasePath || this.basePath;
	}
	reset() {
		this.clearTimers();
		// Lifecycle
		this.active = false;
		this.paused = false;
		this.stepMode = false;
		this.canAskUser = true;
		this.stopAutoCalled = false;
		this.verbose = false;
		this.activeEngineId = null;
		this.activeRunDir = null;
		this.cmdCtx = null;
		// Mode state
		this.workMode = "chat";
		this.runControl = "manual";
		this.permissionProfile = "normal";
		this.modelMode = "smart";
		this.surface = "tui";
		this.modeUpdatedAt = null;
		// Paths
		this.basePath = "";
		this.originalBasePath = "";
		this.previousProjectRootEnv = null;
		this.hadProjectRootEnv = false;
		this.projectRootEnvCaptured = false;
		this.previousMilestoneLockEnv = null;
		this.hadMilestoneLockEnv = false;
		this.milestoneLockEnvCaptured = false;
		this.sessionMilestoneLock = null;
		this.gitService = null;
		// Dispatch
		this.unitDispatchCount.clear();
		this.unitLifetimeDispatches.clear();
		this.unitRecoveryCount.clear();
		// Unit
		this.currentUnit = null;
		this.currentTraceId = null;
		this.currentTurnId = null;
		this.currentUnitRouting = null;
		this.currentMilestoneId = null;
		// Model
		this.autoModeStartModel = null;
		this.autoModeStartThinkingLevel = null;
		this.originalThinkingLevel = null;
		this.manualSessionModelOverride = null;
		this.currentUnitModel = null;
		this.currentDispatchedModelId = null;
		this.modelFailures.length = 0;
		this.originalModelId = null;
		this.originalModelProvider = null;
		this.lastBudgetAlertLevel = 0;
		// Recovery
		this.pendingCrashRecovery = null;
		this.pendingVerificationRetry = null;
		this.pendingRethinkAttempt = null;
		this.verificationRetryCount.clear();
		this.pausedSessionFile = null;
		this.pausedUnitType = null;
		this.pausedUnitId = null;
		this.resourceVersionOnStart = null;
		this.lastStateRebuildAt = 0;
		// Rate-limiting / session tracking
		this.lastRequestTimestamp = 0;
		this.lastUnitAgentEndMessages = null;
		// Metrics
		this.autoStartTime = 0;
		this.lastPromptCharCount = undefined;
		this.lastBaselineCharCount = undefined;
		this.pendingQuickTasks = [];
		this.sidecarQueue = [];
		this.rewriteAttemptCount = 0;
		this.consecutiveCompleteBootstraps = 0;
		this.lastToolInvocationError = null;
		this.lastGitActionFailure = null;
		this.lastGitActionStatus = null;
		this.lastTaskCompleteFailure = null;
		this.pendingTaskCompleteFailures.clear();
		this.isolationDegraded = false;
		this.milestoneMergedInPhases = false;
		this.productAuditMilestoneId = null;
		this.checkpointSha = null;
		this.preUnitDirtyFiles = [];
		this.stagedPendingCommit = false;
		this.pendingCommitTaskContext = null;
		this.milestoneStartShas = new Map();
		// Research terminal transition
		this.researchTerminalTransition = false;
		// Signal handler
		this.sigtermHandler = null;
		// Loop promise state lives in auto-loop.ts module scope
	}
	/**
	 * Update mode state with validation and timestamp.
	 *
	 * Purpose: centralize mode transitions so every change is logged and
	 * validated against canonical vocabulary.
	 *
	 * Consumer: command handlers and auto health gates.
	 */
	setMode({
		workMode,
		runControl,
		permissionProfile,
		modelMode,
		surface,
		reason = "user-command",
		scope = "now",
	} = {}) {
		const prev = this.getMode();
		if (workMode !== undefined) this.workMode = resolveWorkMode(workMode);
		if (runControl !== undefined) {
			const mode = resolveRunControlMode(runControl);
			this.runControl = mode;
			this.stepMode = mode === "assisted";
		}
		if (permissionProfile !== undefined) {
			this.permissionProfile = resolvePermissionProfile(permissionProfile);
		}
		if (modelMode !== undefined) this.modelMode = resolveModelMode(modelMode);
		if (surface !== undefined) this.surface = surface;
		this.modeUpdatedAt = new Date().toISOString();
		const next = this.getMode();
		// Record mode transition metrics
		if (prev.workMode !== next.workMode) {
			recordCounter("sf_mode_transition_total", {
				axis: "work_mode",
				from: prev.workMode,
				to: next.workMode,
				reason,
			});
		}
		if (prev.runControl !== next.runControl) {
			recordCounter("sf_mode_transition_total", {
				axis: "run_control",
				from: prev.runControl,
				to: next.runControl,
				reason,
			});
		}
		if (prev.permissionProfile !== next.permissionProfile) {
			recordCounter("sf_mode_transition_total", {
				axis: "permission_profile",
				from: prev.permissionProfile,
				to: next.permissionProfile,
				reason,
			});
		}
		if (prev.modelMode !== next.modelMode) {
			recordCounter("sf_mode_transition_total", {
				axis: "model_mode",
				from: prev.modelMode,
				to: next.modelMode,
				reason,
			});
		}
		// Persist mode state to DB for durability across sessions
		if (this.basePath) {
			try {
				saveSessionModeState({ ...next, updatedAt: this.modeUpdatedAt });
			} catch {
				// DB persistence is best-effort; don't fail mode transition
			}
			// Log mode transition to journal for audit trail
			try {
				emitJournalEvent(this.basePath, {
					ts: this.modeUpdatedAt,
					flowId: `mode-transition:${Date.now()}`,
					seq: 0,
					eventType: "mode-transition",
					rule: "session-mode-change",
					data: {
						from: prev,
						to: next,
						reason,
						scope,
					},
				});
			} catch {
				// Journal is best-effort; don't fail mode transition
			}
		}
		// Update terminal title with mode state for tmux/terminal visibility
		try {
			updateTerminalTitle(next);
		} catch {
			// Title update is best-effort
		}
		return { from: prev, to: next };
	}
	/**
	 * Get current mode state as a canonical object.
	 */
	getMode() {
		return {
			...buildModeState({
				workMode: this.workMode,
				runControl: this.active
					? this.stepMode
						? "assisted"
						: "autonomous"
					: this.runControl,
				permissionProfile: this.permissionProfile,
				modelMode: this.modelMode,
				surface: this.surface,
			}),
			paused: this.paused,
		};
	}
	/**
	 * Toggle YOLO mode on/off.
	 *
	 * Purpose: one-key shortcut that slams all axes to maximum autonomy
	 * (build + autonomous + deep + unrestricted). On deactivation, restores
	 * the mode that was active before YOLO was turned on.
	 *
	 * Consumer: Ctrl+Y keybinding in steerable-autonomous-extension.js.
	 * Returns the new yolo state (true = on, false = off).
	 */
	toggleYolo() {
		if (this.yolo) {
			// Restore pre-YOLO mode
			this.yolo = false;
			if (this._preYoloMode) {
				this.setMode({ ...this._preYoloMode, reason: "yolo-off" });
				this._preYoloMode = null;
			}
		} else {
			// Save current mode, then slam all axes to maximum
			this._preYoloMode = {
				workMode: this.workMode,
				runControl: this.runControl,
				permissionProfile: this.permissionProfile,
				modelMode: this.modelMode,
			};
			this.yolo = true;
			this.setMode({
				workMode: "build",
				runControl: "autonomous",
				permissionProfile: "unrestricted",
				modelMode: "deep",
				reason: "yolo-on",
			});
		}
		// Refresh terminal title to show/hide 🚀 badge
		try {
			updateTerminalTitle({ ...this.getMode(), yolo: this.yolo });
		} catch {
			// best-effort
		}
		return this.yolo;
	}
	/** Returns true when YOLO mode is currently active. */
	isYolo() {
		return this.yolo;
	}
	toJSON() {
		return {
			active: this.active,
			paused: this.paused,
			stepMode: this.stepMode,
			basePath: this.basePath,
			activeEngineId: this.activeEngineId,
			activeRunDir: this.activeRunDir,
			currentMilestoneId: this.currentMilestoneId,
			currentUnit: this.currentUnit,
			unitDispatchCount: Object.fromEntries(this.unitDispatchCount),
			mode: this.getMode(),
		};
	}
}
