import type { CancellationToken, Command, Selection } from 'vscode';
import { MarkdownString, ThemeColor, ThemeIcon, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode';
import type { GitBranch } from '@gitlens/git/models/branch.js';
import { GitCommit } from '@gitlens/git/models/commit.js';
import type { GitFile } from '@gitlens/git/models/file.js';
import type { GitPausedOperationStatus } from '@gitlens/git/models/pausedOperationStatus.js';
import type { GitRevisionReference } from '@gitlens/git/models/reference.js';
import type { DiffRange } from '@gitlens/git/providers/types.js';
import { getGitFileStatusIcon } from '@gitlens/git/utils/fileStatus.utils.js';
import { getConflictIncomingRef, resolveConflictFilePaths } from '@gitlens/git/utils/pausedOperationStatus.utils.js';
import { joinPaths } from '@gitlens/utils/path.js';
import { getSettledValue, pauseOnCancelOrTimeoutMapTuplePromise } from '@gitlens/utils/promise.js';
import type { DiffWithCommandArgs } from '../../commands/diffWith.js';
import type { DiffWithPreviousCommandArgs } from '../../commands/diffWithPrevious.js';
import type { Colors } from '../../constants.colors.js';
import type { Container } from '../../container.js';
import { CommitFormatter } from '../../git/formatters/commitFormatter.js';
import { StatusFileFormatter } from '../../git/formatters/statusFormatter.js';
import { GitUri } from '../../git/gitUri.js';
import {
	getCommitAssociatedPullRequest,
	getCommitAuthorAvatarUri,
	getCommitEnrichedAutolinks,
	getCommitForFile,
} from '../../git/utils/-webview/commit.utils.js';
import { remoteSupportsIntegration } from '../../git/utils/-webview/remote.utils.js';
import { toAbortSignal } from '../../system/-webview/cancellation.js';
import { createCommand } from '../../system/-webview/command.js';
import { configuration } from '../../system/-webview/configuration.js';
import { editorLineToDiffRange, selectionToDiffRange } from '../../system/-webview/vscode/range.js';
import type { FileHistoryView } from '../fileHistoryView.js';
import type { LineHistoryView } from '../lineHistoryView.js';
import type { ViewsWithCommits } from '../viewBase.js';
import { createViewDecorationUri } from '../viewDecorationProvider.js';
import type { ViewNode } from './abstract/viewNode.js';
import { ContextValues } from './abstract/viewNode.js';
import { ViewRefFileNode } from './abstract/viewRefNode.js';
import { MergeConflictChangesNode } from './mergeConflictChangesNode.js';

export class FileRevisionAsCommitNode extends ViewRefFileNode<
	'file-commit',
	ViewsWithCommits | FileHistoryView | LineHistoryView
> {
	constructor(
		view: ViewsWithCommits | FileHistoryView | LineHistoryView,
		parent: ViewNode,
		file: GitFile,
		public commit: GitCommit,
		private readonly _options: {
			branch?: GitBranch;
			getBranchAndTagTips?: (sha: string, options?: { compact?: boolean }) => string | undefined;
			selection?: Selection;
			unpublished?: boolean;
		} = {},
	) {
		super('file-commit', GitUri.fromFile(file, commit.repoPath, commit.sha), view, parent, file);
	}

	override toClipboard(): string {
		return `${this.commit.shortSha}: ${this.commit.summary}`;
	}

	get isTip(): boolean {
		return (this._options.branch?.current && this._options.branch.sha === this.commit.ref) ?? false;
	}

	get ref(): GitRevisionReference {
		return this.commit;
	}

	private _pausedStatus: Promise<GitPausedOperationStatus | undefined> | undefined;
	private getPausedStatus(): Promise<GitPausedOperationStatus | undefined> {
		this._pausedStatus ??=
			this.view.container.git
				.getRepositoryService(this.commit.repoPath)
				.pausedOps?.getPausedOperationStatus?.() ?? Promise.resolve(undefined);
		return this._pausedStatus;
	}

	private _conflictPaths:
		| { mergeBasePath: string; headPath: string; incomingPath: string; incomingRef: string }
		| undefined;
	private async resolveConflictPaths(): Promise<typeof this._conflictPaths> {
		if (this._conflictPaths != null) return this._conflictPaths;

		const status = await this.getPausedStatus();
		if (status?.mergeBase == null) return undefined;

		const svc = this.view.container.git.getRepositoryService(this.repoPath);
		const incomingRef = getConflictIncomingRef(status);

		const [currentFilesResult, incomingFilesResult] = await Promise.allSettled([
			svc.diff.getDiffStatus(status.mergeBase, 'HEAD', { renameLimit: 0 }),
			incomingRef != null ? svc.diff.getDiffStatus(status.mergeBase, incomingRef, { renameLimit: 0 }) : undefined,
		]);

		const currentFiles = getSettledValue(currentFilesResult);
		const incomingFiles = getSettledValue(incomingFilesResult);

		const currentPaths = resolveConflictFilePaths(currentFiles, incomingFiles, this.file.path);
		const incomingPaths = resolveConflictFilePaths(incomingFiles, currentFiles, this.file.path);

		this._conflictPaths = {
			mergeBasePath: currentPaths.lhsPath,
			headPath: currentPaths.rhsPath,
			incomingPath: incomingPaths.rhsPath,
			incomingRef: incomingRef ?? 'MERGE_HEAD',
		};
		return this._conflictPaths;
	}

	async getChildren(): Promise<ViewNode[]> {
		if (!this.commit.file?.hasConflicts) return [];

		const pausedOpStatus = await this.getPausedStatus();
		if (pausedOpStatus == null) return [];

		const paths = await this.resolveConflictPaths();

		const currentPaths = paths != null ? { lhsPath: paths.mergeBasePath, rhsPath: paths.headPath } : undefined;
		const incomingPaths = paths != null ? { lhsPath: paths.mergeBasePath, rhsPath: paths.incomingPath } : undefined;

		const currentFile = this.createResolvedFile(currentPaths);
		const incomingFile = this.createResolvedFile(incomingPaths);

		return [
			new MergeConflictChangesNode(
				this.view,
				this,
				pausedOpStatus,
				currentFile,
				'current',
				currentPaths?.lhsPath,
				this.file.path,
			),
			new MergeConflictChangesNode(
				this.view,
				this,
				pausedOpStatus,
				incomingFile,
				'incoming',
				incomingPaths?.lhsPath,
				this.file.path,
			),
		];
	}

	private createResolvedFile(paths: { lhsPath: string; rhsPath: string } | undefined): GitFile {
		if (paths == null || paths.rhsPath === this.file.path) return this.file;

		return { ...this.file, path: paths.rhsPath, originalPath: paths.lhsPath };
	}

	async getTreeItem(): Promise<TreeItem> {
		if (this.commit.file == null) {
			// Try to get the commit directly from the multi-file commit
			const commit = await getCommitForFile(this.commit, this.file);
			if (commit == null) {
				const log = await this.view.container.git
					.getRepositoryService(this.repoPath)
					.commits.getLogForPath(this.file.path, this.commit.sha, { isFolder: false, limit: 1 });
				if (log != null) {
					this.commit = log.commits.get(this.commit.sha) ?? this.commit;
				}
			} else {
				this.commit = commit;
			}
		}

		const item = new TreeItem(
			CommitFormatter.fromTemplate(this.view.config.formats.commits.label, this.commit, {
				dateFormat: configuration.get('defaultDateFormat'),
				getBranchAndTagTips: (sha: string) => this._options.getBranchAndTagTips?.(sha, { compact: true }),
				messageTruncateAtNewLine: true,
			}),
			this.commit.file?.hasConflicts ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.None,
		);

		item.contextValue = this.contextValue;

		item.description = CommitFormatter.fromTemplate(this.view.config.formats.commits.description, this.commit, {
			dateFormat: configuration.get('defaultDateFormat'),
			getBranchAndTagTips: (sha: string) => this._options.getBranchAndTagTips?.(sha, { compact: true }),
			messageTruncateAtNewLine: true,
		});

		item.resourceUri = createViewDecorationUri('commit-file', { status: this.file.status });

		if (!this.commit.isUncommitted && this.view.config.avatars) {
			item.iconPath = this._options.unpublished
				? new ThemeIcon('arrow-up', new ThemeColor('gitlens.unpublishedCommitIconColor' satisfies Colors))
				: await getCommitAuthorAvatarUri(this.commit, {
						defaultStyle: configuration.get('defaultGravatarsStyle'),
					});
		}

		if (item.iconPath == null) {
			const icon = getGitFileStatusIcon(this.file.status);
			item.iconPath = {
				dark: Uri.file(this.view.container.context.asAbsolutePath(joinPaths('images', 'dark', icon))),
				light: Uri.file(this.view.container.context.asAbsolutePath(joinPaths('images', 'light', icon))),
			};
		}

		item.command = this.getCommand();

		return item;
	}

	protected get contextValue(): string {
		const submodule = this.file.submodule != null ? '+submodule' : '';
		if (!this.commit.isUncommitted) {
			return `${ContextValues.File}+committed${this._options.branch?.current ? '+current' : ''}${
				this.isTip ? '+HEAD' : ''
			}${this._options.unpublished ? '+unpublished' : ''}${submodule}`;
		}

		return this.commit.file?.hasConflicts
			? `${ContextValues.File}+conflicted${submodule}`
			: this.commit.isUncommittedStaged
				? `${ContextValues.File}+staged${submodule}`
				: `${ContextValues.File}+unstaged${submodule}`;
	}

	override getCommand(): Command | undefined {
		let range: DiffRange;
		if (this.commit.lines.length) {
			// TODO@eamodio should the endLine be the last line of the commit?
			range = {
				startLine: this.commit.lines[0].line,
				startCharacter: 1,
				endLine: this.commit.lines[0].line,
				endCharacter: 1,
			};
		} else {
			range = this.commit.file?.range ?? selectionToDiffRange(this._options?.selection);
		}

		if (this.commit.file?.hasConflicts) {
			const incomingRef = this._conflictPaths?.incomingRef ?? 'MERGE_HEAD';
			const incomingPath = this._conflictPaths?.incomingPath ?? this.file.path;
			const headPath = this._conflictPaths?.headPath ?? this.file.path;

			return createCommand<[DiffWithCommandArgs]>('gitlens.diffWith', 'Open Changes', {
				lhs: {
					sha: incomingRef,
					uri: GitUri.fromFile(incomingPath, this.repoPath, undefined, true),
				},
				rhs: {
					sha: 'HEAD',
					uri: GitUri.fromFile(headPath, this.repoPath),
				},
				repoPath: this.repoPath,
				range: editorLineToDiffRange(0),
				showOptions: { preserveFocus: false, preview: false },
			});
		}

		return createCommand<[undefined, DiffWithPreviousCommandArgs]>(
			'gitlens.diffWithPrevious:views',
			'Open Changes with Previous Revision',
			undefined,
			{
				commit: this.commit,
				uri: GitUri.fromFile(this.file, this.commit.repoPath),
				range: range,
				showOptions: { preserveFocus: true, preview: true },
			},
		);
	}

	override async resolveTreeItem(item: TreeItem, token: CancellationToken): Promise<TreeItem> {
		if (this.commit.file?.hasConflicts) {
			await this.resolveConflictPaths();
			if (token.isCancellationRequested) return item;
			item.command = this.getCommand();
		}
		item.tooltip ??= await this.getTooltip(token);
		return item;
	}

	async getConflictBaseUri(): Promise<Uri | undefined> {
		if (!this.commit.file?.hasConflicts) return undefined;

		const paths = await this.resolveConflictPaths();
		const status = await this.getPausedStatus();
		const mergeBase =
			status?.mergeBase ??
			(await this.view.container.git.getRepositoryService(this.repoPath).refs.getMergeBase('MERGE_HEAD', 'HEAD'));

		const filePath = paths?.mergeBasePath ?? this.file.path;
		return GitUri.fromFile(filePath, this.repoPath, mergeBase ?? 'HEAD');
	}

	private async getTooltip(cancellation: CancellationToken) {
		const tooltip = await getFileRevisionAsCommitTooltip(
			this.view.container,
			this.commit,
			this.file,
			this.view.config.formats.commits.tooltipWithStatus,
			{
				getBranchAndTagTips: this._options.getBranchAndTagTips,
				unpublished: this._options.unpublished,
				cancellation: cancellation,
			},
		);

		const markdown = new MarkdownString(tooltip, true);
		markdown.supportHtml = true;
		markdown.isTrusted = true;
		return markdown;
	}
}

export async function getFileRevisionAsCommitTooltip(
	container: Container,
	commit: GitCommit,
	file: GitFile,
	tooltipWithStatusFormat: string,
	options?: {
		cancellation?: CancellationToken;
		getBranchAndTagTips?: (sha: string, options?: { compact?: boolean }) => string | undefined;
		unpublished?: boolean;
	},
): Promise<string | undefined> {
	const [remotesResult, _] = await Promise.allSettled([
		container.git
			.getRepositoryService(commit.repoPath)
			.remotes.getBestRemotesWithProviders(toAbortSignal(options?.cancellation)),
		commit.message == null ? GitCommit.ensureFullDetails(commit) : undefined,
	]);

	if (options?.cancellation?.isCancellationRequested) return undefined;

	const remotes = getSettledValue(remotesResult, []);
	const [remote] = remotes;

	let enrichedAutolinks;
	let pr;

	if (remote != null && remoteSupportsIntegration(remote)) {
		const [enrichedAutolinksResult, prResult] = await Promise.allSettled([
			pauseOnCancelOrTimeoutMapTuplePromise(
				getCommitEnrichedAutolinks(commit.repoPath, commit.message, commit.summary, remote),
				toAbortSignal(options?.cancellation),
			),
			getCommitAssociatedPullRequest(commit.repoPath, commit.sha, remote),
		]);

		const enrichedAutolinksMaybeResult = getSettledValue(enrichedAutolinksResult);
		if (!enrichedAutolinksMaybeResult?.paused) {
			enrichedAutolinks = enrichedAutolinksMaybeResult?.value;
		}
		pr = getSettledValue(prResult);
	}

	const status = StatusFileFormatter.fromTemplate(
		`\${status}\${ (originalPath)}\${'&nbsp;&nbsp;•&nbsp;&nbsp;'changesDetail}`,
		file,
		{
			outputFormat: 'markdown',
		},
	);
	return CommitFormatter.fromTemplateAsync(
		tooltipWithStatusFormat.replace('{{slot-status}}', status),
		commit,
		{ source: 'view:hover' },
		{
			enrichedAutolinks: enrichedAutolinks,
			dateFormat: configuration.get('defaultDateFormat'),
			getBranchAndTagTips: options?.getBranchAndTagTips,
			messageAutolinks: true,
			messageIndent: 4,
			pullRequest: pr,
			outputFormat: 'markdown',
			remotes: remotes,
			unpublished: options?.unpublished,
		},
	);
}
