import type { Command } from 'vscode';
import { MarkdownString, ThemeIcon, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode';
import type { GitCommit } from '@gitlens/git/models/commit.js';
import type { GitFileWithCommit } from '@gitlens/git/models/file.js';
import { GitFileChange } from '@gitlens/git/models/fileChange.js';
import type { GitRevisionReference } from '@gitlens/git/models/reference.js';
import { getGitFileStatusIcon } from '@gitlens/git/utils/fileStatus.utils.js';
import { shortenRevision } from '@gitlens/git/utils/revision.utils.js';
import { joinPaths } from '@gitlens/utils/path.js';
import type { DiffWithCommandArgs } from '../../commands/diffWith.js';
import type { DiffWithPreviousCommandArgs } from '../../commands/diffWithPrevious.js';
import { StatusFileFormatter } from '../../git/formatters/statusFormatter.js';
import { GitUri } from '../../git/gitUri.js';
import { createCommand } from '../../system/-webview/command.js';
import { relativeDir } from '../../system/-webview/path.js';
import { editorLineToDiffRange } from '../../system/-webview/vscode/range.js';
import type { ViewsWithCommits } from '../viewBase.js';
import { getFileTooltip } from './abstract/viewFileNode.js';
import type { ViewNode } from './abstract/viewNode.js';
import { ContextValues } from './abstract/viewNode.js';
import { ViewRefFileNode } from './abstract/viewRefNode.js';
import { FileRevisionAsCommitNode } from './fileRevisionAsCommitNode.js';
import type { FileNode } from './folderNode.js';

export class StatusFileNode extends ViewRefFileNode<'status-file', ViewsWithCommits> implements FileNode {
	private readonly _files: GitFileWithCommit[];
	private readonly _hasStagedChanges: boolean;
	private readonly _hasUnstagedChanges: boolean;
	private readonly _type: 'ahead' | 'behind' | 'working';

	constructor(
		view: ViewsWithCommits,
		parent: ViewNode,
		repoPath: string,
		files: GitFileWithCommit[],
		type: 'ahead' | 'behind' | 'working',
	) {
		let hasStagedChanges = false;
		let hasUnstagedChanges = false;
		let ref;
		for (const { commit: c } of files) {
			if (c.isUncommitted) {
				if (c.isUncommittedStaged) {
					hasStagedChanges = true;
					if (!hasUnstagedChanges) {
						ref = c.sha;
					}

					break;
				} else {
					ref = undefined;
					hasUnstagedChanges = true;
				}
			} else if (hasUnstagedChanges) {
				break;
			} else {
				ref = c.sha;
				break;
			}
		}

		let file;
		for (const f of files.reverse()) {
			if (file == null) {
				file = f;
			} else if (file.status === 'M' || f.status !== 'M') {
				file = f;
			}
		}
		file ??= files.at(-1)!;

		super('status-file', GitUri.fromFile(file, repoPath, ref), view, parent, file);

		this._files = files;
		this._type = type;
		this._hasStagedChanges = hasStagedChanges;
		this._hasUnstagedChanges = hasUnstagedChanges;
	}

	override toClipboard(): string {
		return this.fileName;
	}

	get commit(): GitCommit {
		return this._files[0]?.commit;
	}

	private _description: string | undefined;
	get description(): string {
		this._description ??= StatusFileFormatter.fromTemplate(
			this.view.config.formats.files.description,
			{ ...this.file, commit: this.commit },
			{ relativePath: this.relativePath },
		);
		return this._description;
	}

	get fileName(): string {
		return this.file.path;
	}

	private _folderName: string | undefined;
	get folderName(): string {
		this._folderName ??= relativeDir(this.uri.relativePath);
		return this._folderName;
	}

	private _label: string | undefined;
	get label(): string {
		this._label ??= StatusFileFormatter.fromTemplate(
			this.view.config.formats.files.label,
			{ ...this.file, commit: this.commit },
			{ relativePath: this.relativePath },
		);
		return this._label;
	}

	get priority(): number {
		if (this._hasStagedChanges && !this._hasUnstagedChanges) return -3;
		if (this._hasStagedChanges) return -2;
		if (this._hasUnstagedChanges) return -1;
		return 0;
	}

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

	private _relativePath: string | undefined;
	get relativePath(): string | undefined {
		return this._relativePath;
	}
	set relativePath(value: string | undefined) {
		this._relativePath = value;
		this._label = undefined;
		this._description = undefined;
	}

	getChildren(): ViewNode[] {
		return this._files.map(f => new FileRevisionAsCommitNode(this.view, this, f, f.commit));
	}

	getTreeItem(): TreeItem {
		const isSingleChange = this._files.length === 1;
		const item = new TreeItem(
			this.label,
			isSingleChange ? TreeItemCollapsibleState.None : TreeItemCollapsibleState.Collapsed,
		);
		item.description = this.description;
		item.command = this.getCommand();

		function getStatusSuffix(f: GitFileWithCommit) {
			return isSingleChange
				? ''
				: `in \`\`\`${f.commit.isUncommitted ? '' : '$(git-commit) '}${shortenRevision(f.commit.sha)}\`\`\``;
		}

		let tooltip = this._files
			.map(
				f =>
					`${getFileTooltip(f, getStatusSuffix(f))}${
						GitFileChange.is(f) && f.stats != null ? '\n\n' : '\\\n'
					}`,
			)
			.join('')
			.trim();
		if (tooltip.endsWith('\\')) {
			tooltip = tooltip.slice(0, -1);
		}
		item.tooltip = new MarkdownString(tooltip, true);

		if (this._hasStagedChanges || this._hasUnstagedChanges) {
			item.contextValue = `${ContextValues.File}${this._hasStagedChanges ? '+staged' : ''}${this._hasUnstagedChanges ? '+unstaged' : ''}${this.file.submodule != null ? '+submodule' : ''}`;

			// Use the file icon and decorations
			item.resourceUri = this.view.container.git.getAbsoluteUri(this.file.path, this.repoPath);
			item.iconPath = ThemeIcon.File;
		} else {
			item.contextValue = ContextValues.StatusFileCommits;

			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))),
			};
		}

		// Only cache the label/description for a single refresh
		this._label = undefined;
		this._description = undefined;

		return item;
	}

	override getCommand(): Command | undefined {
		if ((this._hasStagedChanges || this._hasUnstagedChanges) && this._files.length === 1) {
			return createCommand<[undefined, DiffWithPreviousCommandArgs]>(
				'gitlens.diffWithPrevious:views',
				'Open Changes with Previous Revision',
				undefined,
				{
					commit: this.commit,
					uri: GitUri.fromFile(this.file, this.repoPath),
					range: editorLineToDiffRange(0),
					showOptions: { preserveFocus: true, preview: true },
				},
			);
		}

		let commandArgs: DiffWithCommandArgs;
		switch (this._type) {
			case 'ahead':
			case 'behind': {
				const lhs = this._files.at(-1)!.commit;
				const rhs = this._files[0].commit;

				commandArgs = {
					lhs: {
						sha: `${lhs.sha}^`,
						uri: GitUri.fromFile(
							lhs.fileset?.files?.find(f => f.path === this.file.path) ?? this.file.path,
							this.repoPath,
							`${lhs.sha}^`,
							true,
						),
					},
					rhs: {
						sha: rhs.sha,
						uri: GitUri.fromFile(
							rhs.fileset?.files?.find(f => f.path === this.file.path) ?? this.file.path,
							this.repoPath,
							rhs.sha,
						),
					},
					repoPath: this.repoPath,
					range: editorLineToDiffRange(0),
					showOptions: { preserveFocus: true, preview: true },
				};
				break;
			}
			default: {
				const commit = this._files.at(-1)!.commit;
				const file = commit.fileset?.files?.find(f => f.path === this.file.path) ?? this.file;
				commandArgs = {
					lhs: {
						sha: `${commit.sha}^`,
						uri: GitUri.fromFile(file, this.repoPath, undefined, true),
					},
					rhs: {
						sha: '',
						uri: GitUri.fromFile(this.file, this.repoPath),
					},
					repoPath: this.repoPath,
					range: editorLineToDiffRange(0),
					showOptions: {
						preserveFocus: true,
						preview: true,
					},
				};
				break;
			}
		}

		return createCommand<[DiffWithCommandArgs]>('gitlens.diffWith', 'Open Changes', commandArgs);
	}
}
