/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import { localize } from 'vs/nls';
import { IExtensionManagementService, IGlobalExtensionEnablementService, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { ExtensionType, IExtension } from 'vs/platform/extensions/common/extensions';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { createDecorator, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions';
import { ContextKeyEqualsExpr, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { Registry } from 'vs/platform/registry/common/platform';
import { Extensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { ILogService } from 'vs/platform/log/common/log';
import { IProductService } from 'vs/platform/product/common/productService';
import { IWorkbenchIssueService } from 'vs/workbench/services/issue/common/issue';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';

// --- bisect service

export const IExtensionBisectService = createDecorator<IExtensionBisectService>('IExtensionBisectService');

export interface IExtensionBisectService {

	readonly _serviceBrand: undefined;

	isDisabledByBisect(extension: IExtension): boolean;
	isActive: boolean;
	disabledCount: number;
	start(extensions: ILocalExtension[]): Promise<void>;
	next(seeingBad: boolean): Promise<{ id: string, bad: boolean } | undefined>;
	reset(): Promise<void>;
}

class BisectState {

	static fromJSON(raw: string | undefined): BisectState | undefined {
		if (!raw) {
			return undefined;
		}
		try {
			interface Raw extends BisectState { }
			const data: Raw = JSON.parse(raw);
			return new BisectState(data.extensions, data.low, data.high, data.mid);
		} catch {
			return undefined;
		}
	}

	constructor(
		readonly extensions: string[],
		readonly low: number,
		readonly high: number,
		readonly mid: number = ((low + high) / 2) | 0
	) { }
}

class ExtensionBisectService implements IExtensionBisectService {

	declare readonly _serviceBrand: undefined;

	private static readonly _storageKey = 'extensionBisectState';

	private readonly _state: BisectState | undefined;
	private readonly _disabled = new Map<string, boolean>();

	constructor(
		@ILogService logService: ILogService,
		@IStorageService private readonly _storageService: IStorageService,
		@IWorkbenchEnvironmentService private readonly _envService: IWorkbenchEnvironmentService
	) {
		const raw = _storageService.get(ExtensionBisectService._storageKey, StorageScope.GLOBAL);
		this._state = BisectState.fromJSON(raw);

		if (this._state) {
			const { mid, high } = this._state;
			for (let i = 0; i < this._state.extensions.length; i++) {
				const isDisabled = i >= mid && i < high;
				this._disabled.set(this._state.extensions[i], isDisabled);
			}
			logService.warn('extension BISECT active', [...this._disabled]);
		}
	}

	get isActive() {
		return !!this._state;
	}

	get disabledCount() {
		return this._state ? this._state.high - this._state.mid : -1;
	}

	isDisabledByBisect(extension: IExtension): boolean {
		if (!this._state) {
			// bisect isn't active
			return false;
		}
		if (this._isRemoteResolver(extension)) {
			// the current remote resolver extension cannot be disabled
			return false;
		}
		const disabled = this._disabled.get(extension.identifier.id);
		return disabled ?? false;
	}

	private _isRemoteResolver(extension: IExtension): boolean {
		if (extension.manifest.enableProposedApi !== true) {
			return false;
		}
		const idx = this._envService.remoteAuthority?.indexOf('+');
		const activationEvent = `onResolveRemoteAuthority:${this._envService.remoteAuthority?.substr(0, idx)}`;
		return Boolean(extension.manifest.activationEvents?.find(e => e === activationEvent));
	}

	async start(extensions: ILocalExtension[]): Promise<void> {
		if (this._state) {
			throw new Error('invalid state');
		}
		const extensionIds = extensions.map(ext => ext.identifier.id);
		const newState = new BisectState(extensionIds, 0, extensionIds.length, 0);
		this._storageService.store(ExtensionBisectService._storageKey, JSON.stringify(newState), StorageScope.GLOBAL, StorageTarget.MACHINE);
		await this._storageService.flush();
	}

	async next(seeingBad: boolean): Promise<{ id: string; bad: boolean; } | undefined> {
		if (!this._state) {
			throw new Error('invalid state');
		}
		// check if bad when all extensions are disabled
		if (seeingBad && this._state.mid === 0 && this._state.high === this._state.extensions.length) {
			return { bad: true, id: '' };
		}
		// check if there is only one left
		if (this._state.low === this._state.high - 1) {
			await this.reset();
			return { id: this._state.extensions[this._state.low], bad: seeingBad };
		}
		// the second half is disabled so if there is still bad it must be
		// in the first half
		const nextState = new BisectState(
			this._state.extensions,
			seeingBad ? this._state.low : this._state.mid,
			seeingBad ? this._state.mid : this._state.high,
		);
		this._storageService.store(ExtensionBisectService._storageKey, JSON.stringify(nextState), StorageScope.GLOBAL, StorageTarget.MACHINE);
		await this._storageService.flush();
		return undefined;
	}

	async reset(): Promise<void> {
		this._storageService.remove(ExtensionBisectService._storageKey, StorageScope.GLOBAL);
		await this._storageService.flush();
	}
}

registerSingleton(IExtensionBisectService, ExtensionBisectService, true);

// --- bisect UI

class ExtensionBisectUi {

	static ctxIsBisectActive = new RawContextKey('isExtensionBisectActive', false);

	constructor(
		@IContextKeyService contextKeyService: IContextKeyService,
		@IExtensionBisectService private readonly _extensionBisectService: IExtensionBisectService,
		@INotificationService private readonly _notificationService: INotificationService,
		@ICommandService private readonly _commandService: ICommandService,
	) {
		if (_extensionBisectService.isActive) {
			ExtensionBisectUi.ctxIsBisectActive.bindTo(contextKeyService).set(true);
			this._showBisectPrompt();
		}
	}

	private _showBisectPrompt(): void {

		const goodPrompt: IPromptChoice = {
			label: 'Good now',
			run: () => this._commandService.executeCommand('extension.bisect.next', false)
		};
		const badPrompt: IPromptChoice = {
			label: 'This is bad',
			run: () => this._commandService.executeCommand('extension.bisect.next', true)
		};
		const stop: IPromptChoice = {
			label: 'Stop Bisect',
			run: () => this._commandService.executeCommand('extension.bisect.stop')
		};

		this._notificationService.prompt(
			Severity.Info,
			localize('bisect', "Extension Bisect is active and has disabled {0} extensions. Check if you can still reproduce the problem and proceed by selecting from these options.", this._extensionBisectService.disabledCount),
			[goodPrompt, badPrompt, stop],
			{ sticky: true }
		);
	}
}

Registry.as<IWorkbenchContributionsRegistry>(Extensions.Workbench).registerWorkbenchContribution(
	ExtensionBisectUi,
	LifecyclePhase.Restored
);

registerAction2(class extends Action2 {
	constructor() {
		super({
			id: 'extension.bisect.start',
			title: { value: localize('title.start', "Start Extension Bisect"), original: 'Start Extension Bisect' },
			category: localize('help', "Help"),
			f1: true,
			precondition: ExtensionBisectUi.ctxIsBisectActive.negate(),
			menu: {
				id: MenuId.ViewContainerTitle,
				when: ContextKeyEqualsExpr.create('viewContainer', 'workbench.view.extensions'),
				group: '2_enablement',
				order: 3
			}
		});
	}

	async run(accessor: ServicesAccessor): Promise<void> {
		const dialogService = accessor.get(IDialogService);
		const hostService = accessor.get(IHostService);
		const extensionManagement = accessor.get(IExtensionManagementService);
		const extensionEnablementService = accessor.get(IGlobalExtensionEnablementService);
		const extensionsBisect = accessor.get(IExtensionBisectService);

		const disabled = new Set(extensionEnablementService.getDisabledExtensions().map(id => id.id));
		const extensions = (await extensionManagement.getInstalled(ExtensionType.User)).filter(ext => !disabled.has(ext.identifier.id));

		const res = await dialogService.confirm({
			message: localize('msg.start', "Extension Bisect"),
			detail: localize('detail.start', "Extension Bisect will use binary search to find an extension that causes a problem. During the process the window reloads repeatedly (~{0} times). Each time you must confirm if you are still seeing problems.", 2 + Math.log2(extensions.length) | 0),
			primaryButton: localize('msg2', "Start Extension Bisect")
		});

		if (res.confirmed) {
			await extensionsBisect.start(extensions);
			hostService.reload();
		}
	}
});

registerAction2(class extends Action2 {
	constructor() {
		super({
			id: 'extension.bisect.next',
			title: localize('title.isBad', "Continue Extension Bisect"),
			category: localize('help', "Help"),
			f1: true,
			precondition: ExtensionBisectUi.ctxIsBisectActive
		});
	}

	async run(accessor: ServicesAccessor, seeingBad: boolean | undefined): Promise<void> {
		const dialogService = accessor.get(IDialogService);
		const hostService = accessor.get(IHostService);
		const bisectService = accessor.get(IExtensionBisectService);
		const productService = accessor.get(IProductService);
		const extensionEnablementService = accessor.get(IGlobalExtensionEnablementService);
		const issueService = accessor.get(IWorkbenchIssueService);

		if (!bisectService.isActive) {
			return;
		}
		if (seeingBad === undefined) {
			const goodBadStopCancel = await this._checkForBad(dialogService, bisectService);
			if (goodBadStopCancel === null) {
				return;
			}
			seeingBad = goodBadStopCancel;
		}
		if (seeingBad === undefined) {
			await bisectService.reset();
			hostService.reload();
			return;
		}
		const done = await bisectService.next(seeingBad);
		if (!done) {
			hostService.reload();
			return;
		}

		if (done.bad) {
			// DONE but nothing found
			await dialogService.show(Severity.Info, localize('done.msg', "Extension Bisect"), [], {
				detail: localize('done.detail2', "Extension Bisect is done but no extension has been identified. This might be a problem with {0}.", productService.nameShort)
			});

		} else {
			// DONE and identified extension
			const res = await dialogService.show(Severity.Info, localize('done.msg', "Extension Bisect"),
				[localize('report', "Report Issue & Continue"), localize('done', "Continue")],
				// [],
				{
					detail: localize('done.detail', "Extension Bisect is done and has identified {0} as the extension causing the problem.", done.id),
					checkbox: { label: localize('done.disbale', "Keep this extension disabled"), checked: true },
					cancelId: 1
				}
			);
			if (res.checkboxChecked) {
				await extensionEnablementService.disableExtension({ id: done.id }, undefined);
			}
			if (res.choice === 0) {
				await issueService.openReporter({ extensionId: done.id });
			}
		}
		await bisectService.reset();
		hostService.reload();
	}

	private async _checkForBad(dialogService: IDialogService, bisectService: IExtensionBisectService): Promise<boolean | undefined | null> {
		const options = {
			cancelId: 3,
			detail: localize('bisect', "Extension Bisect is active and has disabled {0} extensions. Check if you can still reproduce the problem and proceed by selecting from these options.", bisectService.disabledCount),
		};
		const res = await dialogService.show(
			Severity.Info,
			localize('msg.next', "Extension Bisect"),
			[localize('next.good', "Good now"), localize('next.bad', "This is bad"), localize('next.stop', "Stop Bisect"), localize('next.cancel', "Cancel")],
			options
		);
		switch (res.choice) {
			case 0: return false; //good now
			case 1: return true; //bad
			case 2: return undefined; //stop
		}
		return null; //cancel
	}
});

registerAction2(class extends Action2 {
	constructor() {
		super({
			id: 'extension.bisect.stop',
			title: localize('title.stop', "Stop Extension Bisect"),
			category: localize('help', "Help"),
			f1: true,
			precondition: ExtensionBisectUi.ctxIsBisectActive
		});
	}

	async run(accessor: ServicesAccessor): Promise<void> {
		const extensionsBisect = accessor.get(IExtensionBisectService);
		const hostService = accessor.get(IHostService);
		await extensionsBisect.reset();
		hostService.reload();
	}
});
