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

import * as azExt from 'az-ext';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { SemVer } from 'semver';
import * as vscode from 'vscode';
import { executeCommand, executeSudoCommand, ExitCodeError, ProcessOutput } from './common/childProcess';
import { HttpClient } from './common/httpClient';
import Logger from './common/logger';
import { NoAzureCLIArcExtError, NoAzureCLIError, searchForCmd } from './common/utils';
import { azArcdataInstallKey, azConfigSection, azFound, debugConfigKey, latestAzArcExtensionVersion, azCliInstallKey, azArcFound, azHostname, azUri } from './constants';
import * as loc from './localizedConstants';

/**
 * The latest Azure CLI arcdata extension version for this extension to function properly
 */
export const LATEST_AZ_ARC_EXTENSION_VERSION = new SemVer(latestAzArcExtensionVersion);

export const enum AzDeployOption {
	dontPrompt = 'dontPrompt',
	prompt = 'prompt'
}

/**
 * Interface for an object to interact with the az tool installed on the box.
 */
export interface IAzTool extends azExt.IAzApi {
	/**
	 * Executes az with the specified arguments (e.g. --version) and returns the result
	 * @param args The args to pass to az
	 * @param parseResult A function used to parse out the raw result into the desired shape
	 */
	executeCommand<R>(args: string[], additionalEnvVars?: azExt.AdditionalEnvVars): Promise<azExt.AzOutput<R>>
}

/**
 * An object to interact with the az tool installed on the box.
 */
export class AzTool implements azExt.IAzApi {

	private _semVersionAz: SemVer;
	private _semVersionArc: SemVer;

	constructor(private _path: string, versionAz: string, versionArc: string) {
		this._semVersionAz = new SemVer(versionAz);
		this._semVersionArc = new SemVer(versionArc);
	}

	/**
	 * The semVersion corresponding to this installation of Azure CLI. version() method should have been run
	 * before fetching this value to ensure that correct value is returned. This is almost always correct unless
	 * Az has gotten reinstalled in the background after this IAzApi object was constructed.
	 */
	public async getSemVersionAz(): Promise<SemVer> {
		return this._semVersionAz;
	}

	/**
	 * The semVersion corresponding to this installation of Azure CLI arcdata extension. version() method should have been run
	 * before fetching this value to ensure that correct value is returned. This is almost always correct unless
	 * arcdata has gotten reinstalled in the background after this IAzApi object was constructed.
	 */
	public async getSemVersionArc(): Promise<SemVer> {
		return this._semVersionArc;
	}

	/**
	 * gets the path where az tool is installed
	 */
	public async getPath(): Promise<string> {
		return this._path;
	}

	public arcdata = {
		dc: {
			endpoint: {
				list: (namespace: string, additionalEnvVars?: azExt.AdditionalEnvVars): Promise<azExt.AzOutput<azExt.DcEndpointListResult[]>> => {
					return this.executeCommand<azExt.DcEndpointListResult[]>(['arcdata', 'dc', 'endpoint', 'list', '--k8s-namespace', namespace, '--use-k8s'], additionalEnvVars);
				}
			},
			config: {
				list: (additionalEnvVars?: azExt.AdditionalEnvVars): Promise<azExt.AzOutput<azExt.DcConfigListResult[]>> => {
					return this.executeCommand<azExt.DcConfigListResult[]>(['arcdata', 'dc', 'config', 'list'], additionalEnvVars);
				},
				show: (namespace: string, additionalEnvVars?: azExt.AdditionalEnvVars): Promise<azExt.AzOutput<azExt.DcConfigShowResult>> => {
					return this.executeCommand<azExt.DcConfigShowResult>(['arcdata', 'dc', 'config', 'show', '--k8s-namespace', namespace, '--use-k8s'], additionalEnvVars);
				}
			},
			listUpgrades: async (namespace: string, usek8s?: boolean, additionalEnvVars?: azExt.AdditionalEnvVars): Promise<azExt.AzOutput<azExt.DcListUpgradesResult>> => {
				const argsArray = ['arcdata', 'dc', 'list-upgrades'];
				if (namespace) { argsArray.push('--k8s-namespace', namespace); }
				if (usek8s) { argsArray.push('--use-k8s'); }

				const output = await this.executeCommand<string>(argsArray, additionalEnvVars);
				const versions = <string[]>parseDcListUpgrades(output.stdout);
				const currentVersion = <string>parseCurrentVersion(output.stdout);
				let dates: string[] = [];
				for (let i = 0; i < versions.length; i++) {
					dates.push(parseReleaseDateFromUpgrade(versions[i]));
				}
				return {
					stdout: {
						versions: versions,
						currentVersion: currentVersion,
						dates: dates
					},
					stderr: output.stderr
				};
			},
			upgrade: (desiredVersion: string, name: string, resourceGroup?: string, namespace?: string, additionalEnvVars?: azExt.AdditionalEnvVars): Promise<azExt.AzOutput<void>> => {
				const argsArray = ['arcdata', 'dc', 'upgrade', '--desired-version', desiredVersion, '--name', name];
				// Direct mode argument
				if (resourceGroup) { argsArray.push('--resource-group', resourceGroup); }
				// K8s API arguments
				if (namespace) {
					argsArray.push('--k8s-namespace', namespace);
					argsArray.push('--use-k8s');
				}
				return this.executeCommand<void>(argsArray, additionalEnvVars);
			}
		}
	};

	public postgres = {
		serverarc: {
			delete: (name: string, namespace: string, additionalEnvVars?: azExt.AdditionalEnvVars): Promise<azExt.AzOutput<void>> => {
				return this.executeCommand<void>(['postgres', 'server-arc', 'delete', '-n', name, '--k8s-namespace', namespace, '--force', '--use-k8s'], additionalEnvVars);
			},
			list: (namespace: string, additionalEnvVars?: azExt.AdditionalEnvVars): Promise<azExt.AzOutput<azExt.PostgresServerListResult[]>> => {
				return this.executeCommand<azExt.PostgresServerListResult[]>(['postgres', 'server-arc', 'list', '--k8s-namespace', namespace, '--use-k8s'], additionalEnvVars);
			},
			show: (name: string, namespace: string, additionalEnvVars?: azExt.AdditionalEnvVars): Promise<azExt.AzOutput<azExt.PostgresServerShowResult>> => {
				return this.executeCommand<azExt.PostgresServerShowResult>(['postgres', 'server-arc', 'show', '-n', name, '--k8s-namespace', namespace, '--use-k8s'], additionalEnvVars);
			},
			update: (
				name: string,
				args: {
					coresLimit?: string,
					coresRequest?: string,
					memoryLimit?: string,
					memoryRequest?: string,
					noWait?: boolean,
					port?: number
				},
				namespace: string,
				additionalEnvVars?: azExt.AdditionalEnvVars): Promise<azExt.AzOutput<void>> => {
				const argsArray = ['postgres', 'server-arc', 'update', '-n', name, '--k8s-namespace', namespace, '--use-k8s'];
				if (args.coresLimit) { argsArray.push('--cores-limit', args.coresLimit); }
				if (args.coresRequest) { argsArray.push('--cores-request', args.coresRequest); }
				if (args.memoryLimit) { argsArray.push('--memory-limit', args.memoryLimit); }
				if (args.memoryRequest) { argsArray.push('--memory-request', args.memoryRequest); }
				if (args.noWait) { argsArray.push('--no-wait'); }
				if (args.port) { argsArray.push('--port', args.port.toString()); }
				return this.executeCommand<void>(argsArray, additionalEnvVars);
			}
		}
	};

	public sql = {
		miarc: {
			delete: (
				name: string,
				args: {
					// ARM API arguments
					resourceGroup?: string,
					// K8s API arguments
					namespace?: string
					// Additional arguments
				},
				additionalEnvVars?: azExt.AdditionalEnvVars
			): Promise<azExt.AzOutput<void>> => {
				const argsArray = ['sql', 'mi-arc', 'delete', '-n', name];
				if (args.resourceGroup) {
					argsArray.push('--resource-group', args.resourceGroup);
				}
				if (args.namespace) {
					argsArray.push('--k8s-namespace', args.namespace);
					argsArray.push('--use-k8s');
				}
				return this.executeCommand<void>(argsArray, additionalEnvVars);
			},
			list: (
				args: {
					// ARM API arguments
					resourceGroup?: string,
					// K8s API arguments
					namespace?: string
					// Additional arguments
				},
				additionalEnvVars?: azExt.AdditionalEnvVars
			): Promise<azExt.AzOutput<azExt.SqlMiListRawOutput>> => {
				const argsArray = ['sql', 'mi-arc', 'list'];
				if (args.resourceGroup) {
					argsArray.push('--resource-group', args.resourceGroup);
				}
				if (args.namespace) {
					argsArray.push('--k8s-namespace', args.namespace);
					argsArray.push('--use-k8s');
				}
				return this.executeCommand<azExt.SqlMiListRawOutput>(argsArray, additionalEnvVars);

			},
			show: (
				name: string,
				args: {
					// ARM API arguments
					resourceGroup?: string,
					// K8s API arguments
					namespace?: string
					// Additional arguments
				},
				additionalEnvVars?: azExt.AdditionalEnvVars
			): Promise<azExt.AzOutput<azExt.SqlMiShowResult>> => {
				const argsArray = ['sql', 'mi-arc', 'show', '-n', name];
				if (args.resourceGroup) {
					argsArray.push('--resource-group', args.resourceGroup);
				}
				if (args.namespace) {
					argsArray.push('--k8s-namespace', args.namespace);
					argsArray.push('--use-k8s');
				}
				return this.executeSqlMiShow(argsArray, additionalEnvVars);
			},
			update: (
				name: string,
				args: {
					coresLimit?: string,
					coresRequest?: string,
					memoryLimit?: string,
					memoryRequest?: string,
					noWait?: boolean,
					retentionDays?: string,
					syncSecondaryToCommit?: string
				},
				// ARM API arguments
				resourceGroup?: string,
				// K8s API arguments
				namespace?: string,
				usek8s?: boolean,
				// Additional arguments
				additionalEnvVars?: azExt.AdditionalEnvVars
			): Promise<azExt.AzOutput<void>> => {
				const argsArray = ['sql', 'mi-arc', 'update', '-n', name];
				if (args.coresLimit) { argsArray.push('--cores-limit', args.coresLimit); }
				if (args.coresRequest) { argsArray.push('--cores-request', args.coresRequest); }
				if (args.memoryLimit) { argsArray.push('--memory-limit', args.memoryLimit); }
				if (args.memoryRequest) { argsArray.push('--memory-request', args.memoryRequest); }
				if (args.noWait) { argsArray.push('--no-wait'); }
				if (args.retentionDays) { argsArray.push('--retention-days', args.retentionDays); }
				if (args.syncSecondaryToCommit) { argsArray.push('--sync-secondary-to-commit', args.syncSecondaryToCommit); }
				if (resourceGroup) { argsArray.push('--resource-group', resourceGroup); }
				if (namespace) { argsArray.push('--k8s-namespace', namespace); }
				if (usek8s) { argsArray.push('--use-k8s'); }
				return this.executeCommand<void>(argsArray, additionalEnvVars);
			},
			upgrade: (
				name: string,
				args: {
					// ARM API arguments
					resourceGroup?: string,
					// K8s API arguments
					namespace?: string
					// Additional arguments
				},
				additionalEnvVars?: azExt.AdditionalEnvVars
			): Promise<azExt.AzOutput<void>> => {
				const argsArray = ['sql', 'mi-arc', 'upgrade', '--name', name];
				if (args.resourceGroup) { argsArray.push('--resource-group', args.resourceGroup); }
				if (args.namespace) {
					argsArray.push('--k8s-namespace', args.namespace);
					argsArray.push('--use-k8s');
				}
				return this.executeCommand<void>(argsArray, additionalEnvVars);
			}
		},
		midbarc: {
			restore: (
				name: string,
				args: {
					destName?: string,
					managedInstance?: string,
					time?: string,
					noWait?: boolean,
					dryRun?: boolean
				},
				namespace: string,
				additionalEnvVars?: azExt.AdditionalEnvVars
			): Promise<azExt.AzOutput<azExt.SqlMiDbRestoreResult>> => {
				const argsArray = ['sql', 'midb-arc', 'restore', '--name', name, '--k8s-namespace', namespace, '--use-k8s'];
				if (args.destName) { argsArray.push('--dest-name', args.destName); }
				if (args.managedInstance) { argsArray.push('--managed-instance', args.managedInstance); }
				if (args.time) { argsArray.push('--time', args.time); }
				if (args.noWait) { argsArray.push('--no-wait'); }
				if (args.dryRun) { argsArray.push('--dry-run'); }
				return this.executeCommand<azExt.SqlMiDbRestoreResult>(argsArray, additionalEnvVars);
			}
		}
	};

	public monitor = {
		logAnalytics: {
			workspace: {
				list: (resourceGroup?: string, subscription?: string, additionalEnvVars?: azExt.AdditionalEnvVars): Promise<azExt.AzOutput<azExt.LogAnalyticsWorkspaceListResult[]>> => {
					const argsArray = ['monitor', 'log-analytics', 'workspace', 'list'];
					if (resourceGroup) { argsArray.push('--resource-group', resourceGroup); }
					if (subscription) { argsArray.push('--subscription', subscription); }
					return this.executeCommand<azExt.LogAnalyticsWorkspaceListResult[]>(argsArray, additionalEnvVars);
				}
			}
		}
	};


	/**
	 * Gets the output of running '--version' command on the az tool.
	 * It also updates the cachedVersion property based on the return value from the tool.
	 */
	public async version(): Promise<azExt.AzOutput<string>> {
		const output = await executeAzCommand(`"${this._path}"`, ['--version']);
		this._semVersionAz = new SemVer(<string>parseVersion(output.stdout));
		return {
			stdout: output.stdout,
			stderr: output.stderr.split(os.EOL)
		};
	}

	/**
	 * Executes az sql mi-arc show and returns a normalized object, SqlMiShowResult, regardless of the indirect or direct mode raw output shape.
	 * @param args The args to pass to az
	 * @param additionalEnvVars Additional environment variables to set for this execution
	 */
	public async executeSqlMiShow(args: string[], additionalEnvVars?: azExt.AdditionalEnvVars): Promise<azExt.AzOutput<azExt.SqlMiShowResult>> {
		try {
			const result = await executeAzCommand(`"${this._path}"`, args.concat(['--output', 'json']), additionalEnvVars);

			let stdout = <unknown>result.stdout;
			let stderr = <unknown>result.stderr;

			try {
				// Automatically try parsing the JSON. This is expected to fail for some az commands such as resource delete.
				stdout = JSON.parse(result.stdout);
			} catch (err) {
				// If the output was not pure JSON, catch the error and log it here.
				Logger.log(loc.azOutputParseErrorCaught(args.concat(['--output', 'json']).toString()));
				throw err;
			}

			if ((<azExt.SqlMiShowResultDirect>stdout).properties) {
				// Then it is direct mode
				return {
					stdout: {
						name: (<azExt.SqlMiShowResultDirect>stdout).name,
						spec: (<azExt.SqlMiShowResultDirect>stdout).properties.k8SRaw.spec,
						status: (<azExt.SqlMiShowResultDirect>stdout).properties.k8SRaw.status
					},
					stderr: <string[]>stderr
				};
			} else {
				// It must be indirect mode
				return {
					stdout: {
						name: (<azExt.SqlMiShowResultIndirect>stdout).metadata?.name,
						spec: (<azExt.SqlMiShowResultIndirect>stdout).spec,
						status: (<azExt.SqlMiShowResultIndirect>stdout).status
					},
					stderr: <string[]>stderr
				};
			}
		} catch (err) {
			if (err instanceof ExitCodeError) {
				try {
					await fs.promises.access(this._path);
					//this.path exists
				} catch (e) {
					// this.path does not exist
					await vscode.commands.executeCommand('setContext', azFound, false);
					throw new NoAzureCLIError();
				}
			}
			throw err;
		}
	}


	/**
	 * Executes the specified az command.
	 * @param args The args to pass to az
	 * @param additionalEnvVars Additional environment variables to set for this execution
	 */
	public async executeCommand<R>(args: string[], additionalEnvVars?: azExt.AdditionalEnvVars): Promise<azExt.AzOutput<R>> {
		try {
			const result = await executeAzCommand(`"${this._path}"`, args.concat(['--output', 'json']), additionalEnvVars);

			let stdout = <unknown>result.stdout;
			let stderr = <unknown>result.stderr;

			try {
				// Automatically try parsing the JSON. This is expected to fail for some az commands such as resource delete.
				stdout = JSON.parse(result.stdout);
			} catch (err) {
				// If the output was not pure JSON, catch the error and log it here.
				Logger.log(loc.azOutputParseErrorCaught(args.concat(['--output', 'json']).toString()));
			}

			return {
				stdout: <R>stdout,
				stderr: <string[]>stderr
			};
		} catch (err) {
			if (err instanceof ExitCodeError) {
				try {
					await fs.promises.access(this._path);
					//this.path exists
				} catch (e) {
					// this.path does not exist
					await vscode.commands.executeCommand('setContext', azFound, false);
					throw new NoAzureCLIError();
				}
			}
			throw err;
		}
	}
}

/**
 * Checks whether az is installed - and if it is not then invokes the process of az installation.
 * @param userRequested true means that this operation by was requested by a user by executing an ads command.
 */
export async function checkAndInstallAz(userRequested: boolean = false): Promise<IAzTool | undefined> {
	try {
		return await findAzAndArc(); // find currently installed Az
	} catch (err) {
		if (err instanceof NoAzureCLIArcExtError) {
			// Az found but arcdata extension not found. Prompt user to install it, then check again.
			if (await promptToInstallArcdata(userRequested)) {
				return await findAzAndArc();
			}
		} else {
			// No az was found. Prompt user to install it, then check again.
			if (await promptToInstallAz(userRequested)) {
				return await findAzAndArc();
			}
		}
	}
	// If user declines to install upon prompt, return an undefined object instead of an AzTool
	return undefined;
}

/**
 * Finds the existing installation of az, or throws an error if it couldn't find it
 * or encountered an unexpected error. If arcdata extension was not found on the az,
 * throw an error. An AzTool will not be returned.
 * The promise is rejected when Az is not found.
 */
export async function findAzAndArc(): Promise<IAzTool> {
	Logger.log(loc.searchingForAz);
	try {
		const azTool = await findSpecificAzAndArc();
		await vscode.commands.executeCommand('setContext', azFound, true); // save a context key that az was found so that command for installing az is no longer available in commandPalette and that for updating it is.
		await vscode.commands.executeCommand('setContext', azArcFound, true); // save a context key that arcdata was found so that command for installing arcdata is no longer available in commandPalette and that for updating it is.
		Logger.log(loc.foundExistingAz(await azTool.getPath(), (await azTool.getSemVersionAz()).raw, (await azTool.getSemVersionArc()).raw));
		return azTool;
	} catch (err) {
		if (err === NoAzureCLIArcExtError) {
			Logger.log(loc.couldNotFindAzArc(err));
			Logger.log(loc.noAzArc);
			await vscode.commands.executeCommand('setContext', azArcFound, false); // save a context key that az was not found so that command for installing az is available in commandPalette and that for updating it is no longer available.
		} else {
			Logger.log(loc.couldNotFindAz(err));
			Logger.log(loc.noAz);
			await vscode.commands.executeCommand('setContext', azFound, false); // save a context key that arcdata was not found so that command for installing arcdata is available in commandPalette and that for updating it is no longer available.
		}
		throw err;
	}
}

/**
 * Find az by searching user's directories. If no az is found, this will error out and no arcdata is found.
 * If az is found, check if arcdata extension exists on it and return true if so, false if not.
 * Attempt to update arcdata extension.
 * Return the AzTool whether or not an arcdata extension has been found.
 */
async function findSpecificAzAndArc(): Promise<IAzTool> {
	// Check if az exists
	const path = await ((process.platform === 'win32') ? searchForCmd('az.cmd') : searchForCmd('az'));
	const versionOutput = await executeAzCommand(`"${path}"`, ['--version']);

	// The arcdata extension can't exist if there is no az. The function will not reach the following code
	// if no az has been found. If found, check if az arcdata extension exists.
	const arcVersion = parseArcExtensionVersion(versionOutput.stdout);
	if (arcVersion === undefined) {
		throw new NoAzureCLIArcExtError;
	}

	// Quietly attempt to update the arcdata extension to the latest. If it is already the latest, then it will not update.
	await executeCommand('az', ['extension', 'update', '--name', 'arcdata']);

	return new AzTool(path, <string>parseVersion(versionOutput.stdout), <string>arcVersion);
}

/**
 * Prompt user to install Azure CLI.
 * @param userRequested - if true this operation was requested in response to a user issued command, if false it was issued at startup by system
 * returns true if installation was done and false otherwise.
 */
async function promptToInstallAz(userRequested: boolean = false): Promise<boolean> {
	let response: string | undefined = loc.yes;
	const config = <AzDeployOption>getAzConfig(azCliInstallKey);
	if (userRequested) {
		Logger.show();
		Logger.log(loc.userRequestedInstall);
	}
	if (config === AzDeployOption.dontPrompt && !userRequested) {
		Logger.log(loc.skipInstall(config));
		return false;
	}
	const responses = userRequested
		? [loc.yes, loc.no]
		: [loc.yes, loc.askLater, loc.doNotAskAgain];
	if (config === AzDeployOption.prompt) {
		Logger.log(loc.promptForAzInstallLog);
		response = await vscode.window.showErrorMessage(loc.promptForAzInstall, ...responses);
		Logger.log(loc.userResponseToInstallPrompt(response));
	}
	if (response === loc.doNotAskAgain) {
		await setAzConfig(azCliInstallKey, AzDeployOption.dontPrompt);
	} else if (response === loc.yes) {
		try {
			await installAz();
			vscode.window.showInformationMessage(loc.azInstalled);
			Logger.log(loc.azInstalled);
			return true;
		} catch (err) {
			// Windows: 1602 is User cancelling installation/update - not unexpected so don't display
			if (!(err instanceof ExitCodeError) || err.code !== 1602) {
				vscode.window.showWarningMessage(loc.installError(err));
				Logger.log(loc.installError(err));
			}
		}
	}
	return false;
}

/**
 * Prompt user to install Azure CLI arcdata extension.
 * @param userRequested - if true this operation was requested in response to a user issued command, if false it was issued at startup by system
 * returns true if installation was done and false otherwise.
 */
async function promptToInstallArcdata(userRequested: boolean = false): Promise<boolean> {
	let response: string | undefined = loc.yes;
	const config = <AzDeployOption>getAzConfig(azArcdataInstallKey);
	if (userRequested) {
		Logger.show();
		Logger.log(loc.userRequestedInstall);
	}
	if (config === AzDeployOption.dontPrompt && !userRequested) {
		Logger.log(loc.skipInstall(config));
		return false;
	}
	const responses = userRequested
		? [loc.yes, loc.no]
		: [loc.yes, loc.askLater, loc.doNotAskAgain];
	if (config === AzDeployOption.prompt) {
		Logger.log(loc.promptForArcdataInstallLog);
		response = await vscode.window.showErrorMessage(loc.promptForArcdataInstall, ...responses);
		Logger.log(loc.userResponseToInstallPrompt(response));
	}
	if (response === loc.doNotAskAgain) {
		await setAzConfig(azArcdataInstallKey, AzDeployOption.dontPrompt);
	} else if (response === loc.yes) {
		try {
			await installArcdata();
			vscode.window.showInformationMessage(loc.arcdataInstalled);
			Logger.log(loc.arcdataInstalled);
			return true;
		} catch (err) {
			// Windows: 1602 is User cancelling installation/update - not unexpected so don't display
			if (!(err instanceof ExitCodeError) || err.code !== 1602) {
				vscode.window.showWarningMessage(loc.installError(err));
				Logger.log(loc.installError(err));
			}
		}
	}
	return false;
}

/**
 * runs the commands to install az, downloading the installation package if needed
 */
export async function installAz(): Promise<void> {
	Logger.show();
	Logger.log(loc.installingAz);
	await vscode.window.withProgress(
		{
			location: vscode.ProgressLocation.Notification,
			title: loc.installingAz,
			cancellable: false
		},
		async (_progress, _token): Promise<void> => {
			switch (process.platform) {
				case 'win32':
					await downloadAndInstallAzWin32();
					break;
				case 'darwin':
					await installAzDarwin();
					break;
				case 'linux':
					await installAzLinux();
					break;
				default:
					throw new Error(loc.platformUnsupported(process.platform));
			}
		}
	);
}

/**
 * Downloads the Windows installer and runs it
 */
async function downloadAndInstallAzWin32(): Promise<void> {
	const downLoadLink = `${azHostname}/${azUri}`;
	const downloadFolder = os.tmpdir();
	const downloadLogs = path.join(downloadFolder, 'ads_az_install_logs.log');
	const downloadedFile = await HttpClient.downloadFile(downLoadLink, downloadFolder);

	try {
		await executeSudoCommand(`msiexec /qn /i "${downloadedFile}" /lvx "${downloadLogs}"`);
	} catch (err) {
		throw new Error(`${err.message}. See logs at ${downloadLogs} for more details.`);
	}
}

/**
 * Runs commands to install az on MacOS
 */
async function installAzDarwin(): Promise<void> {
	await executeCommand('brew', ['update']);
	await executeCommand('brew', ['install', 'azure-cli']);
}

/**
 * Runs commands to install az on Linux
 */
async function installAzLinux(): Promise<void> {
	// Get packages needed for install process
	await executeSudoCommand('apt-get update');
	await executeSudoCommand('apt-get install ca-certificates curl apt-transport-https lsb-release gnupg');
	// Download and install the signing key
	await executeSudoCommand('curl -sL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/microsoft.gpg > /dev/null');
	// Add the az repository information
	await executeSudoCommand('AZ_REPO=$(lsb_release -cs) echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ $AZ_REPO main" | sudo tee /etc/apt/sources.list.d/azure-cli.list');
	// Update repository information and install az
	await executeSudoCommand('apt-get update');
	await executeSudoCommand('apt-get install azure-cli');
}

/**
 * Runs the command to install az arcdata extension
 */
export async function installArcdata(): Promise<void> {
	Logger.show();
	Logger.log(loc.installingArcdata);
	await vscode.window.withProgress(
		{
			location: vscode.ProgressLocation.Notification,
			title: loc.installingArcdata,
			cancellable: false
		},
		async (_progress, _token): Promise<void> => {
			await executeCommand('az', ['extension', 'add', '--name', 'arcdata']);
		}
	);
}

/**
 * Parses out the Azure CLI version from the raw az version output
 * @param raw The raw version output from az --version
 */
function parseVersion(raw: string): string | undefined {
	// Currently the version is a multi-line string that contains other version information such
	// as the Python installation, with the first line holding the version of az itself.
	//
	// The output of az --version looks like:
	// azure-cli                         2.26.1
	// ...
	const exp = /azure-cli\s*(\d*.\d*.\d*)/;
	return exp.exec(raw)?.pop();
}

/**
 * Parses out the arcdata extension version from the raw az version output
 * @param raw The raw version output from az --version
 */
function parseArcExtensionVersion(raw: string): string | undefined {
	// Currently the version is a multi-line string that contains other version information such
	// as the Python installation and any extensions.
	//
	// The output of az --version looks like:
	// azure-cli                         2.26.1
	// ...
	// Extensions:
	// arcdata                            1.0.0
	// connectedk8s                       1.1.5
	// ...
	const exp = /arcdata\s*(\d*.\d*.\d*)/;
	return exp.exec(raw)?.pop();
}

/**
 * Parses out all available upgrades
 * @param raw The raw version output from az arcdata dc list-upgrades
 */
function parseDcListUpgrades(raw: string): string[] | undefined {
	// Currently the version is a multi-line string that contains other version information such
	// as the Python installation, with the first line holding the version of az itself.
	//
	// Found 6 valid versions.  The current datacontroller version is v1.2.0_2021-12-15.
	// v1.4.1_2022-03-08
	// v1.4.0_2022-02-25
	// v1.3.0_2022-01-27
	// v1.2.0_2021-12-15 << current version
	// v1.1.0_2021-11-02
	// v1.0.0_2021-07-30
	let versions: string[] = [];
	const lines = raw.split('\n');
	const exp = /^(v\d*.\d*.\d*.\d*.\d*.\d*.\d)/;
	for (let i = 1; i < lines.length; i++) {
		let result = exp.exec(lines[i])?.pop();
		if (result) {
			versions.push(result);
		}
	}
	return versions;
}

/**
 * Parses out the release date from the upgrade version number and formats it into MM/DD/YYYY format.
 * For example: v1.4.1_2022-03-08 ==> 03/08/2022
 * @param raw The raw upgrade version number, such as: v1.4.1_2022-03-08
 */
function parseReleaseDateFromUpgrade(raw: string): string {
	let formattedDate = '';
	const exp = /^v\d*.\d*.\d*_(\d*).(\d*).(\d*.\d)/;
	let rawDate = exp.exec(raw);
	if (rawDate) {
		formattedDate += rawDate[2] + '/' + rawDate[3] + '/' + rawDate[1];
	} else {
		console.error(loc.releaseDateNotParsed);
	}
	return formattedDate;
}

/**
 * Parses out the current version number out of all available upgrades
 * @param raw The raw version output from az arcdata dc list-upgrades
 */
function parseCurrentVersion(raw: string): string | undefined {
	// Currently the version is a multi-line string that contains other version information such
	// as the Python installation, with the first line holding the version of az itself.
	//
	// Found 6 valid versions.  The current datacontroller version is v1.2.0_2021-12-15.
	// v1.4.1_2022-03-08
	// v1.4.0_2022-02-25
	// v1.3.0_2022-01-27
	// v1.2.0_2021-12-15 << current version
	// v1.1.0_2021-11-02
	// v1.0.0_2021-07-30

	const exp = /The current datacontroller version is\s*(v\d*.\d*.\d*.\d*.\d*.\d*.\d)/;
	return exp.exec(raw)?.pop();
}

async function executeAzCommand(command: string, args: string[], additionalEnvVars: azExt.AdditionalEnvVars = {}): Promise<ProcessOutput> {
	const debug = vscode.workspace.getConfiguration(azConfigSection).get(debugConfigKey);
	if (debug) {
		args.push('--debug');
	}
	return executeCommand(command, args, additionalEnvVars);
}

function getAzConfig(key: string): AzDeployOption | undefined {
	const config = vscode.workspace.getConfiguration(azConfigSection);
	const value = <AzDeployOption>config.get<AzDeployOption>(key);
	return value;
}

async function setAzConfig(key: string, value: string): Promise<void> {
	const config = vscode.workspace.getConfiguration(azConfigSection);
	await config.update(key, value, vscode.ConfigurationTarget.Global);
}
