import type { GitBranchReference, GitReference } from '@gitlens/git/models/reference.js';
import { getReferenceLabel, isBranchReference } from '@gitlens/git/utils/reference.utils.js';
import { isStringArray } from '@gitlens/utils/array.js';
import { fromNow } from '@gitlens/utils/date.js';
import { pad, pluralize } from '@gitlens/utils/string.js';
import { GlyphChars } from '../../constants.js';
import type { Container } from '../../container.js';
import type { GlRepository } from '../../git/models/repository.js';
import { createDirectiveQuickPickItem, Directive } from '../../quickpicks/items/directive.js';
import type { FlagsQuickPickItem } from '../../quickpicks/items/flags.js';
import { createFlagsQuickPickItem } from '../../quickpicks/items/flags.js';
import { configuration } from '../../system/-webview/configuration.js';
import type { ViewsWithRepositoryFolders } from '../../views/viewBase.js';
import type {
	AsyncStepResultGenerator,
	PartialStepState,
	StepGenerator,
	StepsContext,
	StepSelection,
	StepState,
} from '../quick-wizard/models/steps.js';
import { StepResultBreak } from '../quick-wizard/models/steps.js';
import type { QuickPickStep } from '../quick-wizard/models/steps.quickpick.js';
import { FetchQuickInputButton } from '../quick-wizard/quickButtons.js';
import { QuickCommand } from '../quick-wizard/quickCommand.js';
import {
	canSkipRepositoriesPick,
	pickRepositoriesStep,
	pickRepositoryStep,
} from '../quick-wizard/steps/repositories.js';
import { StepsController } from '../quick-wizard/stepsController.js';
import { appendReposToTitle, assertStepState, canPickStepContinue } from '../quick-wizard/utils/steps.utils.js';

const Steps = {
	PickRepos: 'push-pick-repos',
	Confirm: 'push-confirm',
} as const;
type StepNames = (typeof Steps)[keyof typeof Steps];

interface Context extends StepsContext<StepNames> {
	repos: GlRepository[];
	associatedView: ViewsWithRepositoryFolders;
	title: string;
}

type Flags = '--force' | '--set-upstream' | string;
interface State<Repos = string | string[] | GlRepository | GlRepository[]> {
	repos: Repos;
	reference?: GitReference;
	flags: Flags[];
}

export interface PushGitCommandArgs {
	readonly command: 'push';
	confirm?: boolean;
	state?: Partial<State>;
}

export class PushGitCommand extends QuickCommand<State> {
	constructor(container: Container, args?: PushGitCommandArgs) {
		super(container, 'push', 'push', 'Push', {
			description: 'pushes changes from the current branch to a remote',
		});

		this.initialState = { confirm: args?.confirm, ...args?.state };
	}

	private execute(state: StepState<State<GlRepository[]>>) {
		const index = state.flags.indexOf('--set-upstream');
		if (index !== -1) {
			return this.container.git.pushAll(state.repos, {
				force: false,
				publish: { remote: state.flags[index + 1] },
				reference: state.reference,
			});
		}

		return this.container.git.pushAll(state.repos, {
			force: state.flags.includes('--force'),
			reference: state.reference,
		});
	}

	protected createContext(context?: StepsContext<any>): Context {
		return {
			...context,
			container: this.container,
			repos: this.container.git.openRepositories,
			associatedView: this.container.views.commits,
			title: this.title,
		};
	}

	protected async *steps(state: PartialStepState<State>, context?: Context): StepGenerator {
		context ??= this.createContext();
		using steps = new StepsController<StepNames>(context, this);

		state.flags ??= [];

		if (state.repos != null && !Array.isArray(state.repos)) {
			state.repos = typeof state.repos === 'string' ? [state.repos] : [state.repos];
		}

		assertStepState<State<GlRepository[] | string[]>>(state);

		while (!steps.isComplete) {
			context.title = this.title;

			if (steps.isAtStep(Steps.PickRepos) || !state.repos?.length || isStringArray(state.repos)) {
				// Skip the picker only when the sole available repo is the one requested
				if (canSkipRepositoriesPick(context.repos, state.repos)) {
					state.repos = context.repos;
				} else if (state.reference != null) {
					// If a reference is specified, only allow picking the repository that contains it
					using step = steps.enterStep(Steps.PickRepos);

					const result = yield* pickRepositoryStep(
						{ ...state, repos: undefined, repo: state.reference.repoPath },
						context,
						step,
					);
					if (result === StepResultBreak) {
						state.repos = undefined!;
						if (step.goBack() == null) break;
						continue;
					}

					state.repos = [result];
				} else {
					using step = steps.enterStep(Steps.PickRepos);

					const result = yield* pickRepositoriesStep(state, context, step, {
						skipIfPossible: true,
					});
					if (result === StepResultBreak) {
						state.repos = undefined!;
						if (step.goBack() == null) break;
						continue;
					}

					state.repos = result;
				}
			}

			assertStepState<State<GlRepository[]>>(state);

			if (this.confirm(state.confirm)) {
				using step = steps.enterStep(Steps.Confirm);

				const result = yield* this.confirmStep(state, context);
				if (result === StepResultBreak) {
					state.flags = [];
					if (step.goBack() == null) break;
					continue;
				}

				state.flags = result;
			}

			steps.markStepsComplete();
			void this.execute(state);
		}

		return steps.isComplete ? undefined : StepResultBreak;
	}

	private async *confirmStep(
		state: StepState<State<GlRepository[]>>,
		context: Context,
	): AsyncStepResultGenerator<Flags[]> {
		const useForceWithLease = configuration.getCore('git.useForcePushWithLease') ?? true;
		const useForceIfIncludes =
			useForceWithLease &&
			(configuration.getCore('git.useForcePushIfIncludes') ?? true) &&
			(await state.repos[0].git.supports('git:push:force-if-includes'));

		let step: QuickPickStep<FlagsQuickPickItem<Flags>>;

		if (state.repos.length > 1) {
			step = this.createConfirmStep(appendReposToTitle(`Confirm ${context.title}`, state, context), [
				createFlagsQuickPickItem<Flags>(state.flags, [], {
					label: this.title,
					detail: `Will push ${state.repos.length} repos`,
				}),
				createFlagsQuickPickItem<Flags>(state.flags, ['--force'], {
					label: `Force ${this.title}${
						useForceIfIncludes ? ' (with lease and if includes)' : useForceWithLease ? ' (with lease)' : ''
					}`,
					description: `--force${
						useForceWithLease ? `-with-lease${useForceIfIncludes ? ' --force-if-includes' : ''}` : ''
					}`,
					detail: `Will force push${
						useForceIfIncludes ? ' (with lease and if includes)' : useForceWithLease ? ' (with lease)' : ''
					} ${state.repos.length} repos`,
				}),
			]);
		} else {
			const [repo] = state.repos;

			const items: FlagsQuickPickItem<Flags>[] = [];

			if (isBranchReference(state.reference)) {
				if (state.reference.remote) {
					step = this.createConfirmStep(
						appendReposToTitle(context.title, state, context),
						[],
						createDirectiveQuickPickItem(Directive.Cancel, true, {
							label: 'OK',
							detail: 'Cannot push a remote branch',
						}),
						{ placeholder: 'Cannot push a remote branch' },
					);
				} else {
					const branch = await repo.git.branches.getBranch(state.reference.name);

					if (branch != null && branch?.upstream == null) {
						for (const remote of await repo.git.remotes.getRemotes()) {
							items.push(
								createFlagsQuickPickItem<Flags>(
									state.flags,
									['--set-upstream', remote.name, branch.name],
									{
										label: `Publish ${branch.name} to ${remote.name}`,
										detail: `Will publish ${getReferenceLabel(branch)} to ${remote.name}`,
									},
								),
							);
						}

						if (items.length) {
							step = this.createConfirmStep(
								appendReposToTitle('Confirm Publish', state, context),
								items,
								undefined,
								{ placeholder: 'Confirm Publish' },
							);
						} else {
							step = this.createConfirmStep(
								appendReposToTitle('Publish', state, context),
								[],
								createDirectiveQuickPickItem(Directive.Cancel, true, {
									label: 'OK',
									detail: 'No remotes found',
								}),
								{ placeholder: 'Cannot publish; No remotes found' },
							);
						}
					} else if (branch?.upstream?.state.behind) {
						step = this.createConfirmStep(
							appendReposToTitle(`Confirm ${context.title}`, state, context),
							[
								createFlagsQuickPickItem<Flags>(state.flags, ['--force'], {
									label: `Force ${this.title}${
										useForceIfIncludes
											? ' (with lease and if includes)'
											: useForceWithLease
												? ' (with lease)'
												: ''
									}`,
									description: `--force${
										useForceWithLease
											? `-with-lease${useForceIfIncludes ? ' --force-if-includes' : ''}`
											: ''
									}`,
									detail: `Will force push${
										useForceIfIncludes
											? ' (with lease and if includes)'
											: useForceWithLease
												? ' (with lease)'
												: ''
									} ${
										branch?.upstream.state.ahead
											? ` ${pluralize('commit', branch.upstream.state.ahead)}`
											: ''
									}${branch.remoteName ? ` to ${branch.remoteName}` : ''}${
										branch != null && branch.upstream.state.behind > 0
											? `, overwriting ${pluralize('commit', branch.upstream.state.behind)}${
													branch?.remoteName ? ` on ${branch.remoteName}` : ''
												}`
											: ''
									}`,
								}),
							],
							createDirectiveQuickPickItem(Directive.Cancel, true, {
								label: `Cancel ${this.title}`,
								detail: `Cannot push; ${getReferenceLabel(
									branch,
								)} is behind ${branch.remoteName} by ${pluralize(
									'commit',
									branch.upstream.state.behind,
								)}`,
							}),
						);
					} else if (branch?.upstream?.state.ahead) {
						step = this.createConfirmStep(appendReposToTitle(`Confirm ${context.title}`, state, context), [
							createFlagsQuickPickItem<Flags>(state.flags, [branch.remoteName!], {
								label: this.title,
								detail: `Will push ${pluralize(
									'commit',
									branch.upstream.state.ahead,
								)} from ${getReferenceLabel(branch)} to ${branch.remoteName}`,
							}),
						]);
					} else {
						step = this.createConfirmStep(
							appendReposToTitle(context.title, state, context),
							[],
							createDirectiveQuickPickItem(Directive.Cancel, true, {
								label: 'OK',
								detail: 'No commits found to push',
							}),
							{ placeholder: 'Nothing to push; No commits found to push' },
						);
					}
				}
			} else {
				const status = await repo.git.status.getStatus();

				const branch: GitBranchReference = {
					refType: 'branch',
					name: status?.branch ?? 'HEAD',
					ref: status?.branch ?? 'HEAD',
					remote: false,
					repoPath: repo.path,
				};

				if (status?.upstream?.state.ahead === 0) {
					if (!isBranchReference(state.reference) && status.upstream == null) {
						let pushDetails;

						if (state.reference != null) {
							pushDetails = ` up to and including ${getReferenceLabel(state.reference, {
								label: false,
							})}`;
						} else {
							state.reference = branch;
							pushDetails = '';
						}

						for (const remote of await repo.git.remotes.getRemotes()) {
							items.push(
								createFlagsQuickPickItem<Flags>(
									state.flags,
									['--set-upstream', remote.name, status.branch],
									{
										label: `Publish ${branch.name} to ${remote.name}`,
										detail: `Will publish ${getReferenceLabel(branch)}${pushDetails} to ${
											remote.name
										}`,
									},
								),
							);
						}
					}

					if (items.length) {
						step = this.createConfirmStep(
							appendReposToTitle('Confirm Publish', state, context),
							items,
							undefined,
							{ placeholder: 'Confirm Publish' },
						);
					} else if (status.upstream == null) {
						step = this.createConfirmStep(
							appendReposToTitle('Publish', state, context),
							[],
							createDirectiveQuickPickItem(Directive.Cancel, true, {
								label: 'OK',
								detail: 'No remotes found',
							}),
							{ placeholder: 'Cannot publish; No remotes found' },
						);
					} else {
						step = this.createConfirmStep(
							appendReposToTitle(context.title, state, context),
							[],
							createDirectiveQuickPickItem(Directive.Cancel, true, {
								label: 'OK',
								detail: `No commits ahead of ${status.upstream?.name}`,
							}),
							{
								placeholder: `Nothing to push; No commits ahead of ${status.upstream?.name}`,
							},
						);
					}
				} else {
					let lastFetchedOn = '';

					const lastFetched = await repo.getLastFetched();
					if (lastFetched !== 0) {
						lastFetchedOn = `${pad(GlyphChars.Dot, 2, 2)}Last fetched ${fromNow(new Date(lastFetched))}`;
					}

					let pushDetails;
					if (state.reference != null) {
						pushDetails = `${
							status?.upstream?.state.ahead
								? ` commits up to and including ${getReferenceLabel(state.reference, {
										label: false,
									})}`
								: ''
						}${status?.upstream ? ` to ${status.upstream.name}` : ''}`;
					} else {
						pushDetails = `${
							status?.upstream?.state.ahead ? ` ${pluralize('commit', status.upstream.state.ahead)}` : ''
						}${status?.upstream ? ` to ${status.upstream.name}` : ''}`;
					}

					step = this.createConfirmStep(
						appendReposToTitle(`Confirm ${context.title}`, state, context, lastFetchedOn),
						[
							...(status?.upstream?.state.behind
								? []
								: [
										createFlagsQuickPickItem<Flags>(state.flags, [], {
											label: this.title,
											detail: `Will push${pushDetails}`,
										}),
									]),
							createFlagsQuickPickItem<Flags>(state.flags, ['--force'], {
								label: `Force ${this.title}${
									useForceIfIncludes
										? ' (with lease and if includes)'
										: useForceWithLease
											? ' (with lease)'
											: ''
								}`,
								description: `--force${
									useForceWithLease
										? `-with-lease${useForceIfIncludes ? ' --force-if-includes' : ''}`
										: ''
								}`,
								detail: `Will force push${
									useForceIfIncludes
										? ' (with lease and if includes)'
										: useForceWithLease
											? ' (with lease)'
											: ''
								} ${pushDetails}${
									status?.upstream?.state.behind
										? `, overwriting ${pluralize('commit', status.upstream.state.behind)}${
												status?.upstream ? ` on ${status.upstream.name}` : ''
											}`
										: ''
								}`,
							}),
						],
						status?.upstream?.state.behind
							? createDirectiveQuickPickItem(Directive.Cancel, true, {
									label: `Cancel ${this.title}`,
									detail: `Cannot push; ${getReferenceLabel(branch)} is behind${
										status?.upstream ? ` ${status.upstream.name}` : ''
									} by ${pluralize('commit', status.upstream.state.behind)}`,
								})
							: undefined,
					);

					step.additionalButtons = [FetchQuickInputButton];
					step.onDidClickButton = async (quickpick, button) => {
						if (button !== FetchQuickInputButton || quickpick.busy) return false;

						quickpick.title = `Confirm ${context.title}${pad(GlyphChars.Dot, 2, 2)}Fetching${
							GlyphChars.Ellipsis
						}`;

						quickpick.busy = true;
						try {
							await repo.git.fetch({ progress: true });
							// Signal that the step should be retried
							return true;
						} finally {
							quickpick.busy = false;
						}
					};
				}
			}
		}

		const selection: StepSelection<typeof step> = yield step;
		return canPickStepContinue(step, state, selection) ? selection[0].item : StepResultBreak;
	}
}
