// 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 windowscontainerutil implements the the install and uninstall steps for windows for the configurecontainers plugin.
package windowscontainerutil

import (
	"errors"
	"fmt"
	"os"
	"strings"
	"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/fileutil/artifact"
	"github.com/aws/amazon-ssm-agent/agent/framework/processor/executer/iohandler"
	"github.com/aws/amazon-ssm-agent/agent/plugins/pluginutil"
)

const (
	DOCKER_DOWNLOAD_URL = "https://download.docker.com/win/static/stable/x86_64/docker-27.3.1.zip"
)

var (
	PROGRAM_FILES_DIRECTORY    = os.Getenv("ProgramFiles")
	DOCKER_INSTALLED_DIRECTORY = PROGRAM_FILES_DIRECTORY + "\\docker"
	PROGRAM_DATA_DIRECTORY     = os.Getenv("ProgramData")
)

func RunInstallCommands(context context.T, orchestrationDirectory string, out iohandler.IOHandler) {
	var err error
	var command string
	var platformVersion string
	var parameters []string
	var requireReboot bool

	var isNanoServer bool
	var output string

	log := context.Log()
	platformVersion, err = dep.PlatformVersion(log)
	if err != nil {
		log.Error("Error detecting platform version", err)
		out.MarkAsFailed(fmt.Errorf("Error detecting platform version: %v", err))
		return
	}
	log.Debug("Platform Version:", platformVersion)
	if !strings.HasPrefix(platformVersion, "10") {
		out.MarkAsFailed(errors.New("ConfigureDocker is only supported on Microsoft Windows Server 2016 and above."))
		return
	}

	isNanoServer, err = dep.IsPlatformNanoServer(log)
	if err != nil {
		log.Error("Error detecting if Nano Server", err)
		out.MarkAsFailed(fmt.Errorf("Error detecting if Nano Server: %v", err))
		return
	}
	if isNanoServer {
		command = "(Get-PackageProvider -ListAvailable).Name"
		parameters = make([]string, 0)
		output, err = dep.UpdateUtilExeCommandOutput(context, 120, log, command, parameters, "", "", "", "", true)
		if err != nil {
			log.Error("Error getting package providers", err)
			out.MarkAsFailed(fmt.Errorf("Error getting package providers: %v", err))
			return
		}
		log.Debug("Get-PackageProvider output:", output)
		packageInstalled := strings.Contains(output, "NanoServerPackage")

		if !packageInstalled {
			out.AppendInfo("Installing Nano Server package provider.")
			command = `Install-PackageProvider -Name Nuget -MinimumVersion 2.8.5.201 -Force`
			parameters = make([]string, 0)
			output, err = dep.UpdateUtilExeCommandOutput(context, 60, log, command, parameters, "", "", "", "", true)
			if err != nil {
				log.Error("Error installing Nuget package provider", err)
				out.MarkAsFailed(fmt.Errorf("Error installing Nuget package provider: %v", err))
				return
			}
			log.Debug("Install Package provider output:", output)

			command = `Save-Module -Path "$env:programfiles\WindowsPowerShell\Modules\" -Name NanoServerPackage -minimumVersion 1.0.1.0`
			parameters = make([]string, 0)
			output, err = dep.UpdateUtilExeCommandOutput(context, 60, log, command, parameters, "", "", "", "", true)
			if err != nil {
				log.Error("Error saving module", err)
				out.MarkAsFailed(fmt.Errorf("Error saving Nano server package: %v", err))
				return
			}
			log.Debug("Save-Module output:", output)

			command = `Import-PackageProvider NanoServerPackage`
			parameters = make([]string, 0)
			output, err = dep.UpdateUtilExeCommandOutput(context, 30, log, command, parameters, "", "", "", "", true)
			if err != nil {
				log.Error("Error importing package", err)
				out.MarkAsFailed(fmt.Errorf("Error importing package: %v", err))
				return
			}
			log.Debug("Import-PackageProvider output:", output)
		}

		//Install containers package
		command = "(Get-Package -providername NanoServerPackage).Name"
		parameters = make([]string, 0)
		output, err = dep.UpdateUtilExeCommandOutput(context, 30, log, command, parameters, "", "", "", "", true)
		if err != nil {
			log.Error("Error getting microsoft-nanoserver-containers-package", err)
			out.MarkAsFailed(fmt.Errorf("Error getting microsoft-nanoserver-containers-package: %v", err))
			return
		}
		log.Debug("Get-Package output:", output)
		packageInstalled = strings.Contains(output, "Microsoft-NanoServer-Containers-Package")

		if !packageInstalled {
			out.AppendInfo("Installing containers package.")
			command = "Install-NanoServerPackage microsoft-nanoserver-containers-package"
			parameters = make([]string, 0)
			output, err = dep.UpdateUtilExeCommandOutput(context, 30, log, command, parameters, "", "", "", "", true)
			if err != nil {
				log.Error("Error installing microsoft-nanoserver-containers-package", err)
				out.MarkAsFailed(fmt.Errorf("Error installing microsoft-nanoserver-containers-package: %v", err))
				return
			}
			log.Debug("Install-NanoServerPackage output:", output)
			requireReboot = true
		}
	} else {
		//install windows containers feature
		command = "(Get-WindowsFeature -Name containers).Installed"
		parameters = make([]string, 0)
		output, err = dep.UpdateUtilExeCommandOutput(context, 30, log, command, parameters, "", "", "", "", true)
		if err != nil {
			log.Error("Error getting containers feature", err)
			out.MarkAsFailed(fmt.Errorf("Error getting containers feature: %v", err))
			return
		}
		log.Debug("Get-WindowsFeature output:", output)
		packageInstalled := strings.Contains(output, "True")

		if !packageInstalled {
			out.AppendInfo("Installing Windows containers feature.")
			command = "(Install-WindowsFeature -Name containers).RestartNeeded"
			parameters = make([]string, 0)
			output, err = dep.UpdateUtilExeCommandOutput(context, 30, log, command, parameters, "", "", "", "", true)
			if err != nil {
				log.Error("Error installing Windows containers feature", err)
				out.MarkAsFailed(fmt.Errorf("Error installing Windows containers feature: %v", err))
				return
			}
			log.Debug("Install-WindowsFeature output:", output)
			requireReboot = strings.Contains(output, "Yes")
		}
	}

	//Create docker config if it does not exist
	daemonConfigPath := PROGRAM_DATA_DIRECTORY + "\\docker\\config\\daemon.json"
	daemonConfigContent := `{}`

	if err := dep.SetDaemonConfig(daemonConfigPath, daemonConfigContent); err != nil {
		log.Error("Error writing Docker daemon config file", err)
		out.MarkAsFailed(fmt.Errorf("Error writing Docker daemon config file: %v", err))
		return
	}

	//Download docker
	var downloadOutput artifact.DownloadOutput
	downloadOutput, err = dep.ArtifactDownload(context, artifact.DownloadInput{SourceURL: DOCKER_DOWNLOAD_URL, DestinationDirectory: appconfig.DownloadRoot})
	if err != nil {
		log.Errorf("Failed to download Docker Engine file from %v: %v", DOCKER_DOWNLOAD_URL, err)
		out.MarkAsFailed(fmt.Errorf("Failed to download Docker Engine file from %v: %v", DOCKER_DOWNLOAD_URL, err))
		return
	}
	log.Debugf("Zip file downloaded to %v", downloadOutput.LocalFilePath)

	_, installedErr := os.Stat(DOCKER_INSTALLED_DIRECTORY)
	if downloadOutput.IsUpdated || installedErr != nil {
		out.AppendInfo("Unzipping Docker to program files directory.")
		//uncompress docker zip
		fileutil.Uncompress(log, downloadOutput.LocalFilePath, PROGRAM_FILES_DIRECTORY)
	}

	// delete downloaded file, if it exists
	pluginutil.CleanupFile(log, downloadOutput.LocalFilePath)

	//Set this process's path environment variable to include Docker
	if !strings.Contains(strings.ToLower(os.Getenv("path")), strings.ToLower(DOCKER_INSTALLED_DIRECTORY)) {
		out.AppendInfo("Setting process path variable to include docker directory")
		//set envvariable for this process
		os.Setenv("path", DOCKER_INSTALLED_DIRECTORY+";"+os.Getenv("path"))

	}
	log.Debug("Path set to ", os.Getenv("path"))

	//set path env variable for machine to include Docker
	var currentSystemPathValue string
	currentSystemPathValue, _, err = dep.LocalRegistryKeyGetStringValue(`System\CurrentControlSet\Control\Session Manager\Environment`, "Path")
	if err != nil {
		log.Error("Error getting current machine registry key value", err)
		out.MarkAsFailed(fmt.Errorf("Error getting current machine registry key value: %v", err))
		return
	}
	log.Debug("System Path set to ", currentSystemPathValue)
	if !strings.Contains(strings.ToLower(currentSystemPathValue), strings.ToLower(DOCKER_INSTALLED_DIRECTORY)) {
		out.AppendInfo("Setting machine path variable to include docker directory")
		command = "setx"
		parameters = []string{"-m", "path", os.Getenv("path")}
		var setPathOutput string
		setPathOutput, err = dep.UpdateUtilExeCommandOutput(context, 10, log, command, parameters, "", "", "", "", false)
		if err != nil {
			log.Error("Error setting machine path environment variable", err)
			out.MarkAsFailed(fmt.Errorf("Error setting machine path environment variable: %v", err))
			return
		}
		log.Debug("setx path output:", setPathOutput)
	}

	//reboot if needed
	if requireReboot {
		out.AppendInfo("Rebooting machine to complete install")
		log.Debug("require reboot is true")
		out.SetStatus(contracts.ResultStatusSuccessAndReboot)
		return
	}

	//Check if docker daemon registered
	var dockerServiceStatusOutput string
	command = "(Get-Service docker).Status"
	parameters = make([]string, 0)
	dockerServiceStatusOutput, err = dep.UpdateUtilExeCommandOutput(context, 120, log, command, parameters, "", "", "", "", true)
	if err != nil {
		log.Error("Error getting Docker service status", err)
		out.MarkAsFailed(fmt.Errorf("Error getting Docker service status: %v", err))
		return
	}
	log.Debug("Get-Service output:", dockerServiceStatusOutput)

	ServiceRunning := strings.HasPrefix(dockerServiceStatusOutput, "Running")

	//Register Service
	if len(strings.TrimSpace(dockerServiceStatusOutput)) == 0 {
		out.AppendInfo("Registering dockerd.")

		command = `dockerd`
		log.Debug("dockerd cmd:", command)
		parameters = []string{"--register-service"}
		dockerServiceStatusOutput, err = dep.UpdateUtilExeCommandOutput(context, 120, log, command, parameters, DOCKER_INSTALLED_DIRECTORY, "", "", "", false)
		if err != nil {
			log.Error("Error registering Docker service", err)
			out.MarkAsFailed(fmt.Errorf("Error registering Docker service: %v", err))
			return
		}
		log.Debug("dockerd output:", dockerServiceStatusOutput)
		//set service to delayed start
		out.AppendInfo("set dockerd service configuration.")
		command = "sc.exe"
		parameters = []string{"config", "docker", "start=delayed-auto"}
		dockerServiceStatusOutput, err = dep.UpdateUtilExeCommandOutput(context, 10, log, command, parameters, "", "", "", "", false)
		if err != nil {
			log.Error("Error setting delayed start for Docker service", err)
			out.MarkAsFailed(fmt.Errorf("Error setting delayed start for Docker service: %v", err))
			return
		}
		log.Debug("sc output:", dockerServiceStatusOutput)
		//sleep 10 sec after registering
		time.Sleep(10 * time.Second)
	}
	err = dep.LocalRegistryKeySetDWordValue(`SYSTEM\CurrentControlSet\services\docker`, "AutoStartDelay", 240)
	if err != nil {
		log.Error("Error opening registry key to set Docker delayed start", err)
		out.MarkAsFailed(fmt.Errorf("Error opening registry key to set Docker delayed start: %v", err))
		return
	}

	//Start service
	if !ServiceRunning {
		out.AppendInfo("Starting Docker service.")
		command = "Start-Service docker"
		parameters = make([]string, 0)
		dockerServiceStatusOutput, err = dep.UpdateUtilExeCommandOutput(context, 300, log, command, parameters, "", "", "", "", true)
		if err != nil {

			log.Error("Error starting Docker service", err)
			out.MarkAsFailed(fmt.Errorf("Error starting Docker service: %v", err))
			return
		}
		log.Debug("start-service output:", dockerServiceStatusOutput)
	}
	out.AppendInfo("Installation complete")
	log.Debug("require reboot:", requireReboot)
	out.SetStatus(contracts.ResultStatusSuccess)
	return
}

func RunUninstallCommands(context context.T, orchestrationDirectory string, out iohandler.IOHandler) {
	var err error
	var command string
	var parameters []string
	var requireReboot bool
	var platformVersion string

	var isNanoServer bool
	var output string

	var log = context.Log()
	platformVersion, err = dep.PlatformVersion(log)
	if err != nil {
		log.Error("Error detecting platform version", err)
		out.MarkAsFailed(fmt.Errorf("Error detecting platform version: %v", err))
		return
	}
	log.Debug("Platform Version:", platformVersion)
	if !strings.HasPrefix(platformVersion, "10") {
		out.MarkAsFailed(errors.New("ConfigureDocker is only supported on Microsoft Windows Server 2016 and above."))
		return
	}

	//Check if docker daemon registered and running
	var dockerServiceStatusOutput string
	command = "(Get-Service docker).Status"
	parameters = make([]string, 0)
	dockerServiceStatusOutput, err = dep.UpdateUtilExeCommandOutput(context, 120, log, command, parameters, "", "", "", "", true)
	if err != nil {
		log.Error("Error getting Docker service status", err)
		out.MarkAsFailed(fmt.Errorf("Error getting Docker service status: %v", err))
		return
	}
	log.Debug("Get-Service output:", dockerServiceStatusOutput)

	ServiceRunning := strings.Contains(dockerServiceStatusOutput, "Running")

	//Stop service
	if ServiceRunning {
		out.AppendInfo("Stopping Docker Service.")
		command = "Stop-Service docker"
		parameters = make([]string, 0)
		dockerServiceStatusOutput, err = dep.UpdateUtilExeCommandOutput(context, 180, log, command, parameters, "", "", "", "", true)
		if err != nil {
			log.Error("Error stopping Docker service", err)
			out.MarkAsFailed(fmt.Errorf("Error stopping Docker service: %v", err))
			return
		}
		log.Debug("stop-service output:", dockerServiceStatusOutput)
	}

	//Unregister Service
	if len(strings.TrimSpace(dockerServiceStatusOutput)) > 0 {
		out.AppendInfo("Unregistering dockerd service.")

		command = `dockerd`
		log.Debug("dockerd cmd:", command)
		parameters = []string{"--unregister-service"}
		dockerServiceStatusOutput, err = dep.UpdateUtilExeCommandOutput(context, 120, log, command, parameters, "", "", "", "", true)
		if err != nil {
			log.Error("Error unregistering Docker service", err)
			out.MarkAsFailed(fmt.Errorf("Error unregistering Docker service: %v", err))
			return
		}
		log.Debug("dockerd output:", dockerServiceStatusOutput)

	}

	//Remove docker directory
	if _, err := os.Stat(DOCKER_INSTALLED_DIRECTORY); err == nil {
		out.AppendInfo("Removing Docker directory.")
		os.RemoveAll(DOCKER_INSTALLED_DIRECTORY)
	}

	//check if Nano
	isNanoServer, err = dep.IsPlatformNanoServer(log)
	if err != nil {
		log.Error("Error detecting if Nano Server", err)
		out.MarkAsFailed(fmt.Errorf("Error detecting if Nano Server: %v", err))
		return
	}

	if isNanoServer {
		out.AppendInfo("Removing packages from Nano server not currently supported.")
	} else {
		//uninstall windows containers feature
		command = "(Get-WindowsFeature -Name containers).Installed"
		parameters = make([]string, 0)
		output, err = dep.UpdateUtilExeCommandOutput(context, 50, log, command, parameters, "", "", "", "", true)
		if err != nil {
			log.Error("Error getting containers feature", err)
			out.MarkAsFailed(fmt.Errorf("Error getting containers feature: %v", err))
			return
		}
		log.Debug("Get-WindowsFeature output:", output)
		packageInstalled := strings.Contains(output, "True")

		if packageInstalled {
			out.AppendInfo("Uninstalling containers Windows feature.")
			command = "(Uninstall-WindowsFeature -Name containers).RestartNeeded"
			parameters = make([]string, 0)
			output, err = dep.UpdateUtilExeCommandOutput(context, 300, log, command, parameters, "", "", "", "", true)
			if err != nil {
				log.Error("Error uninstalling containers Windows feature", err)
				out.MarkAsFailed(fmt.Errorf("Error uninstalling containers Windows feature: %v", err))
				return
			}
			log.Debug("Uninstall-WindowsFeature output:", output)
			requireReboot = strings.Contains(output, "Yes")
			log.Debug("Requireboot:", requireReboot)
		}
		//reboot if needed
		if requireReboot {
			out.AppendInfo("Rebooting machine to complete install")
			log.Debug("require reboot is true", requireReboot)
			out.SetStatus(contracts.ResultStatusSuccessAndReboot)
			return
		}
	}
	out.AppendInfo("Uninstallation complete.")
	log.Debug("Uninstallation complete")
	out.SetStatus(contracts.ResultStatusSuccess)

	return
}
