import type { Disposable, TextEditor } from 'vscode';
import { TreeItem, TreeItemCollapsibleState, window } from 'vscode';
import { isBranchReference } from '@gitlens/git/utils/reference.utils.js';
import { isSha } from '@gitlens/git/utils/revision.utils.js';
import type { Deferrable } from '@gitlens/utils/debounce.js';
import { debounce } from '@gitlens/utils/debounce.js';
import { debug, trace } from '@gitlens/utils/decorators/log.js';
import { weakEvent } from '@gitlens/utils/event.js';
import { Logger } from '@gitlens/utils/logger.js';
import { getScopedLogger } from '@gitlens/utils/logger.scoped.js';
import { areUrisEqual } from '@gitlens/utils/uri.js';
import type { GitCommitish } from '../../git/gitUri.js';
import { GitUri, unknownGitUri } from '../../git/gitUri.js';
import { ensureWorkingUri } from '../../git/gitUri.utils.js';
import { showReferencePicker } from '../../quickpicks/referencePicker.js';
import { setContext } from '../../system/-webview/context.js';
import { isVirtualUri } from '../../system/-webview/vscode/uris.js';
import { gate } from '../../system/decorators/gate.js';
import type { FileHistoryView } from '../fileHistoryView.js';
import { SubscribeableViewNode } from './abstract/subscribeableViewNode.js';
import type { ViewNode } from './abstract/viewNode.js';
import { ContextValues } from './abstract/viewNode.js';
import { FileHistoryNode } from './fileHistoryNode.js';

export class FileHistoryTrackerNode extends SubscribeableViewNode<'file-history-tracker', FileHistoryView> {
	private _base: string | undefined;

	constructor(view: FileHistoryView) {
		super('file-history-tracker', unknownGitUri, view);
	}

	override dispose(): void {
		super.dispose();
		this.child = undefined;
	}

	private _child: FileHistoryNode | undefined;
	protected get child(): FileHistoryNode | undefined {
		return this._child;
	}
	protected set child(value: FileHistoryNode | undefined) {
		if (this._child === value) return;

		this._child?.dispose();
		this._child = value;
	}

	protected override etag(): number {
		return 0;
	}

	get followingEditor(): boolean {
		return this.canSubscribe;
	}

	get hasUri(): boolean {
		return this._uri !== unknownGitUri && this._uri.repoPath != null;
	}

	async getChildren(): Promise<ViewNode[]> {
		this.view.message = undefined;

		if (this.child == null) {
			this.view.groupedLabel ??= this.view.name.toLocaleLowerCase();

			if (!this.hasUri) {
				this.view.description = this.view.grouped ? this.view.groupedLabel : undefined;

				this.view.message = 'There are no editors open that can provide file history information.';
				this.children = undefined;
				return [];
			}

			const commitish: GitCommitish = {
				...this.uri,
				repoPath: this.uri.repoPath!,
				sha: this._base ?? this.uri.sha,
			};
			const fileUri = new GitUri(this.uri, commitish);
			const svc = this.view.container.git.getRepositoryService(commitish.repoPath);
			const folder = await svc.isFolderUri(this.uri);

			if (this.view.grouped) {
				this.view.groupedLabel = (folder ? 'Folder History' : 'File History').toLocaleLowerCase();
				this.view.description = this.view.groupedLabel;
			} else {
				this.view.title = folder ? 'Folder History' : 'File History';
			}

			let branch;
			if (!commitish.sha || commitish.sha === 'HEAD') {
				branch = await svc.branches.getBranch();
			} else if (!isSha(commitish.sha)) {
				branch = await svc.branches.getBranch(commitish.sha);
			}
			this.child = new FileHistoryNode(fileUri, this.view, this, folder, branch);
		}

		const children = this.child.getChildren();
		void children.then(children => {
			this.children = children;
			if (this._selectSha != null) {
				setTimeout(() => void this.revealCommit(), 250);
			}
		});
		return children;
	}

	getTreeItem(): TreeItem {
		const item = new TreeItem('File History', TreeItemCollapsibleState.Expanded);
		item.contextValue = ContextValues.ActiveFileHistory;

		return item;
	}

	@gate()
	@trace({ exit: true })
	override async refresh(reset: boolean = false): Promise<{ cancel: boolean }> {
		const scope = getScopedLogger();

		if (!this.canSubscribe) return { cancel: false };

		if (reset) {
			if (this._uri != null && this._uri !== unknownGitUri) {
				this.view.container.git.resetCachesForUri(this._uri, 'fileLog');
			}

			this.reset();
		}

		const updated = await this.updateUri(this._selectSha);
		scope?.addExitInfo(`uri=${Logger.toLoggable(this._uri)}`);
		return { cancel: !updated };
	}

	@trace()
	protected async subscribe(): Promise<Disposable | undefined> {
		await this.updateUri(this._selectSha);

		return weakEvent(window.onDidChangeActiveTextEditor, debounce(this.onActiveEditorChanged, 250), this);
	}

	private _triggerChangeDebounced: Deferrable<() => Promise<void>> | undefined;
	@trace({ args: false })
	private onActiveEditorChanged(editor: TextEditor | undefined) {
		// If we are losing the active editor, give more time before assuming its really gone
		// For virtual repositories the active editor event takes a while to fire
		// Ultimately we need to be using the upcoming Tabs api to avoid this
		if (editor == null && isVirtualUri(this._uri)) {
			this._triggerChangeDebounced ??= debounce(() => this.triggerChange(), 1500);
			void this._triggerChangeDebounced();
			return;
		}

		// Only trigger change if the editor's URI is different from the current one
		if (editor != null && areUrisEqual(editor.document.uri, this.uri)) {
			return;
		}

		void this.triggerChange();
	}

	@gate()
	@debug()
	async changeBase(): Promise<void> {
		const pick = await showReferencePicker(
			this.uri.repoPath!,
			'Change File History Base',
			'Choose a reference to set as the new base',
			{
				allowedAdditionalInput: { rev: true },
				picked: this._base,
				sort: { branches: { current: true }, tags: {} },
			},
		);
		if (pick == null) return;

		if (isBranchReference(pick)) {
			const branch = await this.view.container.git.getRepositoryService(this.uri.repoPath!).branches.getBranch();
			this._base = branch?.name === pick.name ? undefined : pick.ref;
		} else {
			this._base = pick.ref;
		}
		if (this.child == null) return;

		this.setUri();
		await this.triggerChange();
	}

	@debug()
	setEditorFollowing(enabled: boolean): void {
		if (enabled) {
			this.setUri();
			// Don't need to call triggerChange here, since canSubscribe will do it
		}

		this.canSubscribe = enabled;
		if (!enabled) {
			void this.triggerChange();
		}
	}

	@trace()
	setUri(uri?: GitUri, sha?: string): void {
		this._uri = uri ?? unknownGitUri;
		this._selectSha = sha ?? uri?.sha;
		void setContext('gitlens:views:fileHistory:canPin', this.hasUri);
	}

	@debug()
	async showHistoryForUri(uri: GitUri): Promise<void> {
		this.setUri(uri);
		await this.triggerChange();
	}

	private reset() {
		this.setUri();
		this.child = undefined;
		this._selectSha = undefined;
	}

	private _selectSha: string | undefined;

	private async updateUri(sha?: string): Promise<boolean> {
		const editor = window.activeTextEditor;
		if (editor == null || !this.view.container.git.isTrackable(editor.document.uri)) {
			if (
				!this.hasUri ||
				(this.view.container.git.isTrackable(this.uri) &&
					window.visibleTextEditors.some(e => e.document?.uri.path === this.uri.path))
			) {
				return false;
			}

			this.reset();
			return true;
		}

		let gitUri = await GitUri.fromUri(editor.document.uri);

		if (editor.document.uri.path === this.uri.path) {
			this._selectSha = sha ?? gitUri.sha;
			queueMicrotask(() => void this.revealCommit());
			return false;
		}

		// If we have a sha, normalize the history to the working file (so we get a full history all the time)
		const uri = await ensureWorkingUri(this.view.container, gitUri);

		if (this.hasUri && areUrisEqual(uri ?? gitUri, this.uri)) {
			this._selectSha = sha ?? gitUri.sha;
			queueMicrotask(() => void this.revealCommit());
			return false;
		}

		if (uri != null) {
			gitUri = await GitUri.fromUri(uri);
		}

		// If we have no repoPath then don't attempt to use the Uri
		if (!gitUri.repoPath) {
			this.reset();
			return true;
		}

		this.setUri(gitUri, sha);
		this.child = undefined;

		return true;
	}

	async revealCommit(): Promise<void> {
		const sha = this._selectSha;
		this._selectSha = undefined;

		const { children } = this;
		if (!children?.length) return;

		let node;
		if (sha == null || sha === 'HEAD') {
			[node] = children;
		} else {
			node = children.find(n =>
				n.is('file-commit') || n.is('commit') ? (n.commit?.sha?.startsWith(sha) ?? false) : false,
			);
			if (!node) {
				node = children.at(-1);
				if (!node?.is('pager')) {
					node = undefined;
				}
			}
		}
		if (!node) return;

		await this.view.reveal(node, { select: true, focus: false });
	}
}
