// 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 updateutil contains updater specific utilities.
package updateutil

import (
	"bufio"
	"bytes"
	"errors"
	"fmt"
	"io"
	"net/url"
	"os"
	"os/exec"
	"path/filepath"
	"runtime/debug"
	"strconv"
	"strings"
	"syscall"
	"time"

	"github.com/aws/amazon-ssm-agent/agent/appconfig"
	"github.com/aws/amazon-ssm-agent/agent/context"
	"github.com/aws/amazon-ssm-agent/agent/contracts"
	"github.com/aws/amazon-ssm-agent/agent/fileutil"
	"github.com/aws/amazon-ssm-agent/agent/log"
	"github.com/aws/amazon-ssm-agent/agent/s3util"
	"github.com/aws/amazon-ssm-agent/agent/updateutil/updateconstants"
	"github.com/aws/amazon-ssm-agent/agent/updateutil/updateinfo"
	"github.com/aws/amazon-ssm-agent/agent/versionutil"
	"github.com/aws/amazon-ssm-agent/common/identity"
	identity2 "github.com/aws/amazon-ssm-agent/common/identity/identity"
	"github.com/aws/amazon-ssm-agent/core/executor"
	"github.com/aws/amazon-ssm-agent/core/workerprovider/longrunningprovider/model"
)

type CommandExecutionSettings struct {
	Log         log.T
	Cmd         []string
	WorkingDir  string
	UpdaterRoot string
	StdOut      string
	StdErr      string
	IsAsync     bool
	Env         []string
}

// T represents the interface for Update utility
type T interface {
	CreateUpdateDownloadFolder() (folder string, err error)
	ExeCommand(input *CommandExecutionSettings) (pid int, exitCode updateconstants.UpdateScriptExitCode, err error)
	ExeCommandWithSlice(input *CommandExecutionSettings) (pid int, exitCode updateconstants.UpdateScriptExitCode, err error)
	ExecCommandWithOutput(input *CommandExecutionSettings) (pId int, updExitCode updateconstants.UpdateScriptExitCode, stdoutBytes *bytes.Buffer, errorBytes *bytes.Buffer, cmdErr error)
	IsServiceRunning(log log.T, i updateinfo.T) (result bool, err error)
	IsWorkerRunning(log log.T) (result bool, err error)
	WaitForServiceToStart(log log.T, i updateinfo.T, targetVersion string) (result bool, err error)
	SaveUpdatePluginResult(log log.T, updaterRoot string, updateResult *UpdatePluginResult) (err error)
	IsDiskSpaceSufficientForUpdate(log log.T) (bool, error)
	UpdateInstallDelayer(ctx context.T, updateRoot string) error
	LoadUpdateDocumentState(ctx context.T, commandId string) error
	VerifyInstalledVersion(log log.T, targetVersion string) updateconstants.ErrorCode
	UpdateExecutionTimeOut(int)
	GetExecutionTimeOut() int
}

// Utility implements interface T
type Utility struct {
	Context                               context.T
	CustomUpdateExecutionTimeoutInSeconds int
	ProcessExecutor                       executor.IExecutor
	UpdateDocState                        contracts.DocumentState
}

var getDiskSpaceInfo = fileutil.GetDiskSpaceInfo
var getRemoteProvider = identity2.GetRemoteProvider
var mkDirAll = os.MkdirAll
var openFile = os.OpenFile
var execCommand = exec.Command
var cmdStart = (*exec.Cmd).Start
var cmdOutput = (*exec.Cmd).Output

const (
	stateJson = "updatestate.json"
)

func NewUpdaterUtilWithLoadedDocContent(ctx context.T, commandId string) *Utility {
	updateUtil := &Utility{
		Context: ctx,
	}
	err := updateUtil.LoadUpdateDocumentState(ctx, commandId)
	if err != nil {
		ctx.Log().Warnf("Could not load doc content for commandID /%v/: %v", commandId, err)
	}
	return updateUtil
}

// CreateInstanceInfo create instance related information such as region, platform and arch
func (util *Utility) CreateInstanceInfo(log log.T) (context updateinfo.T, err error) {
	return nil, nil
}

// CreateUpdateDownloadFolder creates folder for storing update downloads
func (util *Utility) CreateUpdateDownloadFolder() (folder string, err error) {
	root := filepath.Join(appconfig.DownloadRoot, "update")
	if err = mkDirAll(root, os.ModePerm|os.ModeDir); err != nil {
		return "", err
	}

	return root, nil
}

// UpdateExecutionTimeOut updates the command execution timeout
func (util *Utility) UpdateExecutionTimeOut(timeout int) {
	util.CustomUpdateExecutionTimeoutInSeconds = timeout
}

// GetExecutionTimeOut returns the command execution timeout
func (util *Utility) GetExecutionTimeOut() int {
	return util.CustomUpdateExecutionTimeoutInSeconds
}

// ExecCommandWithOutput executes shell command and returns output and error of command execution
func (util *Utility) ExecCommandWithOutput(input *CommandExecutionSettings) (pId int, updExitCode updateconstants.UpdateScriptExitCode, stdoutBytes *bytes.Buffer, errorBytes *bytes.Buffer, cmdErr error) {
	log := input.Log
	parts := input.Cmd
	pid := -1
	var updateExitCode updateconstants.UpdateScriptExitCode = -1
	tempCmd := setPlatformSpecificCommand(parts)
	command := execCommand(tempCmd[0], tempCmd[1:]...)
	command.Dir = input.WorkingDir

	// Update command env if set by the caller
	// If not set, command passes the os.Environ() by default
	if input.Env != nil && len(input.Env) > 0 {
		command.Env = input.Env
	}

	stdoutWriter, stderrWriter, err := setExeOutErr(input.UpdaterRoot, input.StdOut, input.StdErr)
	if err != nil {
		return pid, updateconstants.ExitCodeErrorPrepareUpdateCommand, nil, nil, err
	}
	defer stdoutWriter.Close()
	defer stderrWriter.Close()
	var errBytes, stdOutBytes bytes.Buffer
	command.Stdout = &stdOutBytes
	command.Stderr = &errBytes

	err = cmdStart(command)
	if err != nil {
		return pid, updateconstants.ExitCodeErrorPrepareUpdateCommand, &stdOutBytes, &errBytes, err
	}

	pid = GetCommandPid(command)

	var timeout = updateconstants.DefaultUpdateExecutionTimeoutInSeconds
	if util.CustomUpdateExecutionTimeoutInSeconds != 0 {
		timeout = util.CustomUpdateExecutionTimeoutInSeconds
	}
	timer := time.NewTimer(time.Duration(timeout) * time.Second)
	go killProcessOnTimeout(log, command, timer)
	err = command.Wait()
	if errBytes.Len() != 0 {
		stderrWriter.Write(errBytes.Bytes())
	}
	if stdOutBytes.Len() != 0 {
		stdoutWriter.Write(stdOutBytes.Bytes())
	}
	timedOut := !timer.Stop()
	if err != nil {
		log.Debugf("command returned error %v", err)
		if exitErr, ok := err.(*exec.ExitError); ok {
			// The program has exited with an exit code != 0
			if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
				exitCode := status.ExitStatus()
				if exitCode == -1 && timedOut {
					// set appropriate exit code based on cancel or timeout
					exitCode = appconfig.CommandStoppedPreemptivelyExitCode
					log.Infof("The execution of command was timedout.")
				}
				updateExitCode = updateconstants.UpdateScriptExitCode(exitCode)
				err = fmt.Errorf("The execution of command returned Exit Status: %d \n %v", exitCode, err.Error())
			}
		}
		return pid, updateExitCode, &stdOutBytes, &errBytes, err
	}
	return pid, updateExitCode, &stdOutBytes, nil, nil
}

// ExeCommand executes shell command
func (util *Utility) ExeCommand(input *CommandExecutionSettings) (int, updateconstants.UpdateScriptExitCode, error) { // pid, exitCode, error
	return util.ExeCommandWithSlice(input)
}

// ExeCommandWithSlice executes shell command
func (util *Utility) ExeCommandWithSlice(input *CommandExecutionSettings) (int, updateconstants.UpdateScriptExitCode, error) { // pid, exitCode, error
	log := input.Log
	pid := -1
	var updateExitCode updateconstants.UpdateScriptExitCode = -1
	cmd := input.Cmd
	if input.IsAsync {
		command := execCommand(cmd[0], cmd[1:]...)
		command.Dir = input.WorkingDir
		util.setCommandEnvironmentVariables(command, input.Env)
		prepareProcess(command)
		// Start command asynchronously
		err := cmdStart(command)
		if err != nil {
			return pid, updateconstants.ExitCodeErrorPrepareUpdateCommand, err
		}
		pid = GetCommandPid(command)
	} else {
		tempCmd := setPlatformSpecificCommand(cmd)
		command := execCommand(tempCmd[0], tempCmd[1:]...)
		command.Dir = input.WorkingDir

		// Update command env if set by the caller
		// If not set, command passes the os.Environ() by default
		if input.Env != nil && len(input.Env) > 0 {
			command.Env = input.Env
		}
		stdoutWriter, stderrWriter, err := setExeOutErr(input.UpdaterRoot, input.StdOut, input.StdErr)
		if err != nil {
			return pid, updateconstants.ExitCodeErrorPrepareUpdateCommand, err
		}
		defer stdoutWriter.Close()
		defer stderrWriter.Close()

		command.Stdout = stdoutWriter
		command.Stderr = stderrWriter

		err = cmdStart(command)
		if err != nil {
			return pid, updateconstants.ExitCodeErrorPrepareUpdateCommand, err
		}

		pid = GetCommandPid(command)

		var timeout = updateconstants.DefaultUpdateExecutionTimeoutInSeconds
		if util.CustomUpdateExecutionTimeoutInSeconds != 0 {
			timeout = util.CustomUpdateExecutionTimeoutInSeconds
		}
		timer := time.NewTimer(time.Duration(timeout) * time.Second)
		go killProcessOnTimeout(log, command, timer)
		err = command.Wait()
		timedOut := !timer.Stop()
		if err != nil {
			log.Debugf("command returned error %v", err)
			if exitErr, ok := err.(*exec.ExitError); ok {
				// The program has exited with an exit code != 0
				if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
					exitCode := status.ExitStatus()
					if exitCode == -1 && timedOut {
						// set appropriate exit code based on cancel or timeout
						exitCode = appconfig.CommandStoppedPreemptivelyExitCode
						log.Infof("The execution of command was timedout.")
					}
					updateExitCode = updateconstants.UpdateScriptExitCode(exitCode)
					err = fmt.Errorf("The execution of command returned Exit Status: %d \n %v", exitCode, err.Error())
				}
			}

			return pid, updateExitCode, err
		}
	}
	return pid, updateExitCode, nil
}

// TODO move to commandUtil
// ExeCommandOutput executes shell command and returns the stdout
func (util *Utility) ExeCommandOutput(
	log log.T,
	cmd string,
	parameters []string,
	workingDir string,
	outputRoot string,
	stdOutFileName string,
	stdErrFileName string,
	usePlatformSpecificCommand bool) (output string, err error) {

	parts := append([]string{cmd}, parameters...) //strings.Fields(cmd)
	var tempCmd []string
	if usePlatformSpecificCommand {
		tempCmd = setPlatformSpecificCommand(parts)
	} else {
		tempCmd = parts
	}

	command := execCommand(tempCmd[0], tempCmd[1:]...)
	command.Dir = workingDir
	util.setCommandEnvironmentVariables(command, nil)
	stdoutWriter, stderrWriter, exeErr := setExeOutErr(outputRoot, stdOutFileName, stdErrFileName)
	if exeErr != nil {
		return output, exeErr
	}
	defer stdoutWriter.Close()
	defer stderrWriter.Close()

	// Don't set command.Stdout - we're going to return it instead of writing it
	command.Stderr = stderrWriter

	// Run the command and return its output
	var out []byte
	out, err = cmdOutput(command)
	output = string(out)
	// Write the returned output so that we can upload it if needed
	stdoutWriter.Write(out)
	if err != nil {
		return
	}

	return output, err
}

// TODO move to commandUtil
// ExeCommandOutput executes shell command and returns the stdout
func (util *Utility) NewExeCommandOutput(
	log log.T,
	cmd string,
	parameters []string,
	workingDir string,
	outputRoot string,
	stdoutWriter io.Writer,
	stderrWriter io.Writer,
	usePlatformSpecificCommand bool) (output string, err error) {

	parts := append([]string{cmd}, parameters...) //strings.Fields(cmd)
	var tempCmd []string
	if usePlatformSpecificCommand {
		tempCmd = setPlatformSpecificCommand(parts)
	} else {
		tempCmd = parts
	}

	command := execCommand(tempCmd[0], tempCmd[1:]...)
	command.Dir = workingDir
	util.setCommandEnvironmentVariables(command, nil)
	// Don't set command.Stdout - we're going to return it instead of writing it
	command.Stderr = stderrWriter

	// Run the command and return its output
	var out []byte
	out, err = cmdOutput(command)
	output = string(out)
	// Write the returned output so that we can upload it if needed
	stdoutWriter.Write(out)
	if err != nil {
		return
	}

	return output, err
}

// IsServiceRunning returns is service running
func (util *Utility) IsServiceRunning(log log.T, i updateinfo.T) (result bool, err error) {
	commandOutput := []byte{}
	expectedOutput := ""
	isSystemD := false
	isDarwin := false

	// For mac OS check the running processes
	isDarwin = i.IsPlatformDarwin()
	var allProcesses []executor.OsProcess
	if util.ProcessExecutor == nil {
		util.ProcessExecutor = executor.NewProcessExecutor(log)
	}

	// isSystemD will always be false for Windows
	if isSystemD, err = i.IsPlatformUsingSystemD(); err != nil {
		return false, err
	}

	if isSystemD {
		expectedOutput = "Active: active (running)"
		if commandOutput, err = execCommand("systemctl", "status", "amazon-ssm-agent.service").Output(); err != nil {
			//test snap service enabled
			if commandOutput, err = execCommand("systemctl", "status", "snap.amazon-ssm-agent.amazon-ssm-agent.service").Output(); err != nil {
				return false, err
			}
		}
		agentStatus := strings.TrimSpace(string(commandOutput))
		return strings.Contains(agentStatus, expectedOutput), nil
	} else if isDarwin {
		if allProcesses, err = util.ProcessExecutor.Processes(); err != nil {
			return false, err
		}
		for _, process := range allProcesses {
			if process.Executable == updateconstants.DarwinBinaryPath {
				return true, nil
			}
		}
		return false, nil
	}

	return isAgentServiceRunning(log)
}

// IsWorkerRunning returns true if ssm-agent-worker running
func (util *Utility) IsWorkerRunning(log log.T) (result bool, err error) {
	var allProcesses []executor.OsProcess
	if util.ProcessExecutor == nil {
		util.ProcessExecutor = executor.NewProcessExecutor(log)
	}
	if allProcesses, err = util.ProcessExecutor.Processes(); err != nil {
		return false, err
	}
	for _, process := range allProcesses {
		if process.Executable == model.SSMAgentWorkerBinaryName {
			log.Infof("Detect SSM Agent worker running")
			return true, nil
		}
	}

	// For snap, find the work directory
	if _, err := os.Stat(updateconstants.SnapServiceFile); err == nil {
		log.Infof("snap is installed")
		file, err := os.Open(updateconstants.SnapServiceFile)
		if err != nil {
			return false, fmt.Errorf("failed to open amazon-ssm-agent.service file %s", err)
		}
		defer file.Close()

		scanner := bufio.NewScanner(file)
		for scanner.Scan() {
			line := scanner.Text()

			if strings.HasPrefix(line, "WorkingDirectory") {
				splitResults := strings.Split(line, "/var")
				if len(splitResults) <= 1 {
					return false, fmt.Errorf("failed to find amazon-ssm-agent working directory %s", line)
				}

				workerName := filepath.Join(splitResults[1], model.SSMAgentWorkerName)
				log.Infof("identified worker name %s", workerName)
				for _, process := range allProcesses {
					if process.Executable == workerName {
						log.Infof("Detect SSM Agent worker running")
						return true, nil
					}
				}
			}
		}
	}

	return false, nil
}

// VerifyInstalledVersion verifies whether the expected version is installed on Windows instance.
// For linux, this function always return true
func (util *Utility) VerifyInstalledVersion(log log.T, targetVersion string) updateconstants.ErrorCode {
	return verifyVersion(log, targetVersion)
}

// WaitForServiceToStart wait for service to start and returns is service started
func (util *Utility) WaitForServiceToStart(log log.T, i updateinfo.T, targetVersion string) (result bool, svcRunningErr error) {
	const (
		verifyAttemptCount              = 36
		verifyRetryIntervalMilliseconds = 5000
	)
	isRunning := false
	isWorkerRunning := true
	var workRunningErr error
	for attempt := 0; attempt < verifyAttemptCount; attempt++ {
		if attempt > 0 {
			log.Infof("Retrying update health check %v out of %v", attempt+1, verifyAttemptCount)
			time.Sleep(time.Duration(verifyRetryIntervalMilliseconds) * time.Millisecond)
		}

		isRunning, svcRunningErr = util.IsServiceRunning(log, i)
		if isRunning {
			log.Infof("health check: amazon-ssm-agent is running")
		} else {
			log.Infof("health check: amazon-ssm-agent is not running")
		}

		compareResult, err := versionutil.VersionCompare(targetVersion, updateconstants.SSMAgentWorkerMinVersion)
		if err == nil && compareResult >= 0 {
			isWorkerRunning, workRunningErr = util.IsWorkerRunning(log)
			if isWorkerRunning {
				log.Infof("health check: %s is running", model.SSMAgentWorkerName)
			} else {
				if workRunningErr != nil {
					log.Warnf("health check: failed to get state of %s: %v", model.SSMAgentWorkerName, workRunningErr)
				} else {
					log.Infof("health check: %s is not running", model.SSMAgentWorkerName)
				}
			}
		}

		if svcRunningErr == nil && workRunningErr == nil && isRunning && isWorkerRunning {
			return true, nil
		}

	}

	errorMessage := "checking svc and agent worker running err"
	if svcRunningErr != nil {
		errorMessage += ", " + svcRunningErr.Error()
	}
	if workRunningErr != nil {
		errorMessage += ", " + workRunningErr.Error()
	}

	return false, fmt.Errorf("%s", errorMessage)
}

// IsDiskSpaceSufficientForUpdate loads disk space info and checks the available bytes
// Returns true if the system has at least 100 Mb for available disk space or false if it is less than 100 Mb
func (util *Utility) IsDiskSpaceSufficientForUpdate(log log.T) (bool, error) {
	var diskSpaceInfo fileutil.DiskSpaceInfo
	var err error

	// Get the available disk space
	if diskSpaceInfo, err = getDiskSpaceInfo(); err != nil {
		log.Infof("Failed to load disk space info - %v", err)
		return false, err
	}

	// Return false if available disk space is less than 100 Mb
	if diskSpaceInfo.AvailBytes < updateconstants.MinimumDiskSpaceForUpdate {
		log.Infof("Insufficient available disk space - %d Mb", diskSpaceInfo.AvailBytes/int64(1024*1024))
		return false, nil
	}

	// Return true otherwise
	return true, nil
}

// BuildMessage builds the messages with provided format, error and arguments
func BuildMessage(err error, format string, params ...interface{}) (message string) {
	message = fmt.Sprintf(format, params...)
	if err != nil {
		message = fmt.Sprintf("%v, ErrorMessage=%v", message, err.Error())
	}
	return message
}

// BuildMessages builds the messages with provided format, error and arguments
func BuildMessages(errs []error, format string, params ...interface{}) (message string) {
	message = fmt.Sprintf(format, params...)
	errMessage := ""
	if len(errs) > 0 {
		for _, err := range errs {
			if errMessage == "" {
				errMessage = err.Error()
			} else {
				errMessage = fmt.Sprintf("%v, %v", errMessage, err.Error())
			}
		}

		message = fmt.Sprintf("%v, ErrorMessage=%v", message, errMessage)
	}
	return message
}

// BuildUpdateCommand builds command string with argument and value
func BuildUpdateCommand(cmd string, arg string, value string) string {
	if value == "" || arg == "" {
		return cmd
	}
	return fmt.Sprintf("%v -%v %v", cmd, arg, value)
}

// UpdateArtifactFolder returns the folder path for storing all the update artifacts
func UpdateArtifactFolder(updateRoot string, packageName string, version string) (folder string) {
	return filepath.Join(updateRoot, packageName, version)
}

// UpdateContextFilePath returns Context file path
func UpdateContextFilePath(updateRoot string) (filePath string) {
	return filepath.Join(updateRoot, updateconstants.UpdateContextFileName)
}

// UpdateOutputDirectory returns output directory
func UpdateOutputDirectory(updateRoot string) string {
	return filepath.Join(updateRoot, updateconstants.DefaultOutputFolder)
}

// UpdateStdOutPath returns stand output file path
func UpdateStdOutPath(updateRoot string, fileName string) string {
	if fileName == "" {
		fileName = updateconstants.DefaultStandOut
	}
	return filepath.Join(updateRoot, fileName)
}

// UpdateStdErrPath returns stand error file path
func UpdateStdErrPath(updateRoot string, fileName string) string {
	if fileName == "" {
		fileName = updateconstants.DefaultStandErr
	}
	return filepath.Join(updateRoot, fileName)
}

// UpdatePluginResultFilePath returns update plugin result file path
func UpdatePluginResultFilePath(updateRoot string) (filePath string) {
	return filepath.Join(updateRoot, updateconstants.UpdatePluginResultFileName)
}

// UpdaterFilePath returns updater file path
func UpdaterFilePath(updateRoot string, updaterPackageName string, version string) (filePath string) {
	return filepath.Join(UpdateArtifactFolder(updateRoot, updaterPackageName, version), updateconstants.Updater)
}

// InstallerFilePath returns Installer file path
func InstallerFilePath(updateRoot string, packageName string, version string, installer string) (file string) {
	return filepath.Join(UpdateArtifactFolder(updateRoot, packageName, version), installer)
}

// UnInstallerFilePath returns UnInstaller file path
func UnInstallerFilePath(updateRoot string, packageName string, version string, uninstaller string) (file string) {
	return filepath.Join(UpdateArtifactFolder(updateRoot, packageName, version), uninstaller)
}

func killProcessOnTimeout(log log.T, command *exec.Cmd, timer *time.Timer) {
	defer func() {
		if r := recover(); r != nil {
			log.Errorf("Kill process on timeout panic: \n%v", r)
			log.Errorf("Stacktrace:\n%s", debug.Stack())
		}
	}()
	<-timer.C
	log.Debug("Process exceeded timeout. Attempting to kill process!")

	// task has been exceeded the allowed execution timeout, kill process
	if err := command.Process.Kill(); err != nil {
		log.Error(err)
		return
	}

	log.Debug("Done kill process!")
}

// setExeOutErr creates stderr and stdout file
func setExeOutErr(
	updaterRoot string,
	stdOutFileName string,
	stdErrFileName string) (stdoutWriter *os.File, stderrWriter *os.File, err error) {

	if err = mkDirAll(UpdateOutputDirectory(updaterRoot), appconfig.ReadWriteExecuteAccess); err != nil {
		return
	}

	stdOutPath := UpdateStdOutPath(updaterRoot, stdOutFileName)
	stdErrPath := UpdateStdErrPath(updaterRoot, stdErrFileName)

	// create stdout file
	// Allow append so that if arrays of run command write to the same file, we keep appending to the file.
	if stdoutWriter, err = openFile(stdOutPath, appconfig.FileFlagsCreateOrAppend, appconfig.ReadWriteAccess); err != nil {
		return
	}

	// create stderr file
	// Allow append so that if arrays of run command write to the same file, we keep appending to the file.
	if stderrWriter, err = openFile(stdErrPath, appconfig.FileFlagsCreateOrAppend, appconfig.ReadWriteAccess); err != nil {
		return
	}

	return stdoutWriter, stderrWriter, nil
}

// getCommandPid returns the pid of the process if present, defaults to pid -1
func GetCommandPid(cmd *exec.Cmd) int {
	if cmd != nil && cmd.Process != nil {
		return cmd.Process.Pid
	}
	return -1
}

func CompareVersion(versionOne string, versionTwo string) (int, error) {
	majorOne, minorOne, buildOne, patchOne, err := parseVersion(versionOne)
	if err != nil {
		return 0, err
	}

	majorTwo, minorTwo, buildTwo, patchTwo, err := parseVersion(versionTwo)
	if err != nil {
		return 0, err
	}

	if majorOne < majorTwo {
		return -1, nil
	} else if majorOne > majorTwo {
		return 1, nil
	}

	if minorOne < minorTwo {
		return -1, nil
	} else if minorOne > minorTwo {
		return 1, nil
	}

	if buildOne < buildTwo {
		return -1, nil
	} else if buildOne > buildTwo {
		return 1, nil
	}

	if patchOne < patchTwo {
		return -1, nil
	} else if patchOne > patchTwo {
		return 1, nil
	}

	return 0, nil
}

func parseVersion(version string) (uint64, uint64, uint64, uint64, error) {
	parts := strings.SplitN(version, ".", 4)
	if len(parts) != 4 {
		return 0, 0, 0, 0, errors.New("No Major.Minor.Build.Patch elements found")
	}

	major, err := strconv.ParseUint(parts[0], 10, 64)
	if err != nil {
		return 0, 0, 0, 0, err
	}

	minor, err := strconv.ParseUint(parts[1], 10, 64)
	if err != nil {
		return 0, 0, 0, 0, err
	}

	build, err := strconv.ParseUint(parts[2], 10, 64)
	if err != nil {
		return 0, 0, 0, 0, err
	}

	patch, err := strconv.ParseUint(parts[3], 10, 64)
	if err != nil {
		return 0, 0, 0, 0, err
	}

	return major, minor, build, patch, nil
}

// ConvertToUpdateErrorCode joins the error string array passed and converts to update error code
func ConvertToUpdateErrorCode(errorCodes ...string) updateconstants.ErrorCode {
	var updateErrorCode bytes.Buffer
	for _, errCode := range errorCodes {
		updateErrorCode.WriteString(errCode)
	}
	return updateconstants.ErrorCode(updateErrorCode.String())
}

// GetManifestURLFromSourceUrl parses source url passed to the updater and generates the url for manifest
func GetManifestURLFromSourceUrl(sourceURL string) (string, error) {
	u, err := url.Parse(sourceURL)

	if err != nil {
		return "", err
	}

	pathArgs := strings.Split(u.Path, "/")
	if len(pathArgs) < 4 || len(pathArgs) > 5 {
		return "", fmt.Errorf("URL does not have expected path structure: %s", sourceURL)
	} else if len(pathArgs) == 4 {
		// Case for: https://{bucket}.s3.{region}.amazonaws.com/amazon-ssm-agent/{version}/amazon-ssm-agent.tar.gz
		u.Path = ""
	} else {
		// Case for: https://s3.{region}.amazonaws.com/{bucket}/amazon-ssm-agent/{version}/amazon-ssm-agent.tar.gz
		u.Path = "/" + pathArgs[1]
	}

	u.Path += "/" + updateconstants.ManifestFile

	return u.String(), nil
}

func GetStableURLFromManifestURL(manifestURL string, identity identity.IAgentIdentity) (string, error) {
	if !strings.HasSuffix(manifestURL, "/"+updateconstants.ManifestFile) {
		return "", fmt.Errorf("unexpected manifest url does not end with manifest file: %s", manifestURL)
	}
	stableVersionPath := "stable/VERSION"
	region, err := identity.Region()
	if err != nil {
		return "", err
	}

	stableVersionURL := strings.TrimRight(manifestURL, updateconstants.ManifestFile) + stableVersionPath
	stableVersionURL = strings.Replace(stableVersionURL, updateconstants.RegionHolder, region, -1)
	return stableVersionURL, nil
}

// ResolveAgentReleaseBucketURL makes best effort to generate an url for the ssm agent bucket
func ResolveAgentReleaseBucketURL(region string, identity identity.IAgentIdentity) string {
	s3Url := ""
	if dynamicS3Endpoint := identity.GetServiceEndpoint("s3"); dynamicS3Endpoint != "" {
		s3Url = "https://" + dynamicS3Endpoint
	} else if strings.HasPrefix(region, s3util.ChinaRegionPrefix) {
		s3Url = updateconstants.ChinaS3URL
	} else {
		s3Url = updateconstants.CommonS3URL
	}

	return strings.Replace(s3Url+updateconstants.BucketPath, updateconstants.RegionHolder, region, -1)
}

// IsV1UpdatePlugin returns true if source agent version is equal or below 3.0.882.0, any error defaults to false
//
//	this logic is required since moving logic from plugin to updater would otherwise lead
//	to duplicate aws console logging when upgrading from V1UpdatePlugin agents
func IsV1UpdatePlugin(SourceVersion string) bool {
	const LastV1UpdatePluginAgentVersion = "3.0.882.0"

	comp, err := versionutil.VersionCompare(SourceVersion, LastV1UpdatePluginAgentVersion)
	return err == nil && comp <= 0
}

// IsIdentityRuntimeConfigSupported returns true if source agent version is greater than 3.1.282.0, any error defaults to false
// this logic is required to ensure that updater does not rely on an old runtime config if an agent has been downgraded
// from a version that supported identity runtimeconfig and also to skip the runtimeconfig check when we know it does not exist
func IsIdentityRuntimeConfigSupported(sourceVersion string) bool {
	const LastAgentVersionBeforeIdentityRuntimeConfig = "3.1.282.0"

	comp, err := versionutil.VersionCompare(sourceVersion, LastAgentVersionBeforeIdentityRuntimeConfig)
	return err == nil && comp > 0
}

func (util *Utility) setCommandEnvironmentVariables(command *exec.Cmd, commandEnv []string) {
	credentialProvider, ok := getRemoteProvider(util.Context.Identity())
	if !ok {
		return
	}

	// Don't set environment variables if credentials are not being shared
	if !credentialProvider.SharesCredentials() {
		return
	}

	var osEnv []string
	// set environment variables
	if commandEnv != nil && len(commandEnv) > 0 {
		// Update command env if set by the caller
		// If not set, command passes the os.Environ() by default
		osEnv = commandEnv
	} else {
		osEnv = os.Environ()
	}

	command.Env = osEnv
	if _, ok := os.LookupEnv("AWS_SHARED_CREDENTIALS_FILE"); !ok && credentialProvider.ShareFile() != "" {
		command.Env = append(command.Env, fmt.Sprintf("%s=%s", "AWS_SHARED_CREDENTIALS_FILE", credentialProvider.ShareFile()))
	}

	if _, ok := os.LookupEnv("AWS_PROFILE"); !ok && credentialProvider.ShareProfile() != "" {
		command.Env = append(command.Env, fmt.Sprintf("%s=%s", "AWS_PROFILE", credentialProvider.ShareProfile()))
	}

}
