import type { CancellationToken, ConfigurationChangeEvent } from 'vscode';
import { Disposable, ProgressLocation, ThemeIcon, TreeItem, TreeItemCollapsibleState, window } from 'vscode';
import { GitBranch } from '@gitlens/git/models/branch.js';
import { GitCommit } from '@gitlens/git/models/commit.js';
import type { GitRevisionReference } from '@gitlens/git/models/reference.js';
import type { GitUser } from '@gitlens/git/models/user.js';
import { matchContributor } from '@gitlens/git/utils/contributor.utils.js';
import { getLastFetchedUpdateInterval } from '@gitlens/git/utils/fetch.utils.js';
import { getReferenceLabel } from '@gitlens/git/utils/reference.utils.js';
import { trace } from '@gitlens/utils/decorators/log.js';
import { disposableInterval } from '@gitlens/utils/disposable.js';
import type { CommitsViewConfig, ViewFilesLayout } from '../config.js';
import { GlyphChars } from '../constants.js';
import type { Container } from '../container.js';
import { GitUri } from '../git/gitUri.js';
import type { RepositoryChangeEvent } from '../git/models/repository.js';
import type { UsageChangeEvent } from '../onboarding/usageTracker.js';
import { showContributorsPicker } from '../quickpicks/contributorsPicker.js';
import { getRepositoryOrShowPicker } from '../quickpicks/repositoryPicker.js';
import { createCommand, executeCommand } from '../system/-webview/command.js';
import { configuration } from '../system/-webview/configuration.js';
import { setContext } from '../system/-webview/context.js';
import { gate } from '../system/decorators/gate.js';
import { RepositoriesSubscribeableNode } from './nodes/abstract/repositoriesSubscribeableNode.js';
import { RepositoryFolderNode } from './nodes/abstract/repositoryFolderNode.js';
import type { ViewNode } from './nodes/abstract/viewNode.js';
import { BranchNode } from './nodes/branchNode.js';
import { BranchTrackingStatusNode } from './nodes/branchTrackingStatusNode.js';
import { CommandMessageNode } from './nodes/common.js';
import type { GroupedViewContext, RevealOptions } from './viewBase.js';
import { ViewBase } from './viewBase.js';
import type { CopyNodeCommandArgs } from './viewCommands.js';
import { registerViewCommand } from './viewCommands.js';

export class CommitsRepositoryNode extends RepositoryFolderNode<CommitsView, BranchNode> {
	async getChildren(): Promise<ViewNode[]> {
		this.view.message = undefined;

		if (this.child == null) {
			const branch = await this.repo.git.branches.getBranch();
			if (branch == null) {
				this.view.message = 'No commits could be found.';

				return [];
			}

			const authors = this.view.state.filterCommits.get(this.repo.id);
			this.child = new BranchNode(
				this.uri,
				this.view,
				this.splatted ? (this.parent ?? this) : this,
				this.repo,
				branch,
				true,
				{
					expand: true,
					limitCommits: !this.splatted,
					showComparison: this.view.config.showBranchComparison,
					showStatusDecorationOnly: true,
					showMergeCommits: !this.view.state.hideMergeCommits,
					showStashes: this.view.config.showStashes,
					showTracking: true,
					authors: authors,
				},
			);
		}

		return this.child.getChildren();
	}

	@gate()
	@trace()
	override async refresh(reset: boolean = false): Promise<void> {
		if (reset) {
			this.child = undefined;
		} else {
			void this.parent?.triggerChange(false);
		}

		await this.ensureSubscription();
	}

	@trace()
	protected override async subscribe(): Promise<Disposable> {
		const lastFetched = (await this.repo?.getLastFetched()) ?? 0;

		const interval = getLastFetchedUpdateInterval(lastFetched);
		if (lastFetched !== 0 && interval > 0) {
			return Disposable.from(
				await super.subscribe(),
				disposableInterval(() => {
					// Skip update if view is not visible to reduce unnecessary work
					if (!this.view.visible) return;

					// Check if the interval should change, and if so, reset it
					if (interval !== getLastFetchedUpdateInterval(lastFetched)) {
						void this.resetSubscription();
						return;
					}

					if (this.splatted) {
						this.view.triggerNodeChange(this.parent ?? this);
					} else {
						this.view.triggerNodeChange(this);
					}
				}, interval),
			);
		}

		return super.subscribe();
	}

	protected changed(e: RepositoryChangeEvent): boolean {
		if (this.view.config.showStashes && e.changed('stash')) {
			return true;
		}

		return e.changed(
			'config',
			'head',
			'heads',
			'index',
			'remotes',
			'remoteProviders',
			'pausedOp',
			'worktrees',
			'unknown',
		);
	}
}

export class CommitsViewNode extends RepositoriesSubscribeableNode<CommitsView, CommitsRepositoryNode> {
	async getChildren(): Promise<ViewNode[]> {
		this.view.description = this.view.getViewDescription();
		this.view.message = undefined;

		if (this.children == null) {
			if (this.view.container.git.isDiscoveringRepositories) {
				await this.view.container.git.isDiscoveringRepositories;
			}

			const repositories = this.view.getFilteredRepositories();
			if (!repositories.length) {
				this.view.message = 'No commits could be found.';

				return [];
			}

			const repo = this.view.container.git.getBestRepositoryOrFirst();
			this.children = repositories.map(
				r =>
					new CommitsRepositoryNode(GitUri.fromRepoPath(r.path), this.view, this, r, {
						expand: r === repo,
						showBranchAndLastFetched: true,
					}),
			);
		}

		const children = [];

		if (
			configuration.get('plusFeatures.enabled') &&
			!this.view.grouped &&
			this.view.container.usage.get('graphView:shown') == null &&
			this.view.container.usage.get('graphWebview:shown') == null
		) {
			children.push(
				new CommandMessageNode(
					this.view,
					this,
					createCommand('gitlens.showGraph', 'Show Commit Graph'),
					'Visualize commits on the Commit Graph',
					undefined,
					'Visualize commits on the Commit Graph',
					new ThemeIcon('gitlens-graph'),
				),
			);
		}

		if (this.children.length === 1) {
			const [child] = this.children;

			const branch = await child.repo.git.branches.getBranch();
			if (branch != null) {
				const descParts = [];

				if (branch.rebasing) {
					descParts.push(`${branch.name} (Rebasing)`);
				} else {
					descParts.push(branch.name);
				}

				const status = GitBranch.getTrackingStatus(branch);
				if (status) {
					descParts.push(status);
				}

				this.view.description = `${
					this.view.grouped ? `${this.view.name.toLocaleLowerCase()}: ` : ''
				}${descParts.join(` ${GlyphChars.Dot} `)}`;
			}

			children.push(...(await child.getChildren()));
		} else {
			children.push(...this.children);
		}

		return children;
	}

	getTreeItem(): TreeItem {
		const item = new TreeItem('Commits', TreeItemCollapsibleState.Expanded);
		return item;
	}
}

interface CommitsViewState {
	filterCommits: Map<string, GitUser[] | undefined>;
	hideMergeCommits?: boolean;
}

export class CommitsView extends ViewBase<'commits', CommitsViewNode, CommitsViewConfig> {
	protected readonly configKey = 'commits';

	constructor(container: Container, grouped?: GroupedViewContext) {
		super(container, 'commits', 'Commits', 'commitsView', grouped);
		this.disposables.push(container.usage.onDidChange(this.onUsageChanged, this));
	}

	private onUsageChanged(e: UsageChangeEvent | void) {
		// Refresh the view if the graph usage state has changed, since we render a node for it before the first use
		if (e == null || e.key === 'graphView:shown' || e.key === 'graphWebview:shown') {
			void this.refresh();
		}
	}

	override get canReveal(): boolean {
		return this.config.reveal || !configuration.get('views.repositories.showCommits');
	}

	override get canSelectMany(): boolean {
		return configuration.get('views.multiselect');
	}

	private readonly _state: CommitsViewState = { filterCommits: new Map<string, GitUser[] | undefined>() };
	get state(): CommitsViewState {
		return this._state;
	}

	protected getRoot(): CommitsViewNode {
		return new CommitsViewNode(this);
	}

	protected registerCommands(): Disposable[] {
		return [
			registerViewCommand(
				this.getQualifiedCommand('copy'),
				() => executeCommand<CopyNodeCommandArgs>('gitlens.views.copy', this.activeSelection, this.selection),
				this,
			),
			registerViewCommand(
				this.getQualifiedCommand('refresh'),
				() => {
					this.container.git.resetCaches('branches', 'status', 'tags');
					return this.refresh(true);
				},
				this,
			),
			registerViewCommand(this.getQualifiedCommand('filterRepositories'), () => this.filterRepositories(), this),
			registerViewCommand(
				this.getQualifiedCommand('setFilesLayoutToAuto'),
				() => this.setFilesLayout('auto'),
				this,
			),
			registerViewCommand(
				this.getQualifiedCommand('setFilesLayoutToList'),
				() => this.setFilesLayout('list'),
				this,
			),
			registerViewCommand(
				this.getQualifiedCommand('setFilesLayoutToTree'),
				() => this.setFilesLayout('tree'),
				this,
			),
			registerViewCommand(
				this.getQualifiedCommand('setCommitsFilterAuthors'),
				n => this.setCommitsFilter(n, true),
				this,
			),
			registerViewCommand(
				this.getQualifiedCommand('setCommitsFilterOff'),
				n => this.setCommitsFilter(n, false),
				this,
			),
			registerViewCommand(
				this.getQualifiedCommand('setShowMergeCommitsOn'),
				() => this.setShowMergeCommits(true),
				this,
			),
			registerViewCommand(
				this.getQualifiedCommand('setShowMergeCommitsOff'),
				() => this.setShowMergeCommits(false),
				this,
			),

			registerViewCommand(this.getQualifiedCommand('setShowAvatarsOn'), () => this.setShowAvatars(true), this),
			registerViewCommand(this.getQualifiedCommand('setShowAvatarsOff'), () => this.setShowAvatars(false), this),
			registerViewCommand(
				this.getQualifiedCommand('setShowBranchComparisonOn'),
				() => this.setShowBranchComparison(true),
				this,
			),
			registerViewCommand(
				this.getQualifiedCommand('setShowBranchComparisonOff'),
				() => this.setShowBranchComparison(false),
				this,
			),
			registerViewCommand(
				this.getQualifiedCommand('setShowBranchPullRequestOn'),
				() => this.setShowBranchPullRequest(true),
				this,
			),
			registerViewCommand(
				this.getQualifiedCommand('setShowBranchPullRequestOff'),
				() => this.setShowBranchPullRequest(false),
				this,
			),
			registerViewCommand(this.getQualifiedCommand('setShowStashesOn'), () => this.setShowStashes(true), this),
			registerViewCommand(this.getQualifiedCommand('setShowStashesOff'), () => this.setShowStashes(false), this),
		];
	}

	protected override filterConfigurationChanged(e: ConfigurationChangeEvent): boolean {
		const changed = super.filterConfigurationChanged(e);
		if (
			!changed &&
			!configuration.changed(e, 'defaultCurrentUserNameStyle') &&
			!configuration.changed(e, 'defaultDateFormat') &&
			!configuration.changed(e, 'defaultDateLocale') &&
			!configuration.changed(e, 'defaultDateShortFormat') &&
			!configuration.changed(e, 'defaultDateSource') &&
			!configuration.changed(e, 'defaultDateStyle') &&
			!configuration.changed(e, 'defaultGravatarsStyle') &&
			!configuration.changed(e, 'defaultTimeFormat') &&
			!configuration.changed(e, 'plusFeatures.enabled') &&
			!configuration.changed(e, 'sortRepositoriesBy')
		) {
			return false;
		}

		return true;
	}

	async findCommit(
		commit: GitCommit | { repoPath: string; ref: string },
		token?: CancellationToken,
	): Promise<ViewNode | undefined> {
		const { repoPath } = commit;

		const svc = this.container.git.getRepositoryService(commit.repoPath);
		const branch = await svc.branches.getBranch();
		if (branch == null) return undefined;

		// Check if the commit exists on the current branch
		const branches = await svc.branches.getBranchesWithCommits([commit.ref], branch.name, {
			commitDate: GitCommit.is(commit) ? commit.committer.date : undefined,
		});
		if (!branches.length) return undefined;

		return this.findNode((n: any) => n.commit?.ref === commit.ref, {
			allowPaging: true,
			maxDepth: 2,
			canTraverse: async n => {
				if (n instanceof CommitsViewNode) {
					let node: ViewNode | undefined = await n.getSplattedChild?.();
					if (node instanceof CommitsRepositoryNode) {
						node = await node.getSplattedChild?.();
						if (node instanceof BranchNode) {
							await node.loadMore({ until: commit.ref });
						}
					}

					return true;
				}

				if (n instanceof CommitsRepositoryNode) {
					if (n.repoPath === repoPath) {
						const node = await n.getSplattedChild?.();
						if (node instanceof BranchNode) {
							await node.loadMore({ until: commit.ref });
							return true;
						}
					}
				}

				if (n instanceof BranchTrackingStatusNode) {
					return n.repoPath === repoPath;
				}

				return false;
			},
			token: token,
		});
	}

	@gate(() => '')
	async revealCommit(commit: GitRevisionReference, options?: RevealOptions): Promise<ViewNode | undefined> {
		return window.withProgress(
			{
				location: ProgressLocation.Notification,
				title: `Revealing ${getReferenceLabel(commit, {
					icon: false,
					quoted: true,
				})} in the side bar...`,
				cancellable: true,
			},
			async (_progress, token) => {
				const node = await this.findCommit(commit, token);
				if (node == null) return undefined;

				await this.revealDeep(node, options);

				return node;
			},
		);
	}

	@gate(() => '')
	async revealRepository(repoPath: string, options?: RevealOptions): Promise<ViewNode | undefined> {
		const node = await this.findNode(n => n instanceof RepositoryFolderNode && n.repoPath === repoPath, {
			maxDepth: 1,
			canTraverse: n => n instanceof CommitsViewNode || n instanceof RepositoryFolderNode,
		});

		if (node !== undefined) {
			await this.reveal(node, options);
		}

		return node;
	}

	@gate(() => '')
	async revealPausedOperationStatus(repoPath: string, options?: RevealOptions): Promise<ViewNode | undefined> {
		const node = await this.findNode(
			n => n.is('paused-operation-status') && n.pausedOpStatus.repoPath === repoPath,
			{
				maxDepth: 3,
				canTraverse: n =>
					n instanceof CommitsViewNode || n instanceof RepositoryFolderNode || n instanceof BranchNode,
			},
		);

		if (node !== undefined) {
			await this.reveal(node, options);
		}

		return node;
	}

	private setFilesLayout(layout: ViewFilesLayout) {
		return configuration.updateEffective(`views.${this.configKey}.files.layout` as const, layout);
	}

	private async setCommitsFilter(node: ViewNode, filter: boolean) {
		let repo;
		if (node != null) {
			if (node.is('repo-folder')) {
				repo = node.repo;
			} else {
				let parent: ViewNode | undefined = node;
				do {
					parent = parent.getParent();
					if (parent?.is('repo-folder')) {
						repo = parent.repo;
						break;
					}
				} while (parent != null);
			}
		}

		if (filter) {
			repo ??= await getRepositoryOrShowPicker(
				this.container,
				'Filter Commits',
				'Choose a repository',
				undefined,
				{
					excludeWorktrees: true,
				},
			);
			if (repo == null) return;

			let authors = this.state.filterCommits.get(repo.id);
			if (authors == null) {
				const current = await repo.git.config.getCurrentUser();
				authors = current != null ? [current] : undefined;
			}

			const result = await showContributorsPicker(
				this.container,
				repo,
				'Filter Commits',
				repo.virtual ? 'Choose a contributor to show commits from' : 'Choose contributors to show commits from',
				{
					appendReposToTitle: true,
					clearButton: true,
					multiselect: !repo.virtual,
					picked: c => authors?.some(u => matchContributor(c, u)) ?? false,
				},
			);
			if (result == null) return;

			if (result.length === 0) {
				this.state.filterCommits.delete(repo.id);
			} else {
				this.state.filterCommits.set(repo.id, result);
			}
		} else if (repo != null) {
			this.state.filterCommits.delete(repo.id);
		} else {
			this.state.filterCommits.clear();
		}

		void setContext('gitlens:views:commits:filtered', this.state.filterCommits.size !== 0);
		void this.refresh(true);
	}

	private setShowMergeCommits(on: boolean) {
		void setContext('gitlens:views:commits:hideMergeCommits', !on);
		this.state.hideMergeCommits = !on;
		void this.refresh(true);
	}

	private setShowAvatars(enabled: boolean) {
		return configuration.updateEffective(`views.${this.configKey}.avatars` as const, enabled);
	}

	private setShowBranchComparison(enabled: boolean) {
		return configuration.updateEffective(
			`views.${this.configKey}.showBranchComparison` as const,
			enabled ? 'working' : false,
		);
	}

	private async setShowBranchPullRequest(enabled: boolean) {
		await configuration.updateEffective(`views.${this.configKey}.pullRequests.showForBranches` as const, enabled);
		await configuration.updateEffective(`views.${this.configKey}.pullRequests.enabled` as const, enabled);
	}

	private setShowStashes(enabled: boolean) {
		return configuration.updateEffective(`views.${this.configKey}.showStashes` as const, enabled);
	}
}
