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

import { IScriptingService } from 'sql/platform/scripting/common/scriptingService';
import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { TreeViewItemHandleArg } from 'sql/workbench/common/views';
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
import { IOEShimService } from 'sql/workbench/services/objectExplorer/browser/objectExplorerViewTreeShim';
import { IQueryEditorService } from 'sql/workbench/services/queryEditor/common/queryEditorService';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
import { BaseActionContext } from 'sql/workbench/browser/actions';
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
import { ScriptCreateAction, ScriptDeleteAction, ScriptSelectAction, ScriptExecuteAction, ScriptAlterAction, EditDataAction } from 'sql/workbench/browser/scriptingActions';
import { ObjectExplorerActionsContext, getTreeNode } from 'sql/workbench/services/objectExplorer/browser/objectExplorerActions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/browser/objectExplorerService';
import { TreeSelectionHandler } from 'sql/workbench/services/objectExplorer/browser/treeSelectionHandler';
import { TreeUpdateUtils } from 'sql/workbench/services/objectExplorer/browser/treeUpdateUtils';
import { VIEWLET_ID } from 'sql/workbench/contrib/dataExplorer/browser/dataExplorerViewlet';
import { ILogService } from 'vs/platform/log/common/log';
import { getErrorMessage } from 'vs/base/common/errors';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { localize } from 'vs/nls';
import { ServicesAccessor } from 'vs/editor/browser/editorExtensions';
import { AsyncServerTree } from 'sql/workbench/services/objectExplorer/browser/asyncServerTree';
import { Action, toAction } from 'vs/base/common/actions';

//#region -- Data Explorer
export const SCRIPT_AS_CREATE_COMMAND_ID = 'dataExplorer.scriptAsCreate';
export const SCRIPT_AS_DELETE_COMMAND_ID = 'dataExplorer.scriptAsDelete';
export const SCRIPT_AS_SELECT_COMMAND_ID = 'dataExplorer.scriptAsSelect';
export const SCRIPT_AS_EXECUTE_COMMAND_ID = 'dataExplorer.scriptAsExecute';
export const SCRIPT_AS_ALTER_COMMAND_ID = 'dataExplorer.scriptAsAlter';
export const EDIT_DATA_COMMAND_ID = 'dataExplorer.scriptAsEdit';

// Script as Create
CommandsRegistry.registerCommand({
	id: SCRIPT_AS_CREATE_COMMAND_ID,
	handler: async (accessor, args: TreeViewItemHandleArg): Promise<void> => {
		if (args.$treeItem?.payload) {
			const capabilitiesService = accessor.get(ICapabilitiesService);
			const oeShimService = accessor.get(IOEShimService);
			const queryEditorService = accessor.get(IQueryEditorService);
			const connectionManagementService = accessor.get(IConnectionManagementService);
			const scriptingService = accessor.get(IScriptingService);
			const errorMessageService = accessor.get(IErrorMessageService);
			const progressService = accessor.get(IProgressService);
			const connectionService = accessor.get(IConnectionManagementService);
			let payload = await connectionService.fixProfile(args.$treeItem.payload);
			const profile = new ConnectionProfile(capabilitiesService, payload);
			const baseContext: BaseActionContext = {
				profile: profile,
				object: oeShimService.getNodeInfoForTreeItem(args.$treeItem)!.metadata
			};
			const scriptCreateAction = new ScriptCreateAction(ScriptCreateAction.ID, ScriptCreateAction.LABEL,
				queryEditorService, connectionManagementService, scriptingService, errorMessageService);
			await progressService.withProgress({ location: VIEWLET_ID }, async () => await scriptCreateAction.run(baseContext));
		}
	}
});

// Script as Delete
CommandsRegistry.registerCommand({
	id: SCRIPT_AS_DELETE_COMMAND_ID,
	handler: async (accessor, args: TreeViewItemHandleArg): Promise<void> => {
		if (args.$treeItem?.payload) {
			const capabilitiesService = accessor.get(ICapabilitiesService);
			const oeShimService = accessor.get(IOEShimService);
			const queryEditorService = accessor.get(IQueryEditorService);
			const connectionManagementService = accessor.get(IConnectionManagementService);
			const scriptingService = accessor.get(IScriptingService);
			const errorMessageService = accessor.get(IErrorMessageService);
			const progressService = accessor.get(IProgressService);
			const connectionService = accessor.get(IConnectionManagementService);
			let payload = await connectionService.fixProfile(args.$treeItem.payload);
			const profile = new ConnectionProfile(capabilitiesService, payload);
			const baseContext: BaseActionContext = {
				profile: profile,
				object: oeShimService.getNodeInfoForTreeItem(args.$treeItem)!.metadata
			};
			const scriptDeleteAction = new ScriptDeleteAction(ScriptDeleteAction.ID, ScriptDeleteAction.LABEL,
				queryEditorService, connectionManagementService, scriptingService, errorMessageService);
			await progressService.withProgress({ location: VIEWLET_ID }, async () => await scriptDeleteAction.run(baseContext));
		}
	}
});

// Script as Select
CommandsRegistry.registerCommand({
	id: SCRIPT_AS_SELECT_COMMAND_ID,
	handler: async (accessor, args: TreeViewItemHandleArg): Promise<void> => {
		if (args.$treeItem?.payload) {
			const capabilitiesService = accessor.get(ICapabilitiesService);
			const oeShimService = accessor.get(IOEShimService);
			const queryEditorService = accessor.get(IQueryEditorService);
			const connectionManagementService = accessor.get(IConnectionManagementService);
			const scriptingService = accessor.get(IScriptingService);
			const progressService = accessor.get(IProgressService);
			const errorMessageService = accessor.get(IErrorMessageService);
			const connectionService = accessor.get(IConnectionManagementService);
			let payload = await connectionService.fixProfile(args.$treeItem.payload);
			const profile = new ConnectionProfile(capabilitiesService, payload);
			const baseContext: BaseActionContext = {
				profile: profile,
				object: oeShimService.getNodeInfoForTreeItem(args.$treeItem)!.metadata
			};
			const scriptSelectAction = new ScriptSelectAction(ScriptSelectAction.ID, ScriptSelectAction.LABEL,
				queryEditorService, connectionManagementService, scriptingService, errorMessageService);
			await progressService.withProgress({ location: VIEWLET_ID }, async () => await scriptSelectAction.run(baseContext));
		}
	}
});

// Script as Execute
CommandsRegistry.registerCommand({
	id: SCRIPT_AS_EXECUTE_COMMAND_ID,
	handler: async (accessor, args: TreeViewItemHandleArg): Promise<void> => {
		if (args.$treeItem?.payload) {
			const capabilitiesService = accessor.get(ICapabilitiesService);
			const oeShimService = accessor.get(IOEShimService);
			const queryEditorService = accessor.get(IQueryEditorService);
			const connectionManagementService = accessor.get(IConnectionManagementService);
			const scriptingService = accessor.get(IScriptingService);
			const progressService = accessor.get(IProgressService);
			const errorMessageService = accessor.get(IErrorMessageService);
			const connectionService = accessor.get(IConnectionManagementService);
			let payload = await connectionService.fixProfile(args.$treeItem.payload);
			const profile = new ConnectionProfile(capabilitiesService, payload);
			const baseContext: BaseActionContext = {
				profile: profile,
				object: oeShimService.getNodeInfoForTreeItem(args.$treeItem)!.metadata
			};
			const scriptExecuteAction = new ScriptExecuteAction(ScriptExecuteAction.ID, ScriptExecuteAction.LABEL,
				queryEditorService, connectionManagementService, scriptingService, errorMessageService);
			await progressService.withProgress({ location: VIEWLET_ID }, async () => await scriptExecuteAction.run(baseContext));
		}
	}
});

// Script as Alter
CommandsRegistry.registerCommand({
	id: SCRIPT_AS_ALTER_COMMAND_ID,
	handler: async (accessor, args: TreeViewItemHandleArg): Promise<void> => {
		if (args.$treeItem?.payload) {
			const capabilitiesService = accessor.get(ICapabilitiesService);
			const oeShimService = accessor.get(IOEShimService);
			const queryEditorService = accessor.get(IQueryEditorService);
			const connectionManagementService = accessor.get(IConnectionManagementService);
			const scriptingService = accessor.get(IScriptingService);
			const progressService = accessor.get(IProgressService);
			const errorMessageService = accessor.get(IErrorMessageService);
			const connectionService = accessor.get(IConnectionManagementService);
			let payload = await connectionService.fixProfile(args.$treeItem.payload);
			const profile = new ConnectionProfile(capabilitiesService, payload);
			const baseContext: BaseActionContext = {
				profile: profile,
				object: oeShimService.getNodeInfoForTreeItem(args.$treeItem)!.metadata
			};
			const scriptAlterAction = new ScriptAlterAction(ScriptAlterAction.ID, ScriptAlterAction.LABEL,
				queryEditorService, connectionManagementService, scriptingService, errorMessageService);
			await progressService.withProgress({ location: VIEWLET_ID }, async () => await scriptAlterAction.run(baseContext));
		}
	}
});

// Edit Data
CommandsRegistry.registerCommand({
	id: EDIT_DATA_COMMAND_ID,
	handler: async (accessor, args: TreeViewItemHandleArg): Promise<void> => {
		if (args.$treeItem?.payload) {
			const capabilitiesService = accessor.get(ICapabilitiesService);
			const oeShimService = accessor.get(IOEShimService);
			const queryEditorService = accessor.get(IQueryEditorService);
			const connectionManagementService = accessor.get(IConnectionManagementService);
			const scriptingService = accessor.get(IScriptingService);
			const progressService = accessor.get(IProgressService);
			const errorMessageService = accessor.get(IErrorMessageService);
			const connectionService = accessor.get(IConnectionManagementService);
			let payload = await connectionService.fixProfile(args.$treeItem.payload);
			const profile = new ConnectionProfile(capabilitiesService, payload);
			const baseContext: BaseActionContext = {
				profile: profile,
				object: oeShimService.getNodeInfoForTreeItem(args.$treeItem)!.metadata
			};
			const editDataAction = new EditDataAction(EditDataAction.ID, EditDataAction.LABEL,
				queryEditorService, connectionManagementService, scriptingService, errorMessageService);
			await progressService.withProgress({ location: VIEWLET_ID }, async () => await editDataAction.run(baseContext));
		}
	}
});
//#endregion

//#region -- Object Explorer

export const OE_SCRIPT_AS_CREATE_COMMAND_ID = 'objectExplorer.scriptAsCreate';
export const OE_SCRIPT_AS_DELETE_COMMAND_ID = 'objectExplorer.scriptAsDelete';
export const OE_SCRIPT_AS_SELECT_COMMAND_ID = 'objectExplorer.scriptAsSelect';
export const OE_SCRIPT_AS_EXECUTE_COMMAND_ID = 'objectExplorer.scriptAsExecute';
export const OE_SCRIPT_AS_ALTER_COMMAND_ID = 'objectExplorer.scriptAsAlter';
export const OE_EDIT_DATA_COMMAND_ID = 'objectExplorer.scriptAsEdit';
export const OE_REFRESH_COMMAND_ID = 'objectExplorer.refreshNode';


async function runScriptingAction(actionDisplayName: string, accessor: ServicesAccessor, args: ObjectExplorerActionsContext, createAction: (instantiationService: IInstantiationService) => Action): Promise<void> {
	const instantiationService = accessor.get(IInstantiationService);
	const objectExplorerService = accessor.get(IObjectExplorerService);
	const notificationService = accessor.get(INotificationService);
	const logService = accessor.get(ILogService);
	const selectionHandler = instantiationService.createInstance(TreeSelectionHandler);
	const notificationHandle = notificationService.notify({
		message: localize('scriptingAction.inProgress', "Executing action: {0}...", actionDisplayName),
		severity: Severity.Info,
		progress: {
			infinite: true
		}
	});
	try {
		const node = (await getTreeNode(args, objectExplorerService))!;
		selectionHandler.onTreeActionStateChange(true);
		let connectionProfile = TreeUpdateUtils.getConnectionProfile(node)!;
		let metadata = node.metadata;
		const action = createAction(instantiationService);
		await action.run({ profile: connectionProfile, object: metadata })
		selectionHandler.onTreeActionStateChange(false);
		notificationHandle.close();
	} catch (error) {
		const msg = localize('scriptActionError', "An error occurred while executing the action '{0}': {1}.", actionDisplayName, getErrorMessage(error));
		logService.error(msg);
		notificationHandle.updateSeverity(Severity.Error);
		notificationHandle.updateMessage(msg);
		notificationHandle.progress.done();
		notificationHandle.updateActions({
			primary: [
				toAction({
					id: 'closeScriptingNotification',
					label: localize('scriptingAction.close', "Close"),
					run: () => {
						notificationHandle.close();
					}
				})]
		})
	}
}

// Script as Select
CommandsRegistry.registerCommand({
	id: OE_SCRIPT_AS_SELECT_COMMAND_ID,
	handler: async (accessor, args: ObjectExplorerActionsContext) => {
		const displayName = args.connectionProfile.providerName === 'KUSTO' || args.connectionProfile.providerName === 'LOGANALYTICS' ? ScriptSelectAction.KUSTOLABEL : ScriptSelectAction.LABEL;
		return runScriptingAction(displayName, accessor, args, (instantiationService) => {
			return instantiationService.createInstance(ScriptSelectAction, ScriptSelectAction.ID, displayName);
		});
	}
});

// Edit Data
CommandsRegistry.registerCommand({
	id: OE_EDIT_DATA_COMMAND_ID,
	handler: async (accessor, args: ObjectExplorerActionsContext) => {
		return runScriptingAction(EditDataAction.LABEL, accessor, args, (instantiationService) => {
			return instantiationService.createInstance(EditDataAction, EditDataAction.ID, EditDataAction.LABEL);
		});
	}
});

// Script as Create
CommandsRegistry.registerCommand({
	id: OE_SCRIPT_AS_CREATE_COMMAND_ID,
	handler: async (accessor, args: ObjectExplorerActionsContext) => {
		return runScriptingAction(ScriptCreateAction.LABEL, accessor, args, (instantiationService) => {
			return instantiationService.createInstance(ScriptCreateAction, ScriptCreateAction.ID, ScriptCreateAction.LABEL);
		});
	}
});

// Script as Execute
CommandsRegistry.registerCommand({
	id: OE_SCRIPT_AS_EXECUTE_COMMAND_ID,
	handler: async (accessor, args: ObjectExplorerActionsContext) => {
		return runScriptingAction(ScriptExecuteAction.LABEL, accessor, args, (instantiationService) => {
			return instantiationService.createInstance(ScriptExecuteAction, ScriptExecuteAction.ID, ScriptExecuteAction.LABEL);
		});
	}
});

// Script as Alter
CommandsRegistry.registerCommand({
	id: OE_SCRIPT_AS_ALTER_COMMAND_ID,
	handler: async (accessor, args: ObjectExplorerActionsContext) => {
		return runScriptingAction(ScriptAlterAction.LABEL, accessor, args, (instantiationService) => {
			return instantiationService.createInstance(ScriptAlterAction, ScriptAlterAction.ID, ScriptAlterAction.LABEL);
		});
	}
});


// Script as Delete
CommandsRegistry.registerCommand({
	id: OE_SCRIPT_AS_DELETE_COMMAND_ID,
	handler: async (accessor, args: ObjectExplorerActionsContext) => {
		return runScriptingAction(ScriptDeleteAction.LABEL, accessor, args, (instantiationService) => {
			return instantiationService.createInstance(ScriptDeleteAction, ScriptDeleteAction.ID, ScriptDeleteAction.LABEL);
		});
	}
});

// Refresh Action for Scriptable objects
CommandsRegistry.registerCommand({
	id: OE_REFRESH_COMMAND_ID,
	handler: handleOeRefreshCommand
});

export async function handleOeRefreshCommand(accessor: ServicesAccessor, args: ObjectExplorerActionsContext): Promise<void> {
	const objectExplorerService = accessor.get(IObjectExplorerService);
	const logService = accessor.get(ILogService);
	const notificationService = accessor.get(INotificationService);
	const treeNode = await getTreeNode(args, objectExplorerService);
	const tree = objectExplorerService.getServerTreeView()!.tree;
	try {
		await objectExplorerService.refreshTreeNode(treeNode!.getSession()!, treeNode!);
		if (tree instanceof AsyncServerTree) {
			await tree.updateChildren(treeNode);
		} else {
			await tree.refresh(treeNode);
		}
	} catch (err) {
		// Display message to the user but also log the entire error to the console for the stack trace
		notificationService.error(localize('refreshError', "An error occurred refreshing node '{0}': {1}", args.nodeInfo?.label, getErrorMessage(err)));
		logService.error(err);
	}
}
//#endregion

//#region -- explorer widget

export class ExplorerScriptSelectAction extends ScriptSelectAction {
	constructor(
		id: string, label: string,
		@IQueryEditorService queryEditorService: IQueryEditorService,
		@IConnectionManagementService connectionManagementService: IConnectionManagementService,
		@IScriptingService scriptingService: IScriptingService,
		@IProgressService private readonly progressService: IProgressService,
		@IErrorMessageService errorMessageService: IErrorMessageService
	) {
		super(id, label, queryEditorService, connectionManagementService, scriptingService, errorMessageService);
	}

	public override async run(actionContext: BaseActionContext): Promise<void> {
		await this.progressService.withProgress({ location: ProgressLocation.Window }, async () => await super.run(actionContext));
	}
}

export class ExplorerScriptCreateAction extends ScriptCreateAction {
	constructor(
		id: string, label: string,
		@IQueryEditorService queryEditorService: IQueryEditorService,
		@IConnectionManagementService connectionManagementService: IConnectionManagementService,
		@IScriptingService scriptingService: IScriptingService,
		@IErrorMessageService errorMessageService: IErrorMessageService,
		@IProgressService private readonly progressService: IProgressService
	) {
		super(id, label, queryEditorService, connectionManagementService, scriptingService, errorMessageService);
	}

	public override async run(actionContext: BaseActionContext): Promise<void> {
		await this.progressService.withProgress({ location: ProgressLocation.Window }, async () => await super.run(actionContext));
	}
}

export class ExplorerScriptAlterAction extends ScriptAlterAction {
	constructor(
		id: string, label: string,
		@IQueryEditorService queryEditorService: IQueryEditorService,
		@IConnectionManagementService connectionManagementService: IConnectionManagementService,
		@IScriptingService scriptingService: IScriptingService,
		@IErrorMessageService errorMessageService: IErrorMessageService,
		@IProgressService private readonly progressService: IProgressService
	) {
		super(id, label, queryEditorService, connectionManagementService, scriptingService, errorMessageService);
	}

	public override async run(actionContext: BaseActionContext): Promise<void> {
		await this.progressService.withProgress({ location: ProgressLocation.Window }, async () => await super.run(actionContext));
	}
}

export class ExplorerScriptExecuteAction extends ScriptExecuteAction {
	constructor(
		id: string, label: string,
		@IQueryEditorService queryEditorService: IQueryEditorService,
		@IConnectionManagementService connectionManagementService: IConnectionManagementService,
		@IScriptingService scriptingService: IScriptingService,
		@IErrorMessageService errorMessageService: IErrorMessageService,
		@IProgressService private readonly progressService: IProgressService
	) {
		super(id, label, queryEditorService, connectionManagementService, scriptingService, errorMessageService);
	}

	public override async run(actionContext: BaseActionContext): Promise<void> {
		await this.progressService.withProgress({ location: ProgressLocation.Window }, async () => await super.run(actionContext));
	}
}
//#endregion
