import type { ConfigurationChangeEvent, ViewColumn } from 'vscode';
import { ConfigurationTarget, Disposable, workspace } from 'vscode';
import { GitCommit, GitCommitIdentity } from '@gitlens/git/models/commit.js';
import { GitFileChange } from '@gitlens/git/models/fileChange.js';
import { GitFileIndexStatus } from '@gitlens/git/models/fileStatus.js';
import { PullRequest } from '@gitlens/git/models/pullRequest.js';
import { map } from '@gitlens/utils/iterable.js';
import { getSettledValue } from '@gitlens/utils/promise.js';
import { fileUri, joinUriPath } from '@gitlens/utils/uri.js';
import { IssuesCloudHostIntegrationId } from '../../constants.integrations.js';
import { extensionPrefix } from '../../constants.js';
import type { WebviewTelemetryContext } from '../../constants.telemetry.js';
import type { Container } from '../../container.js';
import { CommitFormatter } from '../../git/formatters/commitFormatter.js';
import type { SubscriptionChangeEvent } from '../../plus/gk/subscriptionService.js';
import { isIssueCloudIntegrationId } from '../../plus/integrations/authentication/models.js';
import type { ConnectionStateChangeEvent } from '../../plus/integrations/integrationService.js';
import type { ConfigPath, CoreConfigPath } from '../../system/-webview/configuration.js';
import { configuration } from '../../system/-webview/configuration.js';
import type { IpcParams, IpcResponse } from '../ipc/handlerRegistry.js';
import { ipcCommand, ipcRequest } from '../ipc/handlerRegistry.js';
import type { CustomConfigPath } from '../protocol.js';
import {
	assertsConfigKeyValue,
	DidChangeConfigurationNotification,
	isCustomConfigKey,
	UpdateConfigurationCommand,
} from '../protocol.js';
import type { WebviewHost, WebviewProvider } from '../webviewProvider.js';
import type { State } from './protocol.js';
import {
	DidChangeAccountNotification,
	DidChangeIssueIntegrationConnectedNotification,
	DidOpenAnchorNotification,
	GenerateConfigurationPreviewRequest,
} from './protocol.js';
import type { SettingsWebviewShowingArgs } from './registration.js';

export class SettingsWebviewProvider implements WebviewProvider<State, State, SettingsWebviewShowingArgs> {
	private readonly _disposable: Disposable;
	private _pendingJumpToAnchor: string | undefined;

	constructor(
		protected readonly container: Container,
		protected readonly host: WebviewHost<'gitlens.settings'>,
	) {
		this._disposable = Disposable.from(
			configuration.onDidChangeAny(this.onAnyConfigurationChanged, this),
			container.subscription.onDidChange(this.onSubscriptionChanged, this),
			container.integrations.onDidChangeConnectionState(this.onIntegrationConnectionStateChanged, this),
		);
	}

	dispose(): void {
		this._disposable.dispose();
	}

	getTelemetryContext(): WebviewTelemetryContext {
		return {
			...this.host.getTelemetryContext(),
		};
	}

	private onSubscriptionChanged(e: SubscriptionChangeEvent) {
		void this.host.notify(DidChangeAccountNotification, { hasAccount: e.current.account != null });
	}

	private onIntegrationConnectionStateChanged(e: ConnectionStateChangeEvent) {
		const key = e.key;
		if (isIssueCloudIntegrationId(key)) {
			void this.host.notify(DidChangeIssueIntegrationConnectedNotification, {
				integrationId: key,
				connected: e.reason === 'connected',
			});
		}
	}

	async getAccountState(): Promise<boolean> {
		return (await this.container.subscription.getSubscription()).account != null;
	}

	async getJiraConnected(): Promise<boolean> {
		const jira = await this.container.integrations.get(IssuesCloudHostIntegrationId.Jira);
		if (jira == null) return false;
		return jira.maybeConnected ?? jira.isConnected();
	}

	async getLinearConnected(): Promise<boolean> {
		const linear = await this.container.integrations.get(IssuesCloudHostIntegrationId.Linear);
		if (linear == null) return false;
		return linear.maybeConnected ?? linear.isConnected();
	}

	async includeBootstrap(_deferrable?: boolean): Promise<State> {
		const scopes: ['user' | 'workspace', string][] = [['user', 'User']];
		if (workspace.workspaceFolders?.length) {
			scopes.push(['workspace', 'Workspace']);
		}

		const [jiraConnected, linearConnected] = (
			await Promise.allSettled([this.getJiraConnected(), this.getLinearConnected()])
		).map(r => getSettledValue(r, false));

		return {
			...this.host.baseWebviewState,
			version: this.container.version,
			// Make sure to get the raw config, not from the container which has the modes mixed in
			config: configuration.getAll(true),
			customSettings: this.getCustomSettings(),
			scope: 'user',
			scopes: scopes,
			hasAccount: await this.getAccountState(),
			hasConnectedJira: jiraConnected,
			hasConnectedLinear: linearConnected,
		};
	}

	onReloaded(): void {
		void this.notifyDidChangeConfiguration();
	}

	onShowing?(
		loading: boolean,
		_options: { column?: ViewColumn; preserveFocus?: boolean },
		...args: SettingsWebviewShowingArgs
	): [boolean, Record<`context.${string}`, string | number | boolean> | undefined] {
		const anchor = args[0];
		if (anchor && typeof anchor === 'string') {
			if (!loading && this.host.ready && this.host.visible) {
				queueMicrotask(
					() =>
						void this.host.notify(DidOpenAnchorNotification, {
							anchor: anchor,
							scrollBehavior: 'smooth',
						}),
				);
				return [true, undefined];
			}

			this._pendingJumpToAnchor = anchor;
		}

		return [true, undefined];
	}

	onActiveChanged(active: boolean): void {
		// Anytime the webview becomes active, make sure it has the most up-to-date config
		if (active) {
			void this.notifyDidChangeConfiguration();
		}
	}

	onReady(): void {
		if (this._pendingJumpToAnchor != null) {
			const anchor = this._pendingJumpToAnchor;
			this._pendingJumpToAnchor = undefined;

			void this.host.notify(DidOpenAnchorNotification, { anchor: anchor, scrollBehavior: 'auto' });
		}
	}

	@ipcCommand(UpdateConfigurationCommand)
	private async onUpdateConfiguration(params: IpcParams<typeof UpdateConfigurationCommand>) {
		const target = params.scope === 'workspace' ? ConfigurationTarget.Workspace : ConfigurationTarget.Global;

		let key: keyof typeof params.changes;
		for (key in params.changes) {
			let value = params.changes[key];

			if (isCustomConfigKey(key)) {
				const customSetting = this.customSettings.get(key);
				if (customSetting != null) {
					if (typeof value === 'boolean') {
						await customSetting.update(value);
					} else {
						debugger;
					}
				}

				continue;
			}

			assertsConfigKeyValue(key, value);

			const inspect = configuration.inspect(key)!;

			if (value != null) {
				if (params.scope === 'workspace') {
					if (value === inspect.workspaceValue) continue;
				} else {
					if (value === inspect.globalValue && value !== inspect.defaultValue) continue;

					if (value === inspect.defaultValue) {
						value = undefined;
					}
				}
			}

			await configuration.update(key, value, target);
		}

		for (const key of params.removes) {
			await configuration.update(key as ConfigPath, undefined, target);
		}
	}

	@ipcRequest(GenerateConfigurationPreviewRequest)
	private onGenerateConfigurationPreview(
		params: IpcParams<typeof GenerateConfigurationPreviewRequest>,
	): IpcResponse<typeof GenerateConfigurationPreviewRequest> {
		switch (params.type) {
			case 'commit':
			case 'commit-uncommitted': {
				const commit = new GitCommit(
					'~/code/eamodio/vscode-gitlens-demo',
					'fe26af408293cba5b4bfd77306e1ac9ff7ccaef8',
					new GitCommitIdentity(
						'Eric Amodio',
						'eamodio@gmail.com',
						new Date('2016-11-12T20:41:00.000Z'),
						undefined,
						true,
					),
					new GitCommitIdentity(
						'Eric Amodio',
						'eamodio@gmail.com',
						new Date('2020-11-01T06:57:21.000Z'),
						undefined,
						true,
					),
					params.type === 'commit-uncommitted' ? 'Uncommitted changes' : 'Supercharged',
					['3ac1d3f51d7cf5f438cc69f25f6740536ad80fef'],
					params.type === 'commit-uncommitted' ? 'Uncommitted changes' : 'Supercharged',
					{
						files: undefined,
						filtered: {
							files: [
								new GitFileChange(
									'~/code/eamodio/vscode-gitlens-demo',
									'code.ts',
									GitFileIndexStatus.Modified,
									joinUriPath(fileUri('/code/eamodio/vscode-gitlens-demo'), 'code.ts'),
								),
							],
							pathspec: 'code.ts',
						},
					},
					undefined,
					[],
				);

				let includePullRequest = false;
				switch (params.key) {
					case configuration.name('currentLine.format'):
						includePullRequest = configuration.get('currentLine.pullRequests.enabled');
						break;
					case configuration.name('statusBar.format'):
						includePullRequest = configuration.get('statusBar.pullRequests.enabled');
						break;
				}

				let pr: PullRequest | undefined;
				if (includePullRequest) {
					pr = new PullRequest(
						{ id: 'github', name: 'GitHub', domain: 'github.com', icon: 'github' },
						{
							id: 'eamodio',
							name: 'Eric Amodio',
							avatarUrl: 'https://avatars1.githubusercontent.com/u/641685?s=32&v=4',
							url: 'https://github.com/eamodio',
						},
						'1',
						undefined,
						'Supercharged',
						'https://github.com/gitkraken/vscode-gitlens/pulls/1',
						{ owner: 'gitkraken', repo: 'vscode-gitlens' },
						'merged',
						new Date('Sat, 12 Nov 2016 19:41:00 GMT'),
						new Date('Sat, 12 Nov 2016 19:41:00 GMT'),
						undefined,
						new Date('Sat, 12 Nov 2016 20:41:00 GMT'),
					);
				}

				let preview;
				try {
					preview = CommitFormatter.fromTemplate(params.format, commit, {
						dateFormat: configuration.get('defaultDateFormat'),
						pullRequest: pr,
						messageTruncateAtNewLine: true,
					});
				} catch {
					preview = 'Invalid format';
				}

				return { preview: preview };
			}
		}
	}

	private onAnyConfigurationChanged(e: ConfigurationChangeEvent) {
		if (!configuration.changedAny(e, extensionPrefix)) {
			const notify = configuration.changedAny<CustomSetting['name']>(e, [
				...map(this.customSettings.values(), s => s.name),
			]);
			if (!notify) return;
		}

		void this.notifyDidChangeConfiguration();
	}

	private _customSettings: Map<CustomConfigPath, CustomSetting> | undefined;
	private get customSettings() {
		this._customSettings ??= new Map<CustomConfigPath, CustomSetting>([
			[
				'rebaseEditor.enabled',
				{
					name: 'workbench.editorAssociations',
					enabled: () => this.container.rebaseEditor.enabled,
					update: this.container.rebaseEditor.setEnabled,
				},
			],
			[
				'currentLine.useUncommittedChangesFormat',
				{
					name: 'currentLine.uncommittedChangesFormat',
					enabled: () => configuration.get('currentLine.uncommittedChangesFormat') != null,
					update: async enabled =>
						configuration.updateEffective(
							'currentLine.uncommittedChangesFormat',
							// eslint-disable-next-line no-template-curly-in-string
							enabled ? '\u270F\ufe0f ${ago}' : null,
						),
				},
			],
		]);
		return this._customSettings;
	}

	protected getCustomSettings(): Record<string, boolean> {
		const customSettings: Record<string, boolean> = Object.create(null);
		for (const [key, setting] of this.customSettings) {
			customSettings[key] = setting.enabled();
		}
		return customSettings;
	}

	private notifyDidChangeConfiguration() {
		// Make sure to get the raw config, not from the container which has the modes mixed in
		return this.host.notify(DidChangeConfigurationNotification, {
			config: configuration.getAll(true),
			customSettings: this.getCustomSettings(),
		});
	}
}

interface CustomSetting {
	name: ConfigPath | CoreConfigPath;
	enabled: () => boolean;
	update: (enabled: boolean) => Promise<void>;
}
