import type { TextDocumentShowOptions, TextEditor, ViewColumn } from 'vscode';
import { env, Range, Uri, window, workspace } from 'vscode';
import { GitCommit } from '@gitlens/git/models/commit.js';
import type { GitFile } from '@gitlens/git/models/file.js';
import type { GitFileChangeShape } from '@gitlens/git/models/fileChange.js';
import { GitFileChange } from '@gitlens/git/models/fileChange.js';
import type { GitRevisionReference } from '@gitlens/git/models/reference.js';
import { deletedOrMissing, uncommittedStaged } from '@gitlens/git/models/revision.js';
import { createReference, getReferenceLabel } from '@gitlens/git/utils/reference.utils.js';
import {
	createRevisionRange,
	isUncommitted,
	isUncommittedStaged,
	shortenRevision,
} from '@gitlens/git/utils/revision.utils.js';
import { basename } from '@gitlens/utils/path.js';
import { getSettledValue } from '@gitlens/utils/promise.js';
import { fileUri, joinUriPath } from '@gitlens/utils/uri.js';
import type { DiffWithCommandArgs } from '../../commands/diffWith.js';
import type { DiffWithPreviousCommandArgs } from '../../commands/diffWithPrevious.js';
import type { DiffWithWorkingCommandArgs } from '../../commands/diffWithWorking.js';
import type { ExplainCommitCommandArgs } from '../../commands/explainCommit.js';
import type { OpenFileOnRemoteCommandArgs } from '../../commands/openFileOnRemote.js';
import type { OpenOnlyChangedFilesCommandArgs } from '../../commands/openOnlyChangedFiles.js';
import type { OpenWorkingFileCommandArgs } from '../../commands/openWorkingFile.js';
import type { ShowQuickCommitCommandArgs } from '../../commands/showQuickCommit.js';
import type { ShowQuickCommitFileCommandArgs } from '../../commands/showQuickCommitFile.js';
import type { FileAnnotationType } from '../../config.js';
import { GlyphChars } from '../../constants.js';
import type { Source } from '../../constants.telemetry.js';
import { Container } from '../../container.js';
import { showGitErrorMessage } from '../../messages.js';
import { showRevisionFilesPicker } from '../../quickpicks/revisionFilesPicker.js';
import { executeCommand, executeEditorCommand } from '../../system/-webview/command.js';
import { configuration } from '../../system/-webview/configuration.js';
import {
	getOrOpenTextEditor,
	openChangesEditor,
	openDiffEditor,
	openTextEditor,
	openTextEditors,
} from '../../system/-webview/vscode/editors.js';
import type { ViewNode } from '../../views/nodes/abstract/viewNode.js';
import type { RevealOptions } from '../../views/viewBase.js';
import type { ShowInCommitGraphCommandArgs } from '../../webviews/plus/graph/registration.js';
import { GitUri } from '../gitUri.js';
import { getAheadBehindFilesQuery } from '../queryResults.js';
import { findCommitFile, getCommitDate, getCommitForFile } from '../utils/-webview/commit.utils.js';
import { getReferenceFromRevision } from '../utils/-webview/reference.utils.js';

export type Ref = { repoPath: string; ref: string };
export type RefRange = { repoPath: string; rhs: string; lhs: string };

type ShowOptions = TextDocumentShowOptions & { sourceViewColumn?: ViewColumn; title?: string };

export interface FilesComparison {
	files: GitFile[];
	repoPath: string;
	ref1: string;
	ref2: string;
	title?: string;
}

const filesOpenThreshold = 10;
const filesOpenDiffsThreshold = 10;
const filesOpenMultiDiffThreshold = 50;

export async function applyChanges(
	file: string | GitFile,
	rev1: GitRevisionReference,
	rev2?: GitRevisionReference,
): Promise<void> {
	let create = false;
	let ref1 = rev1.ref;
	let ref2 = rev2?.ref;
	if (typeof file !== 'string') {
		// If the file is `?` (untracked), then this must be a stash, so get the ^3 commit to access the untracked file
		if (file.status === '?') {
			ref1 = `${ref1}^3`;
			create = true;
		} else if (file.status === 'A') {
			create = true;
		} else if (file.status === 'D') {
			// If the file is deleted, check to see if it exists, if so, apply the delete, otherwise restore it from the previous commit
			const uri = GitUri.fromFile(file, rev1.repoPath);
			try {
				await workspace.fs.stat(uri);
			} catch {
				create = true;

				// If restoring a deleted file (e.g. a newly added file), swap the refs to restore from the previous commit
				[ref1, ref2] = [ref2 === '' ? 'HEAD' : (ref2 ?? `${ref1}^`), ref1];
			}
		}
	}

	if (create) {
		const uri = GitUri.fromFile(file, rev1.repoPath);
		await Container.instance.git.applyChangesToWorkingFile(uri, ref1, ref2);
		await openFile(uri, { preserveFocus: true, preview: false });
	} else {
		// Open the working file to ensure undo will work
		await openFile(file, rev1, { preserveFocus: true, preview: false });
		await Container.instance.git.applyChangesToWorkingFile(GitUri.fromFile(file, rev1.repoPath, ref1), ref1, ref2);
	}
}

export async function copyIdToClipboard(ref: Ref | GitCommit): Promise<void> {
	await env.clipboard.writeText(ref.ref);
}

export async function copyMessageToClipboard(ref: Ref | GitCommit): Promise<void> {
	let commit;
	if (GitCommit.is(ref)) {
		commit = ref;
		if (commit.message == null) {
			await GitCommit.ensureFullDetails(commit);
		}
	} else {
		commit = await Container.instance.git.getRepositoryService(ref.repoPath).commits.getCommit(ref.ref);
		if (commit == null) return;
	}

	const message = commit.message ?? commit.summary;
	await env.clipboard.writeText(message);
}

export async function openCommitChanges(
	container: Container,
	commit: GitCommit,
	openIndividually: boolean | undefined,
	options?: ShowOptions,
	filter?: (file: GitFileChange) => boolean,
): Promise<void> {
	const { files, refs } = await getCommitChangesArgs(commit, filter);

	openIndividually ??= !configuration.get('views.openChangesInMultiDiffEditor');
	if (!options?.title) {
		options = {
			...options,
			title: `Changes in ${shortenRevision(commit.sha, { strings: { working: 'Working Tree' } })}`,
		};
	}

	return openMultipleChanges(container, files, refs, openIndividually, options);
}

export async function openCommitChangesInDiffTool(commit: GitCommit): Promise<void> {
	const { files } = await getCommitChangesArgs(commit);

	if (
		!(await confirmOpenIfNeeded(files, {
			message: `Are you sure you want to externally open the changes for each of the ${files.length} files?`,
			confirmButton: 'Open Changes',
			threshold: filesOpenDiffsThreshold,
		}))
	) {
		return;
	}

	for (const file of files) {
		void openChangesInDiffTool(file, commit);
	}
}

export async function openCommitChangesWithWorking(
	container: Container,
	commit: GitCommit,
	openIndividually: boolean | undefined,
	options?: ShowOptions,
	filter?: (file: GitFileChange) => boolean,
): Promise<void> {
	const { files } = await getCommitChangesArgs(commit, filter);
	openIndividually ??= !configuration.get('views.openChangesInMultiDiffEditor');
	return openMultipleChangesWithWorking(container, files, commit, openIndividually, options);
}

export async function openMultipleChanges(
	container: Container,
	files: GitFile[] | readonly GitFile[],
	refs: RefRange,
	openIndividually: boolean | undefined,
	options?: ShowOptions,
): Promise<void> {
	openIndividually ??= !configuration.get('views.openChangesInMultiDiffEditor');
	if (openIndividually) {
		if (
			!(await confirmOpenIfNeeded(files, {
				message: `Are you sure you want to open the changes for each of the ${files.length} files?`,
				confirmButton: 'Open Changes',
				threshold: filesOpenDiffsThreshold,
			}))
		) {
			return;
		}

		options = { preserveFocus: true, preview: false, ...options };

		for (const file of files) {
			if (refs.rhs === '') {
				await openChangesWithWorking(file, { repoPath: refs.repoPath, ref: refs.lhs }, options);
			} else {
				await openChanges(file, refs, options);
			}
		}

		return;
	}

	if (
		!(await confirmOpenIfNeeded(files, {
			message: `Are you sure you want to view the changes for all ${files.length} files?`,
			confirmButton: 'View Changes',
			threshold: filesOpenMultiDiffThreshold,
		}))
	) {
		return;
	}

	let title;
	if (options != null) {
		({ title, ...options } = options);
	}
	title ??= `Changes between ${shortenRevision(refs.lhs, { strings: { working: 'Working Tree' } })} ${
		GlyphChars.ArrowLeftRightLong
	} ${shortenRevision(refs.rhs, { strings: { working: 'Working Tree' } })}`;

	const svc = container.git.getRepositoryService(refs.repoPath);

	const resources: Parameters<typeof openChangesEditor>[0] = [];
	for (const file of files) {
		// Untracked files in a stash live in the `^3` parent (status `?` implies stash, by convention).
		const rhsRef = file.status === '?' ? `${refs.rhs}^3` : refs.rhs;
		let rhs = file.status === 'D' ? undefined : (await svc.getBestRevisionUri(file.path, rhsRef))!;
		if (refs.rhs === '') {
			if (rhs != null) {
				rhs = await svc.getWorkingUri(rhs);
			} else {
				rhs = Uri.from({
					scheme: 'untitled',
					authority: '',
					path: svc.getAbsoluteUri(file.path, refs.repoPath).fsPath,
				});
			}
		}

		const lhs =
			file.status === 'A'
				? undefined
				: file.status === '?'
					? svc.getRevisionUri(deletedOrMissing, file.originalPath ?? file.path)
					: (await svc.getBestRevisionUri(file.originalPath ?? file.path, refs.lhs))!;

		const uri = (file.status === 'D' ? lhs : rhs) ?? svc.getAbsoluteUri(file.path, refs.repoPath);
		if (rhs?.scheme === 'untitled' && lhs == null) continue;

		resources.push({ uri: uri, lhs: lhs, rhs: rhs });
	}

	await openChangesEditor(resources, title, options);
}

export async function openMultipleChangesWithWorking(
	container: Container,
	files: GitFile[] | readonly GitFile[],
	ref: Ref,
	openIndividually: boolean | undefined,
	options?: ShowOptions,
): Promise<void> {
	return openMultipleChanges(
		container,
		files,
		{ repoPath: ref.repoPath, lhs: ref.ref, rhs: '' },
		openIndividually,
		options,
	);
}

/**
 * Open working-tree changes in VS Code's multi-diff editor with per-file
 * staged-vs-unstaged routing.
 *
 * For each file, routes based on the `staged` flag:
 * - `staged: true`  → HEAD  ↔ index  (staged diff)
 * - `staged: false` → index ↔ working tree (unstaged diff)
 *
 * A file that appears twice (once staged, once unstaged) yields two entries
 * with different diffs instead of two identical HEAD↔working-tree entries.
 */
export async function openWipMultipleChanges(
	container: Container,
	files: readonly GitFileChangeShape[],
	repoPath: string,
	options?: ShowOptions,
): Promise<void> {
	if (
		!(await confirmOpenIfNeeded(files, {
			message: `Are you sure you want to view the changes for all ${files.length} files?`,
			confirmButton: 'View Changes',
			threshold: filesOpenMultiDiffThreshold,
		}))
	) {
		return;
	}

	let title;
	if (options != null) {
		({ title, ...options } = options);
	}
	title ??= 'Working Changes';

	const svc = container.git.getRepositoryService(repoPath);

	const resources: Parameters<typeof openChangesEditor>[0] = [];
	for (const file of files) {
		const staged = file.staged === true;
		const lhsRef = staged ? 'HEAD' : uncommittedStaged;
		const rhsRef = staged ? uncommittedStaged : '';

		let rhs = file.status === 'D' ? undefined : (await svc.getBestRevisionUri(file.path, rhsRef))!;
		if (rhsRef === '' && rhs != null) {
			rhs = await svc.getWorkingUri(rhs);
		} else if (rhsRef === '' && rhs == null) {
			rhs = Uri.from({
				scheme: 'untitled',
				authority: '',
				path: svc.getAbsoluteUri(file.path, repoPath).fsPath,
			});
		}

		const lhs =
			file.status === 'A'
				? undefined
				: file.status === '?'
					? svc.getRevisionUri(deletedOrMissing, file.originalPath ?? file.path)
					: (await svc.getBestRevisionUri(file.originalPath ?? file.path, lhsRef))!;

		const uri = (file.status === 'D' ? lhs : rhs) ?? svc.getAbsoluteUri(file.path, repoPath);
		if (rhs?.scheme === 'untitled' && lhs == null) continue;

		resources.push({ uri: uri, lhs: lhs, rhs: rhs });
	}

	await openChangesEditor(resources, title, options);
}

/**
 * Single-file equivalent of {@link openWipMultipleChanges}. Routes by `file.staged` and matches
 * SCM-view behavior for the non-diff cases (untracked → open file, staged add → open index blob,
 * staged delete → open HEAD blob). Revision URI construction delegates to
 * {@link GitRepositoryService.getBestRevisionUri}, which:
 *  - returns a `git:` URI (registered with VS Code's git extension) for the index when the SCM
 *    knows about the repo — gutter Stage/Unstage Hunk + SCM toolbar work in that case
 *  - falls back to a `gitlens:` URI served by our own FS provider otherwise — diff content is
 *    correct but the gutter actions won't appear
 * and crucially handles the canonical-path remapping for symlinked workspaces. Constructing
 * `git:` URIs directly (without that probe) silently produces broken content for worktrees that
 * the SCM hasn't registered.
 *
 * Routing:
 * - `staged: true`  → HEAD ↔ index  (the staged-portion diff)
 * - `staged: false` → index ↔ working tree (the unstaged-portion diff; equals HEAD ↔ working
 *   when the file has no staged content)
 *
 * SCM-style handling for non-modified statuses:
 *  - Untracked (`?`)                  → open the working file directly. No prior revision exists.
 *  - Staged addition (`A` && staged)  → open the index blob as a single readonly editor.
 *  - Staged deletion (`D` && staged)  → open the HEAD blob as a single readonly editor.
 *  - Unstaged deletion                → diff with an empty rhs placeholder (all deletions).
 *  - Added & non-staged (rare)        → diff with an empty lhs placeholder (all additions).
 *  - Renamed/Copied                   → lhs uses `originalPath` so the pre-rename rev compares.
 */
export async function openWipChanges(
	file: GitFileChangeShape,
	repoPath: string,
	options?: TextDocumentShowOptions & { sideTitle?: string },
): Promise<void> {
	const svc = Container.instance.git.getRepositoryService(repoPath);
	const fileName = basename(file.path);

	// Untracked: open directly — SCM parity, no diff makes sense.
	if (file.status === '?') {
		await openTextEditor(svc.getAbsoluteUri(file.path, repoPath), options);
		return;
	}

	const staged = file.staged === true;

	const emptyLhsUri = (): Uri => svc.getRevisionUri(deletedOrMissing, file.originalPath ?? file.path);
	const emptyRhsUri = (): Uri => svc.getRevisionUri(deletedOrMissing, file.path);
	// Always delegate to getBestRevisionUri so we get the SCM-aware scheme selection + canonical
	// remapping for free. For HEAD ref it returns gitlens:; for uncommittedStaged it returns git:
	// when the SCM knows the repo and gitlens: when not.
	const revisionUri = async (relPath: string, ref: string, emptyFallback: () => Uri): Promise<Uri> =>
		(await svc.getBestRevisionUri(relPath, ref)) ?? emptyFallback();

	// Staged deletion: open the HEAD blob as a single readonly editor — SCM does the same because
	// the rhs (index) has no content to diff against. Use the originalPath when present so a
	// staged rename-delete still surfaces the right pre-rename content.
	if (file.status === 'D' && staged) {
		await openTextEditor(await revisionUri(file.originalPath ?? file.path, 'HEAD', emptyLhsUri), options);
		return;
	}

	// Staged addition: open the index blob as a single readonly editor — SCM does the same
	// because the lhs (HEAD) has no content to diff against.
	if (file.status === 'A' && staged) {
		await openTextEditor(await revisionUri(file.path, uncommittedStaged, emptyRhsUri), options);
		return;
	}

	const lhsRef = staged ? 'HEAD' : uncommittedStaged;
	const rhsRef = staged ? uncommittedStaged : '';

	const lhs: Uri =
		file.status === 'A' ? emptyLhsUri() : await revisionUri(file.originalPath ?? file.path, lhsRef, emptyLhsUri);

	let rhs: Uri;
	if (file.status === 'D') {
		// Unstaged delete only at this point (staged delete returned above).
		rhs = emptyRhsUri();
	} else if (rhsRef === '') {
		rhs = svc.getAbsoluteUri(file.path, repoPath);
	} else {
		rhs = await revisionUri(file.path, rhsRef, emptyRhsUri);
	}

	const sideLabel = options?.sideTitle ?? (staged ? 'Index' : 'Working Tree');
	const title = `${fileName} (${sideLabel})`;

	await openDiffEditor(lhs, rhs, title, options);
}

export async function openChanges(
	file: string | Uri | GitFile,
	commit: GitCommit,
	options?: TextDocumentShowOptions,
): Promise<void>;
export async function openChanges(
	file: GitFile,
	refs: RefRange,
	options?: TextDocumentShowOptions & { lhsTitle?: string; rhsTitle?: string },
): Promise<void>;
export async function openChanges(
	file: GitFile,
	commitOrRefs: GitCommit | RefRange,
	options?: TextDocumentShowOptions & { lhsTitle?: string; rhsTitle?: string },
): Promise<void>;
export async function openChanges(
	file: string | Uri | GitFile,
	commitOrRefs: GitCommit | RefRange,
	options?: TextDocumentShowOptions & { lhsTitle?: string; rhsTitle?: string },
): Promise<void> {
	const hasCommit = GitCommit.is(commitOrRefs);

	if (typeof file === 'string' || file instanceof Uri) {
		if (!hasCommit) throw new Error('Invalid arguments');

		const f = await findCommitFile(commitOrRefs, file);
		if (f == null) throw new Error('Invalid arguments');

		file = f;
	} else if (!hasCommit && commitOrRefs.rhs === '') {
		return openChangesWithWorking(file, { repoPath: commitOrRefs.repoPath, ref: commitOrRefs.lhs }, options);
	}

	options = { preserveFocus: true, preview: false, ...options };

	if (file.status === 'A' && hasCommit) {
		const commit = await getCommitForFile(commitOrRefs, file);
		void executeCommand<DiffWithPreviousCommandArgs>('gitlens.diffWithPrevious:command', {
			commit: commit,
			showOptions: options,
		});

		return;
	}

	let refs: RefRange = hasCommit
		? {
				repoPath: commitOrRefs.repoPath,
				rhs: commitOrRefs.sha,
				// Don't need to worry about verifying the previous sha, as the DiffWith command will
				lhs: commitOrRefs.unresolvedPreviousSha,
			}
		: commitOrRefs;

	// For an untracked file in a stash, the content lives in the `^3` parent and there is no
	// "previous" content to compare against. Build a fresh refs object so we don't mutate a
	// caller-owned RefRange in the !hasCommit path.
	if (file.status === '?') {
		refs = { repoPath: refs.repoPath, lhs: deletedOrMissing, rhs: `${refs.rhs}^3` };
	}

	const rhsUri = GitUri.fromFile(file, refs.repoPath);
	const lhsUri =
		file.status === 'R' || file.status === 'C' ? GitUri.fromFile(file, refs.repoPath, refs.lhs, true) : rhsUri;

	void (await executeCommand<DiffWithCommandArgs>('gitlens.diffWith', {
		repoPath: refs.repoPath,
		lhs: { uri: lhsUri, sha: refs.lhs, title: options?.lhsTitle },
		rhs: { uri: rhsUri, sha: refs.rhs, title: options?.rhsTitle },
		showOptions: options,
	}));
}

export function openChangesInDiffTool(file: string | GitFile, commit: GitCommit, tool?: string): Promise<void>;
export function openChangesInDiffTool(file: GitFile, ref: Ref, tool?: string): Promise<void>;
export async function openChangesInDiffTool(
	file: string | GitFile,
	commitOrRef: GitCommit | Ref,
	tool?: string,
): Promise<void> {
	if (typeof file === 'string') {
		if (!GitCommit.is(commitOrRef)) throw new Error('Invalid arguments');

		const f = await findCommitFile(commitOrRef, file);
		if (f == null) throw new Error('Invalid arguments');

		file = f;
	}

	return Container.instance.git
		.getRepositoryService(commitOrRef.repoPath)
		.diff.openDiffTool?.(GitUri.fromFile(file, file.repoPath ?? commitOrRef.repoPath), {
			ref1: isUncommitted(commitOrRef.ref) ? '' : `${commitOrRef.ref}^`,
			ref2: isUncommitted(commitOrRef.ref) ? '' : commitOrRef.ref,
			staged: isUncommittedStaged(commitOrRef.ref) || file.indexStatus != null,
			tool: tool,
		});
}

export async function openChangesWithWorking(
	file: string | Uri | GitFile,
	commit: GitCommit,
	options?: TextDocumentShowOptions & { lhsTitle?: string },
): Promise<void>;
export async function openChangesWithWorking(
	file: GitFile,
	ref: Ref,
	options?: TextDocumentShowOptions & { lhsTitle?: string },
): Promise<void>;
export async function openChangesWithWorking(
	file: string | Uri | GitFile,
	commitOrRef: GitCommit | Ref,
	options?: TextDocumentShowOptions & { lhsTitle?: string },
): Promise<void> {
	if (typeof file === 'string' || file instanceof Uri) {
		if (!GitCommit.is(commitOrRef)) throw new Error('Invalid arguments');

		const f = await findCommitFile(commitOrRef, file);
		if (f == null) throw new Error('Invalid arguments');

		file = f;
	}

	if (file.status === 'D') return;

	let ref;
	if (GitCommit.is(commitOrRef)) {
		ref = {
			repoPath: commitOrRef.repoPath,
			ref: commitOrRef.sha,
		};
	} else {
		ref = commitOrRef;
	}

	options = { preserveFocus: true, preview: false, ...options };

	void (await executeEditorCommand<DiffWithWorkingCommandArgs>('gitlens.diffWithWorking:command', undefined, {
		uri: GitUri.fromFile(file, ref.repoPath, ref.ref),
		showOptions: options,
		lhsTitle: options?.lhsTitle,
	}));
}

export async function openComparisonChanges(
	container: Container,
	refs: RefRange,
	options?: ShowOptions,
): Promise<void> {
	refs.lhs = refs.lhs || 'HEAD';
	refs.rhs = refs.rhs || 'HEAD';

	const { files } = await getAheadBehindFilesQuery(
		container,
		refs.repoPath,
		createRevisionRange(refs.lhs, refs.rhs, '...'),
		refs.rhs === '',
	);

	await openMultipleChanges(container, files ?? [], refs, false, options);
}

export async function openDirectoryCompare(
	repoPath: string,
	ref: string,
	ref2: string | undefined,
	tool?: string,
): Promise<void> {
	return Container.instance.git.getRepositoryService(repoPath).diff.openDirectoryCompare?.(ref, ref2, tool);
}

export async function openDirectoryCompareWithPrevious(ref: Ref | GitCommit): Promise<void> {
	return openDirectoryCompare(ref.repoPath, ref.ref, `${ref.ref}^`);
}

export async function openDirectoryCompareWithWorking(ref: Ref | GitCommit): Promise<void> {
	return openDirectoryCompare(ref.repoPath, ref.ref, undefined);
}

export async function openFolderCompare(
	container: Container,
	pathOrUri: string | Uri,
	refs: RefRange,
	options?: TextDocumentShowOptions,
): Promise<void> {
	let comparison;
	if (refs.lhs === '') {
		debugger;
		throw new Error('Cannot get files for comparisons of a ref with working tree');
	} else if (refs.rhs === '') {
		comparison = refs.lhs;
	} else {
		comparison = `${refs.lhs}..${refs.rhs}`;
	}

	const svc = container.git.getRepositoryService(refs.repoPath);
	const relativePath = svc.getRelativePath(pathOrUri, refs.repoPath);

	const files = await svc.diff.getDiffStatus(comparison, undefined, { path: relativePath });
	if (files == null) {
		void window.showWarningMessage(
			`No changes in '${relativePath}' between ${shortenRevision(refs.lhs, {
				strings: { working: 'Working Tree' },
			})} ${GlyphChars.ArrowLeftRightLong} ${shortenRevision(refs.rhs, {
				strings: { working: 'Working Tree' },
			})}`,
		);
		return;
	}

	const title = `Changes in ${relativePath} between ${shortenRevision(refs.lhs, {
		strings: { working: 'Working Tree' },
	})} ${GlyphChars.ArrowLeftRightLong} ${shortenRevision(refs.rhs, { strings: { working: 'Working Tree' } })}`;

	return openMultipleChanges(container, files, refs, false, { ...options, title: title });
}

export async function openFile(uri: Uri, options?: TextDocumentShowOptions): Promise<void>;
export async function openFile(
	file: string | GitFile,
	ref: GitRevisionReference,
	options?: TextDocumentShowOptions,
): Promise<void>;
export async function openFile(
	fileOrUri: string | GitFile | Uri,
	refOrOptions?: GitRevisionReference | TextDocumentShowOptions,
	options?: TextDocumentShowOptions,
): Promise<void> {
	let uri;
	if (fileOrUri instanceof Uri) {
		uri = fileOrUri;
		options = refOrOptions as TextDocumentShowOptions;
	} else {
		const ref = refOrOptions as GitRevisionReference;

		uri = GitUri.fromFile(fileOrUri, ref.repoPath, ref.ref);
		// An untracked file in a stash typically has no working-tree copy to open. Mirror the
		// warning surfaced by `gitlens.openWorkingFile:command` for the same case so the click
		// isn't a silent no-op; users wanting the stashed content can pick "Open File at Revision".
		// Skip this guard for WIP refs — untracked files in the working tree are real files.
		if (typeof fileOrUri !== 'string' && fileOrUri.status === '?' && !isUncommitted(ref.ref)) {
			void window.showWarningMessage('Unable to open working file. File could not be found in the working tree');
			return;
		}
	}

	options = { preserveFocus: true, preview: false, ...options };

	void (await executeEditorCommand<OpenWorkingFileCommandArgs>('gitlens.openWorkingFile:command', undefined, {
		uri: uri,
		showOptions: options,
	}));
}

export async function openFileAtRevision(
	revisionUri: Uri,
	options?: TextDocumentShowOptions & { annotationType?: FileAnnotationType; line?: number },
): Promise<void>;
export async function openFileAtRevision(
	file: string | GitFile,
	commit: GitCommit,
	options?: TextDocumentShowOptions & { annotationType?: FileAnnotationType; line?: number },
): Promise<void>;
export async function openFileAtRevision(
	fileOrRevisionUri: string | GitFile | Uri,
	commitOrOptions?: GitCommit | TextDocumentShowOptions,
	options?: TextDocumentShowOptions & { annotationType?: FileAnnotationType; line?: number },
): Promise<void> {
	let uri: Uri;
	if (fileOrRevisionUri instanceof Uri) {
		if (GitCommit.is(commitOrOptions)) throw new Error('Invalid arguments');

		uri = fileOrRevisionUri;
		options = commitOrOptions;
	} else {
		if (!GitCommit.is(commitOrOptions)) throw new Error('Invalid arguments');

		const commit = commitOrOptions;

		let file;
		if (typeof fileOrRevisionUri === 'string') {
			const f = await findCommitFile(commit, fileOrRevisionUri);
			if (f == null) throw new Error('Invalid arguments');

			file = f;
		} else {
			file = fileOrRevisionUri;
		}

		uri = Container.instance.git
			.getRepositoryService(commit.repoPath)
			.getRevisionUri(
				file.status === 'D'
					? ((await GitCommit.getPreviousSha(commit)) ?? deletedOrMissing)
					: file.status === '?'
						? `${commit.sha}^3`
						: commit.sha,
				file,
			);
	}

	const { annotationType, line, ...opts }: Exclude<typeof options, undefined> = {
		preserveFocus: true,
		preview: false,
		...options,
	};

	if (line != null && line !== 0) {
		opts.selection = new Range(line, 0, line, 0);
	}

	const gitUri = await GitUri.fromUri(uri);

	let editor: TextEditor | undefined;
	try {
		editor = await getOrOpenTextEditor(uri, { throwOnError: true, ...opts });
	} catch (ex) {
		if (!ex?.message?.includes('Unable to resolve nonexistent file')) {
			void window.showErrorMessage(`Unable to open '${gitUri.relativePath}' in revision '${gitUri.sha}'`);
			return;
		}

		const picked = await showRevisionFilesPicker(
			Container.instance,
			createReference(gitUri.sha!, gitUri.repoPath!),
			{
				ignoreFocusOut: true,
				initialPath: gitUri.relativePath,
				title: `Open File at Revision \u2022 Unable to open '${gitUri.relativePath}'`,
				placeholder: 'Choose a file revision to open',
				keyboard: {
					keys: ['right', 'alt+right', 'ctrl+right'],
					onDidPressKey: async (_key, uri) => {
						await getOrOpenTextEditor(uri, { ...opts, preserveFocus: true, preview: true });
					},
				},
			},
		);
		if (picked == null) return;

		editor = await getOrOpenTextEditor(picked?.uri, opts);
	}

	if (annotationType != null && editor != null) {
		void (await Container.instance.fileAnnotations.show(editor, annotationType, {
			selection: { line: line },
		}));
	}
}

export async function openFileOnRemote(uri: Uri): Promise<void>;
export async function openFileOnRemote(file: string | GitFile, ref: GitRevisionReference): Promise<void>;
export async function openFileOnRemote(fileOrUri: string | GitFile | Uri, ref?: GitRevisionReference): Promise<void> {
	let uri;
	if (fileOrUri instanceof Uri) {
		uri = fileOrUri;
	} else {
		if (ref == null) throw new Error('Invalid arguments');

		uri = GitUri.fromFile(fileOrUri, ref.repoPath, ref.ref);
		// If the file is `?` (untracked), then this must be an untracked file in a stash, so just return
		if (typeof fileOrUri !== 'string' && fileOrUri.status === '?') return;
	}

	void (await executeCommand<[Uri, OpenFileOnRemoteCommandArgs]>('gitlens.openFileOnRemote', uri, {
		sha: ref?.ref,
	}));
}

export async function openFiles(commit: GitCommit, options?: TextDocumentShowOptions): Promise<void>;
export async function openFiles(files: GitFile[], ref: Ref, options?: TextDocumentShowOptions): Promise<void>;
export async function openFiles(
	commitOrFiles: GitCommit | GitFile[],
	refOrOptions: Ref | TextDocumentShowOptions | undefined,
	maybeOptions?: TextDocumentShowOptions,
): Promise<void> {
	const { files, ref, options } = await getChangesRefArgs(commitOrFiles, refOrOptions, maybeOptions);

	if (
		!(await confirmOpenIfNeeded(files, {
			message: `Are you sure you want to open each of the ${files.length} files?`,
			confirmButton: 'Open Files',
			threshold: filesOpenThreshold,
		}))
	) {
		return;
	}

	const svc = Container.instance.git.getRepositoryService(ref.repoPath);
	const uris: Uri[] = (
		await Promise.all(files.map(file => svc.getWorkingUri(GitUri.fromFile(file, ref.repoPath, ref.ref))))
	).filter(<T>(u?: T): u is T => Boolean(u));
	openTextEditors(uris, options);
}

export async function openFilesAtRevision(commit: GitCommit, options?: TextDocumentShowOptions): Promise<void>;
export async function openFilesAtRevision(
	files: GitFile[],
	refs: RefRange,
	options?: TextDocumentShowOptions,
): Promise<void>;
export async function openFilesAtRevision(
	commitOrFiles: GitCommit | GitFile[],
	refOrOptions: RefRange | TextDocumentShowOptions | undefined,
	maybeOptions?: TextDocumentShowOptions,
): Promise<void> {
	const { files, refs, options } = await getChangesRefsArgs(commitOrFiles, refOrOptions, maybeOptions);

	if (
		!(await confirmOpenIfNeeded(files, {
			message: `Are you sure you want to open each of the ${files.length} file revisions?`,
			confirmButton: 'Open Revisions',
			threshold: filesOpenThreshold,
		}))
	) {
		return;
	}

	const svc = Container.instance.git.getRepositoryService(refs.repoPath);
	openTextEditors(
		files.map(file => svc.getRevisionUri(file.status === 'D' ? refs.lhs : refs.rhs, file)),
		options,
	);
}

export async function restoreFile(
	file: string | GitFile,
	revision: GitRevisionReference,
	previous?: boolean,
): Promise<void> {
	let path;
	let rev;
	if (typeof file === 'string') {
		path = file;
		rev = previous ? `${revision.ref}^` : revision.ref;
	} else {
		path = file.path;
		if (file.status === 'D') {
			// If the file is deleted, check to see if it exists, if so, restore it from the previous commit, otherwise restore it from the current commit
			const uri = GitUri.fromFile(file, revision.repoPath);
			try {
				await workspace.fs.stat(uri);
				rev = `${revision.ref}^`;
			} catch {
				rev = previous ? `${revision.ref}^` : revision.ref;
			}
		} else if (file.status === '?') {
			rev = `${revision.ref}^3`;
		} else {
			rev = previous ? `${revision.ref}^` : revision.ref;
		}
	}

	try {
		await Container.instance.git.getRepositoryService(revision.repoPath).ops?.restore(path, { ref: rev });
	} catch (ex) {
		void showGitErrorMessage(
			ex,
			`Unable to restore '${path}' from revision '${getReferenceLabel(revision, {
				icon: false,
				capitalize: false,
			})}': ${ex.message}`,
		);
	}
}

export function revealCommit(commit: GitRevisionReference, options?: RevealOptions): Promise<ViewNode | undefined> {
	return Container.instance.views.revealCommit(commit, options);
}

export async function showDetailsQuickPick(commit: GitCommit, uri?: Uri): Promise<void>;
export async function showDetailsQuickPick(commit: GitCommit, file?: string | GitFile): Promise<void>;
export async function showDetailsQuickPick(commit: GitCommit, fileOrUri?: string | GitFile | Uri): Promise<void> {
	if (fileOrUri == null) {
		void (await executeCommand<ShowQuickCommitCommandArgs>('gitlens.showQuickCommitDetails', { commit: commit }));
		return;
	}

	let uri;
	if (fileOrUri instanceof Uri) {
		uri = fileOrUri;
	} else {
		uri = GitUri.fromFile(fileOrUri, commit.repoPath, commit.ref);
	}

	void (await executeCommand<[Uri, ShowQuickCommitFileCommandArgs]>('gitlens.showQuickCommitFileDetails', uri, {
		sha: commit.sha,
		commit: commit,
	}));
}

export function showCommitInDetailsView(
	commit: GitRevisionReference | GitCommit,
	options?: { pin?: boolean; preserveFocus?: boolean; preserveVisibility?: boolean },
): Promise<void> {
	const { preserveFocus, ...opts } = { ...options, commit: commit };
	return Container.instance.views.commitDetails.show({ preserveFocus: preserveFocus }, opts);
}

export async function showCommitInGraph(
	commit: GitRevisionReference | GitCommit,
	options?: { preserveFocus?: boolean; source?: Source },
): Promise<void> {
	void (await executeCommand<ShowInCommitGraphCommandArgs>('gitlens.showInCommitGraph', {
		ref: getReferenceFromRevision(commit),
		preserveFocus: options?.preserveFocus,
		source: options?.source,
	}));
}

export async function explainCommit(
	commit: GitRevisionReference | GitCommit,
	options: { source: Source },
): Promise<void> {
	void (await executeCommand<ExplainCommitCommandArgs>('gitlens.ai.explainCommit', {
		repoPath: commit.repoPath,
		rev: commit.ref,
		source: { ...options?.source, context: { type: 'commit' } },
	}));
}

export async function openOnlyChangedFiles(container: Container, commit: GitCommit): Promise<void>;
export async function openOnlyChangedFiles(container: Container, files: GitFile[]): Promise<void>;
export async function openOnlyChangedFiles(_container: Container, commitOrFiles: GitCommit | GitFile[]): Promise<void> {
	let files;
	if (GitCommit.is(commitOrFiles)) {
		if (!commitOrFiles.hasFullDetails()) {
			await GitCommit.ensureFullDetails(commitOrFiles);
		}

		files = commitOrFiles.fileset?.files ?? [];
	} else {
		files = commitOrFiles.map(
			f =>
				new GitFileChange(
					f.repoPath!,
					f.path,
					f.status,
					joinUriPath(fileUri(f.repoPath!), f.path),
					f.originalPath,
					f.originalPath != null ? joinUriPath(fileUri(f.repoPath!), f.originalPath) : undefined,
					undefined,
					undefined,
					undefined,
					undefined,
					f.submodule != null ? '160000' : undefined,
					f.submodule,
				),
		);
	}

	if (
		!(await confirmOpenIfNeeded(files, {
			message: `Are you sure you want to open each of the ${files.length} files?`,
			confirmButton: 'Open Files',
			threshold: 10,
		}))
	) {
		return;
	}

	void (await executeCommand<OpenOnlyChangedFilesCommandArgs>('gitlens.openOnlyChangedFiles', {
		uris: files.filter(f => f.status !== 'D').map(f => f.uri),
	}));
}

export async function undoCommit(
	container: Container,
	commit: GitRevisionReference,
	options?: { onBeforeReset?: (message: string) => void },
): Promise<string | undefined> {
	const svc = container.git.getRepositoryService(commit.repoPath);
	if (svc.ops == null) return undefined;

	const headCommit = await svc.commits.getCommit('HEAD');

	if (headCommit?.sha !== commit.ref) {
		void window.showWarningMessage(
			`Commit ${getReferenceLabel(commit, {
				capitalize: true,
				icon: false,
			})} cannot be undone, because it is no longer the most recent commit.`,
		);

		return undefined;
	}

	// Check for uncommitted changes before prompting
	const hasChanges = await svc.status.hasWorkingChanges();
	if (hasChanges) {
		const confirm = { title: 'Undo Commit' };
		const cancel = { title: 'Cancel', isCloseAffordance: true };
		// Label from `headCommit` (just confirmed to be HEAD) rather than the passed `commit` ref —
		// its summary is the exact, un-emojified subject straight from git, so the dialog matches the
		// commit regardless of how the caller's ref message was formatted (e.g. graph display emoji).
		const exactRef = createReference(headCommit.ref, headCommit.repoPath, {
			refType: 'revision',
			message: headCommit.message ?? headCommit.summary,
		});
		const result = await window.showWarningMessage(
			`You have uncommitted changes in the working tree.\n\nDo you still want to undo ${getReferenceLabel(
				exactRef,
				{
					capitalize: false,
					icon: false,
				},
			)}?`,
			{ modal: true },
			confirm,
			cancel,
		);

		if (result !== confirm) return undefined;
	}

	try {
		await GitCommit.ensureFullDetails(headCommit);
	} catch {}

	const message = headCommit.message ?? headCommit.summary ?? '';
	options?.onBeforeReset?.(message);

	await svc.ops.reset('HEAD~1', { mode: 'soft' });

	const scmRepo = await svc.getScmRepository();
	if (scmRepo != null && message) {
		scmRepo.inputBox.value = message;
	}

	return message;
}

async function confirmOpenIfNeeded(
	items: readonly unknown[],
	options: { cancelButton?: string; confirmButton?: string; message: string; threshold: number },
): Promise<boolean> {
	if (items.length <= options.threshold) return true;

	const confirm = { title: options.confirmButton ?? 'Open' };
	const cancel = { title: options.cancelButton ?? 'Cancel', isCloseAffordance: true };
	const result = await window.showWarningMessage(options.message, { modal: true }, confirm, cancel);
	return result === confirm;
}

async function getChangesRefArgs(
	commitOrFiles: GitCommit | GitFile[],
	refOrOptions: Ref | TextDocumentShowOptions | undefined,
	options?: TextDocumentShowOptions,
): Promise<{
	commit?: GitCommit;
	files: readonly GitFile[];
	options: TextDocumentShowOptions | undefined;
	ref: Ref;
}> {
	if (!GitCommit.is(commitOrFiles)) {
		return {
			files: commitOrFiles,
			options: options,
			ref: refOrOptions as Ref,
		};
	}

	if (commitOrFiles.fileset?.files == null) {
		await GitCommit.ensureFullDetails(commitOrFiles);
	}

	return {
		commit: commitOrFiles,
		files: commitOrFiles.fileset?.files ?? [],
		options: refOrOptions as TextDocumentShowOptions | undefined,
		ref: {
			repoPath: commitOrFiles.repoPath,
			ref: commitOrFiles.sha,
		},
	};
}

async function getChangesRefsArgs(
	commitOrFiles: GitCommit | GitFile[],
	refsOrOptions: RefRange | TextDocumentShowOptions | undefined,
	options?: TextDocumentShowOptions,
): Promise<{
	commit?: GitCommit;
	files: readonly GitFile[];
	options: TextDocumentShowOptions | undefined;
	refs: RefRange;
}> {
	if (!GitCommit.is(commitOrFiles)) {
		return {
			files: commitOrFiles,
			options: options,
			refs: refsOrOptions as RefRange,
		};
	}

	if (commitOrFiles.fileset?.files == null) {
		await GitCommit.ensureFullDetails(commitOrFiles);
	}

	return {
		commit: commitOrFiles,
		files: commitOrFiles.fileset?.files ?? [],
		options: refsOrOptions as TextDocumentShowOptions | undefined,
		refs: {
			repoPath: commitOrFiles.repoPath,
			rhs: commitOrFiles.sha,
			lhs:
				commitOrFiles.resolvedPreviousSha ??
				(await GitCommit.getPreviousSha(commitOrFiles)) ??
				commitOrFiles.unresolvedPreviousSha,
		},
	};
}

async function getCommitChangesArgs(
	commit: GitCommit,
	filter?: (file: GitFileChange) => boolean,
): Promise<{ files: readonly GitFile[]; refs: RefRange }> {
	if (commit.fileset?.files == null) {
		await GitCommit.ensureFullDetails(commit);
	}

	return {
		files: (filter != null ? commit.fileset?.files?.filter(filter) : commit.fileset?.files) ?? [],
		refs: {
			repoPath: commit.repoPath,
			rhs: commit.sha,
			lhs: commit.resolvedPreviousSha ?? (await GitCommit.getPreviousSha(commit)) ?? commit.unresolvedPreviousSha,
		},
	};
}

export async function getOrderedComparisonRefs(
	container: Container,
	repoPath: string,
	refA: string,
	refB: string,
): Promise<[string, string]> {
	const commitsSvc = container.git.getRepositoryService(repoPath).commits;

	// Check the ancestry of refA and refB to determine which is the "newer" one
	const ancestor = await commitsSvc.isAncestorOf(refA, refB);
	// If refB is an ancestor of refA, compare refA to refB (as refA is "newer")
	if (ancestor) return [refB, refA];

	const ancestor2 = await commitsSvc.isAncestorOf(refB, refA);
	// If refA is an ancestor of refB, compare refB to refA (as refB is "newer")
	if (ancestor2) return [refA, refB];

	const [commitRefAResult, commitRefBResult] = await Promise.allSettled([
		commitsSvc.getCommit(refA),
		commitsSvc.getCommit(refB),
	]);

	const commitRefA = getSettledValue(commitRefAResult);
	const commitRefB = getSettledValue(commitRefBResult);

	if (commitRefB != null && commitRefA != null && getCommitDate(commitRefB) > getCommitDate(commitRefA)) {
		// If refB is "newer", compare refB to refA
		return [refB, refA];
	}

	// If refA is "newer", compare refA to refB
	return [refA, refB];
}
