// Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may not
// use this file except in compliance with the License. A copy of the
// License is located at
//
// http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
// either express or implied. See the License for the specific language governing
// permissions and limitations under the License.

// Package application implements the application plugin.
//
//go:build windows
// +build windows

package application

import (
	"fmt"

	"github.com/aws/amazon-ssm-agent/agent/appconfig"
	"github.com/aws/amazon-ssm-agent/agent/contracts"
	"github.com/aws/amazon-ssm-agent/agent/framework/processor/executer/iohandler"
	"github.com/aws/amazon-ssm-agent/agent/log"
	"github.com/aws/amazon-ssm-agent/agent/task"
)

const (
	INSTALL = "Install"

	UNINSTALL = "Uninstall"

	REPAIR = "Repair"
)

const (
	ErrorUnknownProduct = 1605

	ErrorSuccessRebootInitiated = 1641
)

// getMsiApplicationMode returns the msi exec mode based on plugin input
func getMsiApplicationMode(log log.T, pluginInput ApplicationPluginInput) (string, error) {
	switch pluginInput.Action {
	case INSTALL:
		return "/i", nil
	case UNINSTALL:
		return "/x", nil
	case REPAIR:
		return "/fa", nil
	default:
		return "", fmt.Errorf("MsiAction is set to unsupported value: %v", pluginInput.Action)
	}
}

// setMsiExecStatus sets the exit status and output to be returned to the user based on exit code
func setMsiExecStatus(log log.T, pluginInput ApplicationPluginInput, cancelFlag task.CancelFlag, out iohandler.IOHandler) {
	out.AppendInfo(pluginInput.Source)
	out.SetStatus(contracts.ResultStatusFailed)
	isUnKnownError := false

	switch out.GetExitCode() {
	case appconfig.SuccessExitCode:
		out.SetStatus(contracts.ResultStatusSuccess)
	case ErrorUnknownProduct:
		if pluginInput.Action == UNINSTALL {
			// Uninstall will skip, if product is not currently installed.
			// This is needed to support idempotent behavior.
			out.SetStatus(contracts.ResultStatusSuccess)
		}
	case ErrorSuccessRebootInitiated:
		fallthrough
	case appconfig.RebootExitCode:
		out.SetStatus(contracts.ResultStatusSuccessAndReboot)
	case appconfig.CommandStoppedPreemptivelyExitCode:
		if cancelFlag.ShutDown() {
			out.SetStatus(contracts.ResultStatusFailed)
		}
		if cancelFlag.Canceled() {
			out.SetStatus(contracts.ResultStatusCancelled)
		}
		out.SetStatus(contracts.ResultStatusTimedOut)
	default:
		isUnKnownError = true
	}

	if isUnKnownError {
		// Note: Sample Stderr:
		// Action:{Installed}; Status:{Failed};
		// ErrorCode:{1620}; ErrorMsg:{ERROR_INSTALL_PACKAGE_INVALID};
		// Description:{This installation package could not be opened. Contact the application vendor to verify that this is a valid Windows Installer package.};
		// Source:{https:///}

		// Construct stderr in above format using StandardMsiErrorCodes
		out.AppendErrorf("Action:{%v}; Status:{%v}; ErrorCode:{%v}; %v Source:{%v};", pluginInput.Action, out.GetStatus(), out.GetExitCode(), getExitCodeDescription(out.GetExitCode()), pluginInput.Source)
	}

	// Logging msiexec.Result
	log.Debug("logging stdouts & errors after setting final status for msiexec")
	log.Debugf("resultCode: %v", out.GetExitCode())
	log.Debugf("resultCode: %v", out.GetExitCode())
	log.Debugf("stdout: %v", out.GetStdout())
	log.Debugf("stderr: %v", out.GetStderr())
}

// processParams smartly divides the input parameter string into valid string blocks
func processParams(log log.T, str string) []string {

	// Sample transformation:
	// str = "/v value "some path" myproperty=value"
	// result: []string{"/v", "value", "some path", "myproperty=value"}

	// contains the last split location of the string
	lastbit := 0

	params := []string{}

	// true if first quote was encountered else false
	quoteInit := false

	// Iterate through each character in str
	for i, c := range str {

		// Look for quotes or spaces
		// By default we split a string using space as a delimiter
		// If a quote(") is encountered then wait for the next quote irrespective of any spaces in between
		if c == '"' {
			if quoteInit {
				quoteInit = false
				params = append(params, str[lastbit:i+1])
				lastbit = i + 1
			} else {
				quoteInit = true
				lastbit = i
			}
		} else if c == ' ' && !quoteInit {
			if lastbit != i {
				params = append(params, str[lastbit:i])
			}
			lastbit = i + 1
		}
	}

	// This handles the last word in str
	if lastbit < len(str) {
		params = append(params, str[lastbit:])
	}

	log.Debug("Parameters after processing...")
	for _, param := range params {
		log.Debug(param)
	}

	return params
}

// Using https://msdn.microsoft.com/en-us/library/aa376931(v=vs.85).aspx as a reference, this returns
// description of error codes regarding msi-exec.
// getExitCodeDescription returns the description for errorCode
func getExitCodeDescription(errorCode int) string {

	// NOTE: The error description is in the format of ErrorMsg:{message}; Description:{description};
	switch errorCode {
	case 0:
		return "ErrorMsg:{ERROR_SUCCESS}; Description:{The action completed successfully.};"
	case 13:
		return "ErrorMsg:{ERROR_INVALID_DATA}; Description:{The data is invalid.};"
	case 87:
		return "ErrorMsg:{ERROR_INVALID_PARAMETER}; Description:{One of the parameters was invalid.};"
	case 120:
		return "ErrorMsg:{ERROR_CALL_NOT_IMPLEMENTED}; Description:{This value is returned when a custom action attempts to call a function that cannot be called from custom actions. The function returns the value ERROR_CALL_NOT_IMPLEMENTED. Available beginning with Windows Installer version 3.0.};"
	case 1259:
		// this might not be used afterall since we do /quiet install
		return "ErrorMsg:{ERROR_APPHELP_BLOCK}; Description:{If Windows Installer determines a product may be incompatible with the current operating system, it displays a dialog box informing the user and asking whether to try to install anyway. This error code is returned if the user chooses not to try the installation.};"
	case 1601:
		return "ErrorMsg:{ERROR_INSTALL_SERVICE_FAILURE}; Description:{The Windows Installer service could not be accessed. Contact your support personnel to verify that the Windows Installer service is properly registered.};"
	case 1602:
		return "ErrorMsg:{ERROR_INSTALL_USEREXIT}; Description:{The user cancels installation.};"
	case 1603:
		return "ErrorMsg:{ERROR_INSTALL_FAILURE}; Description:{A fatal error occurred during installation.};"
	case 1604:
		return "ErrorMsg:{ERROR_INSTALL_SUSPEND}; Description:{Installation suspended, incomplete.};"
	case 1605:
		return "ErrorMsg:{ERROR_UNKNOWN_PRODUCT}; Description:{This action is only valid for products that are currently installed.};"
	case 1606:
		return "ErrorMsg:{ERROR_UNKNOWN_FEATURE}; Description:{The feature identifier is not registered.};"
	case 1607:
		return "ErrorMsg:{ERROR_UNKNOWN_COMPONENT}; Description:{The component identifier is not registered.};"
	case 1608:
		return "ErrorMsg:{ERROR_INSTALL_SUSPEND}; Description:{Installation suspended, incomplete.};"
	case 1609:
		return "ErrorMsg:{ERROR_INVALID_HANDLE_STATE}; Description: The handle is in an invalid state.};"
	case 1610:
		return "ErrorMsg:{ERROR_BAD_CONFIGURATION}; Description:{The configuration data for this product is corrupt. Contact your support personnel.};"
	case 1611:
		return "ErrorMsg:{ERROR_INDEX_ABSENT}; Descriptio{: The component qualifier not present.};"
	case 1612:
		return "ErrorMsg:{ERROR_INSTALL_SOURCE_ABSENT}; Description:{The installation source for this product is not available. Verify that the source exists and that you can access it.};"
	case 1613:
		return "ErrorMsg:{ERROR_INSTALL_PACKAGE_VERSION}; Description:{This installation package cannot be installed by the Windows Installer service. You must install a Windows service pack that contains a newer version of the Windows Installer service.};"
	case 1614:
		return "ErrorMsg:{ERROR_PRODUCT_UNINSTALLED}; Description:{The product is uninstalled.};"
	case 1615:
		return "ErrorMsg:{ERROR_BAD_QUERY_SYNTAX}; Description:{The SQL query syntax is invalid or unsupported.};"
	case 1616:
		return "ErrorMsg:{ERROR_INVALID_FIELD}; Description:{The record field does not exist.};"
	case 1618:
		return "ErrorMsg:{ERROR_INSTALL_ALREADY_RUNNING}; Description:{Another installation is already in progress. Complete that installation before proceeding with this install.};"
	case 1619:
		return "ErrorMsg:{ERROR_INSTALL_PACKAGE_OPEN_FAILED}; Description:{This installation package could not be opened. Verify that the package exists and is accessible, or contact the application vendor to verify that this is a valid Windows Installer package.};"
	case 1620:
		return "ErrorMsg:{ERROR_INSTALL_PACKAGE_INVALID}; Description:{This installation package could not be opened. Contact the application vendor to verify that this is a valid Windows Installer package.};"
	case 1621:
		// this should not be ever used - because we do /quiet installation of msi
		return "ErrorMsg:{ERROR_INSTALL_UI_FAILURE}; Description:{There was an error starting the Windows Installer service user interface. Contact your support personnel.};"
	case 1622:
		return "ErrorMsg:{ERROR_INSTALL_LOG_FAILURE}; Description:{There was an error opening installation log file. Verify that the specified log file location exists and is writable.};"
	case 1623:
		return "ErrorMsg:{ERROR_INSTALL_LANGUAGE_UNSUPPORTED}; Description:{This language of this installation package is not supported by your system.};"
	case 1624:
		return "ErrorMsg:{ERROR_INSTALL_TRANSFORM_FAILURE}; Description:{There was an error applying transforms. Verify that the specified transform paths are valid.};"
	case 1625:
		return "ErrorMsg:{ERROR_INSTALL_PACKAGE_REJECTED}; Description:{This installation is forbidden by system policy. Contact your system administrator.};"
	case 1626:
		return "ErrorMsg:{ERROR_FUNCTION_NOT_CALLED}; Description:{The function could not be executed.};"
	case 1627:
		return "ErrorMsg:{ERROR_FUNCTION_FAILED}; Description:{The function failed during execution.};"
	case 1628:
		return "ErrorMsg:{ERROR_INVALID_TABLE}; Description:{The function failed during execution.};"
	case 1629:
		return "ErrorMsg:{ERROR_DATATYPE_MISMATCH}; Description: The data supplied is the wrong type.};"
	case 1630:
		return "ErrorMsg:{ERROR_UNSUPPORTED_TYPE}; Description:{Data of this type is not supported.};"
	case 1631:
		return "ErrorMsg:{ERROR_CREATE_FAILED}; Description:{The Windows Installer service failed to start. Contact your support personnel.};"
	case 1632:
		return "ErrorMsg:{ERROR_INSTALL_TEMP_UNWRITABLE}; Description:{The Temp folder is either full or inaccessible. Verify that the Temp folder exists and that you can write to it.};"
	case 1633:
		return "ErrorMsg:{ERROR_INSTALL_PLATFORM_UNSUPPORTED}; Description:{This installation package is not supported on this platform. Contact your application vendor.};"
	case 1634:
		return "ErrorMsg:{ERROR_INSTALL_NOTUSED}; Description:{Component is not used on this machine.};"
	case 1635:
		return "ErrorMsg:{ERROR_PATCH_PACKAGE_OPEN_FAILED}; Description:{This patch package could not be opened. Verify that the patch package exists and is accessible, or contact the application vendor to verify that this is a valid Windows Installer patch package.};"
	case 1636:
		return "ErrorMsg:{ERROR_PATCH_PACKAGE_INVALID}; Description:{This patch package could not be opened. Contact the application vendor to verify that this is a valid Windows Installer patch package.};"
	case 1637:
		return "ErrorMsg:{ERROR_PATCH_PACKAGE_UNSUPPORTED}; Description:{This patch package cannot be processed by the Windows Installer service. You must install a Windows service pack that contains a newer version of the Windows Installer service.};"
	case 1638:
		return "ErrorMsg:{ERROR_PRODUCT_VERSION}; Description:{Another version of this product is already installed. Installation of this version cannot continue. To configure or remove the existing version of this product, use Add/Remove Programs in Control Panel.};"
	case 1639:
		return "ErrorMsg:{ERROR_INVALID_COMMAND_LINE}; Description:{Invalid command line argument. Consult the Windows Installer SDK for detailed command-line help.};"
	case 1640:
		return "ErrorMsg:{ERROR_INSTALL_REMOTE_DISALLOWED}; Description:{The current user is not permitted to perform installations from a client session of a server running the Terminal Server role service.};"
	case 1641:
		return "ErrorMsg:{ERROR_SUCCESS_REBOOT_INITIATED}; Description:{The installer has initiated a restart. This message is indicative of a success.};"
	case 1642:
		return "ErrorMsg:{ERROR_PATCH_TARGET_NOT_FOUND}; Description:{The installer cannot install the upgrade patch because the program being upgraded may be missing or the upgrade patch updates a different version of the program. Verify that the program to be upgraded exists on your computer and that you have the correct upgrade patch.};"
	case 1643:
		return "ErrorMsg:{ERROR_PATCH_PACKAGE_REJECTED}; Description:{The patch package is not permitted by system policy.};"
	case 1644:
		return "ErrorMsg:{ERROR_INSTALL_TRANSFORM_REJECTED}; Description:{One or more customizations are not permitted by system policy.};"
	case 1645:
		return "ErrorMsg:{ERROR_INSTALL_REMOTE_PROHIBITED}; Description:{Windows Installer does not permit installation from a Remote Desktop Connection.};"
	case 1646:
		return "ErrorMsg:{ERROR_PATCH_REMOVAL_UNSUPPORTED}; Description:{The patch package is not a removable patch package. Available beginning with Windows Installer version 3.0.};"
	case 1647:
		return "ErrorMsg:{ERROR_UNKNOWN_PATCH}; Description:{The patch is not applied to this product. Available beginning with Windows Installer version 3.0.};"
	case 1648:
		return "ErrorMsg:{ERROR_PATCH_NO_SEQUENCE}; Description:{No valid sequence could be found for the set of patches. Available beginning with Windows Installer version 3.0.};"
	case 1649:
		return "ErrorMsg:{ERROR_PATCH_REMOVAL_DISALLOWED}; Description:{Patch removal was disallowed by policy. Available beginning with Windows Installer version 3.0.};"
	case 1650:
		return "ErrorMsg:{ERROR_INVALID_PATCH_XML}; Description:{The XML patch data is invalid. Available beginning with Windows Installer version 3.0.};"
	case 1651:
		return "ErrorMsg:{ERROR_PATCH_MANAGED_ADVERTISED_PRODUCT}; Description:{Administrative user failed to apply patch for a per-user managed or a per-machine application that is in advertise state. Available beginning with Windows Installer version 3.0.};"
	case 1652:
		return "ErrorMss:{ERROR_INSTALL_SERVICE_SAFEBOOT}; Description:{Windows Installer is not accessible when the computer is in Safe Mode. Exit Safe Mode and try again or try using System Restore to return your computer to a previous state. Available beginning with Windows Installer version 4.0.};"
	case 1653:
		return "ErrorMsg:{ERROR_ROLLBACK_DISABLED}; Description:{Could not perform a multiple-package transaction because rollback has been disabled. Multiple-Package Installations cannot run if rollback is disabled. Available beginning with Windows Installer version 4.5.};"
	case 1654:
		return "ErrorMsg:{ERROR_INSTALL_REJECTED}; Description:{The app that you are trying to run is not supported on this version of Windows. A Windows Installer package, patch, or transform that has not been signed by Microsoft cannot be installed on an ARM computer.};"
	case 3010:
		return "ErrorMsg:{ERROR_SUCCESS_REBOOT_REQUIRED}; Description:{A restart is required to complete the install. This message is indicative of a success. This does not include installs where the ForceReboot action is run.};"
	default:
		return fmt.Sprintf("ErrorCode: %v; Description: Unknown", errorCode)
	}
}
