/**
 * task/worktree-cleanup.mjs — honor task_isolation.cleanup_on_close on task close (C29).
 *
 * Extracted from cmdTask close-case to keep bin/apt-tools.mjs → commands/task.mjs
 * under the 800 LOC gate. Pure behavior move, no semantic change.
 *
 * Modes:
 *   'never' → no-op, return action:'noop'
 *   'ask'   → return cleanup_command for the skill to prompt
 *   'auto'  → merge back into base_branch, then remove the worktree + branch
 */

import { execFileSync } from 'node:child_process'

/**
 * Truncate a thrown git error to its first 3 stderr/message lines for
 * surfacing in cleanup envelopes. Extracted from the original inline
 * duplication at the auto-mode merge-error catch.
 *
 * @param {unknown} err
 * @returns {string}
 */
function shortErr(err) {
	return (err?.stderr || err?.message || '').toString().trim().split('\n').slice(0, 3).join(' ')
}

/**
 * Sanitize an error message for inclusion in returned envelopes: first line
 * only, absolute paths replaced with a placeholder to avoid leaking local
 * filesystem layout.
 *
 * @param {string} msg
 * @returns {string}
 */
function sanitizeErrMsg(msg) {
	return msg
		.split('\n')[0]
		.replace(/\/[^\s:'"]+/g, '<path>')
		.trim()
}

/**
 * Run `git <args>` with up to 3 attempts and ~200ms backoff when the
 * thrown error's stderr names `.git/index.lock`. Other errors propagate
 * on the first failure. Two concurrent `task close-merged` invocations
 * from different terminals race on git's internal index.lock;
 * retry-on-EEXIST handles that without an app-level mutex (which
 * wouldn't prevent the underlying git race anyway).
 *
 * Sleep uses the same Atomics.wait pattern as withFileLock in
 * util/fs-lock.mjs to park the event loop without spinning the CPU.
 *
 * @param {string[]} args
 * @param {object} opts - forwarded to execFileSync
 * @returns {string} stdout
 */
function runGitWithRetry(args, opts) {
	const maxAttempts = 3
	const backoffMs = 200
	let lastErr = null
	for (let attempt = 1; attempt <= maxAttempts; attempt++) {
		try {
			return execFileSync('git', args, opts)
		} catch (err) {
			lastErr = err
			const stderrText = String(err?.stderr ?? '')
			if (!/index\.lock/i.test(stderrText) || attempt === maxAttempts) {
				throw err
			}
			try {
				Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, backoffMs)
			} catch {
				/* runtime without SharedArrayBuffer — fall through, retry immediately */
			}
		}
	}
	// Unreachable — the loop either returns or throws.
	throw lastErr
}

/**
 * @param {object} task - closed task record with worktree_path/branch/base_branch.
 * @param {string} mode - 'never' | 'ask' | 'auto'
 * @param {string} targetDir - main-repo project directory (cwd for git ops).
 * @param {object} [opts]
 * @param {boolean} [opts.prMerged=false] - true when the PR has been confirmed
 *   merged on the remote. Skips local merge-back regardless of merge strategy
 *   (merge / squash / rebase). Instead: fetch + ff-only the base branch, then
 *   GC worktree + branch. Defaults to false so existing callers retain
 *   byte-identical legacy behavior.
 * @returns {object|null} cleanup report block, or null if no worktree on task.
 */
export function computeWorktreeCleanup(task, mode, targetDir, opts = {}) {
	if (!task?.worktree_path) return null

	const wtPath = task.worktree_path
	const taskBranch = task.branch
	const baseBranch = task.base_branch || 'main'

	if (mode === 'never') {
		return {
			mode,
			worktree_path: wtPath,
			branch: taskBranch,
			base_branch: baseBranch,
			action: 'noop',
		}
	}

	if (mode === 'ask') {
		// When the PR is already merged on the remote, teach humans the
		// fetch + ff-only recipe instead of the legacy local-merge recipe
		// (which corrupts main when local task ref is stale or the PR used
		// squash/rebase).
		const cleanupCmd =
			opts.prMerged === true
				? `git fetch origin '${baseBranch}' && git checkout '${baseBranch}' && git merge --ff-only 'origin/${baseBranch}' && git worktree remove '${wtPath}' && git branch -d '${taskBranch}'`
				: `git checkout '${baseBranch}' && git merge --no-edit '${taskBranch}' && git worktree remove '${wtPath}' && git branch -d '${taskBranch}'`
		return {
			mode,
			worktree_path: wtPath,
			branch: taskBranch,
			base_branch: baseBranch,
			action: 'pending',
			cleanup_command: cleanupCmd,
			...(opts.prMerged === true ? { pr_merged_remotely: true } : {}),
		}
	}

	// mode === 'auto' && opts.prMerged === true.
	// Trust origin as the merge authority. Method-agnostic across merge,
	// squash, and rebase: fetch + checkout + ff-only, then GC. Never
	// local-merge the task branch when GitHub already merged the PR.
	if (opts.prMerged === true) {
		let fetchOk = false
		let ffOk = false
		let branchDeleted = false
		const warnings = []
		try {
			runGitWithRetry(['fetch', 'origin', baseBranch, '--no-tags'], {
				cwd: targetDir,
				encoding: 'utf-8',
				stdio: 'pipe',
			})
			fetchOk = true
		} catch (err) {
			// Offline or network-error. Skip ff-only; user must
			// `git pull --ff-only` later. Cleanup still proceeds.
			warnings.push(`fetch_failed: ${sanitizeErrMsg(shortErr(err))}`)
		}

		try {
			runGitWithRetry(['checkout', baseBranch], {
				cwd: targetDir,
				encoding: 'utf-8',
				stdio: 'pipe',
			})
		} catch (err) {
			// Couldn't switch to base branch (e.g. fresh clone where base
			// doesn't exist locally). Surface and abort cleanup; preserve
			// local repo state — do NOT GC the worktree+branch.
			return {
				mode,
				worktree_path: wtPath,
				branch: taskBranch,
				base_branch: baseBranch,
				action: 'checkout_failed',
				error: shortErr(err),
				message: `Could not checkout ${baseBranch}; local repo state preserved. Resolve and re-run.`,
			}
		}

		if (fetchOk) {
			try {
				runGitWithRetry(['merge', '--ff-only', `origin/${baseBranch}`], {
					cwd: targetDir,
					encoding: 'utf-8',
					stdio: 'pipe',
				})
				ffOk = true
			} catch (err) {
				// ff-only failed → local base has commits not on origin. Do
				// NOT auto-resolve; warn and leave local at its current
				// commit. Worktree + branch GC still runs.
				warnings.push(`ff_only_failed: ${sanitizeErrMsg(shortErr(err))}`)
			}
		}

		try {
			runGitWithRetry(['worktree', 'remove', wtPath], {
				cwd: targetDir,
				encoding: 'utf-8',
				stdio: 'pipe',
			})
		} catch {
			try {
				runGitWithRetry(['worktree', 'remove', '--force', wtPath], {
					cwd: targetDir,
					encoding: 'utf-8',
					stdio: 'pipe',
				})
			} catch {
				/* worktree already gone or unreachable */
			}
		}
		try {
			runGitWithRetry(['branch', '-d', taskBranch], {
				cwd: targetDir,
				encoding: 'utf-8',
				stdio: 'pipe',
			})
			branchDeleted = true
		} catch (err) {
			// Squash/rebase strategies leave the local task branch as
			// "not fully merged" (different SHAs from origin). Surface as
			// a warning rather than force-delete; user can `git branch -D`
			// manually if they want.
			warnings.push(`branch_delete_failed: ${sanitizeErrMsg(shortErr(err))}`)
		}

		return {
			mode,
			worktree_path: wtPath,
			branch: taskBranch,
			base_branch: baseBranch,
			action: 'pr_merged_remotely_cleaned',
			fetched: fetchOk,
			fast_forwarded: ffOk,
			branch_deleted: branchDeleted,
			...(warnings.length ? { warnings } : {}),
		}
	}

	// mode === 'auto' (legacy) — attempt merge-back in the main repo
	// (never in the worktree). Direct `task close` callers without
	// --pr-merged-remotely hit this path; byte-identical legacy behavior.
	let mergeOk = false
	let mergeError = null
	try {
		execFileSync('git', ['checkout', baseBranch], {
			cwd: targetDir,
			encoding: 'utf-8',
			stdio: 'pipe',
		})
		execFileSync('git', ['merge', '--no-edit', taskBranch], {
			cwd: targetDir,
			encoding: 'utf-8',
			stdio: 'pipe',
		})
		mergeOk = true
	} catch (err) {
		mergeError = shortErr(err)
		// Abort the failed merge so the main repo is left clean.
		try {
			execFileSync('git', ['merge', '--abort'], {
				cwd: targetDir,
				encoding: 'utf-8',
				stdio: 'pipe',
			})
		} catch {
			/* */
		}
	}

	if (mergeOk) {
		try {
			execFileSync('git', ['worktree', 'remove', wtPath], {
				cwd: targetDir,
				encoding: 'utf-8',
				stdio: 'pipe',
			})
		} catch {
			try {
				execFileSync('git', ['worktree', 'remove', '--force', wtPath], {
					cwd: targetDir,
					encoding: 'utf-8',
					stdio: 'pipe',
				})
			} catch {
				/* */
			}
		}
		try {
			execFileSync('git', ['branch', '-d', taskBranch], {
				cwd: targetDir,
				encoding: 'utf-8',
				stdio: 'pipe',
			})
		} catch {
			/* already deleted or unmerged */
		}
		return {
			mode,
			worktree_path: wtPath,
			branch: taskBranch,
			base_branch: baseBranch,
			action: 'merged_and_removed',
		}
	}

	// Preserve the branch + worktree — surface merge-pending.
	return {
		mode,
		worktree_path: wtPath,
		branch: taskBranch,
		base_branch: baseBranch,
		action: 'merge_pending',
		merge_error: mergeError,
		message: `Task branch ${taskBranch} has merge conflicts against ${baseBranch}. Worktree preserved at ${wtPath}. Resolve manually: git checkout ${baseBranch} && git merge ${taskBranch}`,
	}
}
