// 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.
//
//go:build windows
// +build windows

// Package cloudwatch implements cloudwatch plugin and its configuration
package cloudwatch

import (
	"bytes"
	"errors"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"

	"github.com/aws/amazon-ssm-agent/agent/appconfig"
	"github.com/aws/amazon-ssm-agent/agent/context"
	"github.com/aws/amazon-ssm-agent/agent/executers"
	"github.com/aws/amazon-ssm-agent/agent/fileutil"
	"github.com/aws/amazon-ssm-agent/agent/framework/processor/executer/iohandler"
	"github.com/aws/amazon-ssm-agent/agent/jsonutil"
	"github.com/aws/amazon-ssm-agent/agent/log/logger"
	"github.com/aws/amazon-ssm-agent/agent/plugins/pluginutil"
	"github.com/aws/amazon-ssm-agent/agent/task"
)

// Plugin is the type for the Cloudwatch plugin.
type Plugin struct {
	Context                            context.T
	CommandExecuter                    executers.T
	Process                            *os.Process
	WorkingDir                         string
	ExeLocation                        string
	Name                               string
	DefaultHealthCheckOrchestrationDir string
}

const (
	//TODO: Change the way the output is being returned to return exit codes
	IsProcessRunning = "$ProcessActive = Get-Process -Name %v -ErrorAction SilentlyContinue ; $ProcessActive -ne $null"
	GetPidOfExe      = "Get-Process -Name %v -ErrorAction SilentlyContinue | Select ProcessName, Id | ConvertTo-Json"
	ProcessNotFound  = "Process not found"
	// CloudWatchProcessName represents CloudWatch Exe Absolute Path
	CloudWatchProcessName = "AWS.CloudWatch"
	// CloudWatchExeName represents the name of the executable file of cloud watch
	CloudWatchExeName = "AWS.CloudWatch.exe"
	// CloudWatchFolderName represents the default folder name for cloud watch plugin
	CloudWatchFolderName = "awsCloudWatch"
)

// CloudwatchProcessInfo is a structure for info returned by Cloudwatch process
type CloudwatchProcessInfo struct {
	ProcessName string `json:"ProcessName"`
	PId         int    `json:"Id"`
}

// Assign method to global variables to allow unittest to override
// TODO change these to deps.go later
var exec = executers.ShellCommandExecuter{}
var findProcess = os.FindProcess
var killProcess = func(process *os.Process) error {
	return process.Kill()
}

// var createScript = pluginutil.CreateScriptFile

// todo: honor cancel flag for Start
// todo: honor cancel flag for Stop
// todo: Start,Stop -> should return plugin.result or error as well -> so that caller can report the results/errors accordingly.
// NewPlugin returns a new instance of Cloudwatch plugin
func NewPlugin(context context.T, pluginConfig iohandler.PluginConfig) (*Plugin, error) {

	//Note: This is a wrapper on top of cloudwatch.exe - basically this executes the exe in a separate process.

	var plugin Plugin
	plugin.Context = context
	plugin.WorkingDir = fileutil.BuildPath(appconfig.DefaultPluginPath, CloudWatchFolderName)
	plugin.ExeLocation = filepath.Join(plugin.WorkingDir, CloudWatchExeName)

	plugin.Name = Name()

	//health check specific stuff will be done here
	instanceId, _ := context.Identity().ShortInstanceID()
	plugin.DefaultHealthCheckOrchestrationDir = fileutil.BuildPath(appconfig.DefaultDataStorePath,
		instanceId,
		appconfig.LongRunningPluginsLocation,
		appconfig.LongRunningPluginsHealthCheck,
		plugin.Name)
	_ = fileutil.MakeDirsWithExecuteAccess(plugin.DefaultHealthCheckOrchestrationDir)
	plugin.CommandExecuter = exec

	return &plugin, nil
}

// Name returns the plugin name
func Name() string {
	return appconfig.PluginNameCloudWatch
}

// IsRunning returns if the said plugin is running or not
func (p *Plugin) IsRunning() bool {
	//working directory here doesn't really matter much since we run a powershell script to determine if exe is running
	return p.IsCloudWatchExeRunning(p.DefaultHealthCheckOrchestrationDir, p.DefaultHealthCheckOrchestrationDir, task.NewChanneledCancelFlag())
}

// Start starts the executable file and returns encountered errors
func (p *Plugin) Start(configuration string, orchestrationDir string, cancelFlag task.CancelFlag, out iohandler.IOHandler) (err error) {
	log := p.Context.Log()
	logFormatConfig := logger.PrintCWConfig(configuration, log)
	log.Infof("CloudWatch Configuration to be applied - %s ", logFormatConfig)

	//check if the exe is located
	if !FileExist(p.ExeLocation) {
		errorMessage := "unable to locate cloudwatch.exe"
		log.Errorf(errorMessage)
		return errors.New(errorMessage)
	}

	//if no orchestration directory specified, create temp directory
	var useTempDirectory = (orchestrationDir == "")
	var tempDir string

	//var err error
	if useTempDirectory {
		if tempDir, err = ioutil.TempDir("", "Ec2RunCommand"); err != nil {
			log.Error(err)
			return
		}
		orchestrationDir = tempDir
	}

	//workingDirectory -> is the location where the exe runs from -> for cloudwatch this is where all configurations are present
	orchestrationDir = fileutil.BuildPath(orchestrationDir, p.Name)
	log.Debugf("Cloudwatch specific commands will be run in workingDirectory %v; orchestrationDir %v ", p.WorkingDir, orchestrationDir)
	// create orchestration dir if needed
	if !FileExist(orchestrationDir) {
		if err = fileutil.MakeDirsWithExecuteAccess(orchestrationDir); err != nil {
			log.Errorf("Encountered error while creating orchestrationDir directory %s:%s", orchestrationDir, err.Error())
			return
		}
	}

	//check if cloudwatch.exe is already running or not
	if p.IsCloudWatchExeRunning(p.DefaultHealthCheckOrchestrationDir, p.DefaultHealthCheckOrchestrationDir, cancelFlag) {
		log.Debug("Cloudwatch executable is already running. Starting to terminate the process")
		p.Stop(cancelFlag)
	}

	/*
		In general exec.Execute -> waits for the command to finish with added attribute to timeout and cancel the command
		We don't want that for Cloudwatch.exe -> because we simply launch the exe and forget about it, hence we are using
		exec.StartExe that just launches an exe.

		Also, for aws:runPowerShellScript, aws:psModule & aws:applications plugins -> we create a powershellscript which
		has all commands expressed as []string and then we execute that script. For cloudwatch we directly invoke the exe,
		 and that's why we don't have to create any powershellscript.
	*/

	//construct command name and arguments that will be run by executer
	commandName := p.ExeLocation
	var commandArguments []string
	var instanceId, instanceRegion string
	if instanceId, err = p.Context.Identity().InstanceID(); err != nil {
		log.Error("Cannot get the current instance ID")
		return
	}

	if instanceRegion, err = p.Context.Identity().Region(); err != nil {
		log.Error("Cannot get the current instance region information")
		return
	}

	commandArguments = append(commandArguments, instanceId, instanceRegion, getFileName())

	value, _, err := pluginutil.LocalRegistryKeyGetStringsValue(appconfig.ItemPropertyPath, appconfig.ItemPropertyName)
	if err != nil {
		log.Debug("Cannot find customized proxy setting.")
	}
	// if user has customized proxy setting
	if (err == nil) && (len(value) != 0) {
		url, noProxy := pluginutil.GetProxySetting(value)
		if (len(url) != 0) && (len(noProxy) != 0) {
			commandArguments = append(commandArguments, url, noProxy)
		} else if len(url) != 0 {
			commandArguments = append(commandArguments, url)
		}
	}

	log.Debugf("commandName: %s", commandName)
	log.Debugf("arguments passed: %s", commandArguments)

	//start the new process
	stdoutFilePath := filepath.Join(orchestrationDir, "stdout")
	stderrFilePath := filepath.Join(orchestrationDir, "stderr")

	//remove previous output log files if they are present
	fileutil.DeleteFile(stdoutFilePath)
	fileutil.DeleteFile(stderrFilePath)

	process, exitCode, err := p.CommandExecuter.StartExe(p.Context, p.WorkingDir, out.GetStdoutWriter(), out.GetStderrWriter(), cancelFlag, commandName, commandArguments)
	if err != nil || exitCode != 0 {
		return fmt.Errorf("Errors occurred while starting Cloudwatch exit code %v, error %v", exitCode, err)
	}

	// Cloudwatch process details
	p.Process = process
	log.Infof("Process id of cloudwatch.exe -> %v", p.Process.Pid)

	return nil
}

// Stop returns true if it successfully killed the cloudwatch exe or else it returns false
func (p *Plugin) Stop(cancelFlag task.CancelFlag) (err error) {
	log := p.Context.Log()

	var cwProcInfo []CloudwatchProcessInfo
	if cwProcInfo, err = p.GetProcInfoOfCloudWatchExe(
		p.DefaultHealthCheckOrchestrationDir,
		p.DefaultHealthCheckOrchestrationDir,
		task.NewChanneledCancelFlag()); err != nil {
		log.Errorf("Can't stop cloudwatch because unable to find Pid of cloudwatch.exe : %v", err)
		return err
	}

	log.Info("The number of cloudwatch processes running are ", len(cwProcInfo))
	var processKillError error
	var currentProcess *os.Process
	processKillError = nil
	//Iterating through the cwProcess info to in case multiple Cloudwatch processes are running.
	//All existing processes must be killed
	for _, cloudwatchInfo := range cwProcInfo {
		//Assigning existing cloudwatch process Id to currentProcess in order to kill that process.
		log.Debug("PID of Cloudwatch is ", cloudwatchInfo.PId)

		if currentProcess, err = findProcess(cloudwatchInfo.PId); err != nil {
			err = fmt.Errorf("failed to find process CloudWatch process with pid %v. Err: %w", cloudwatchInfo.PId, err)
			log.Error(err)
			processKillError = err
			continue
		}

		if err = killProcess(currentProcess); err != nil {
			// Continuing here without returning to kill whatever processes can be killed even if something
			// goes wrong. Return on error later
			log.Errorf("Encountered error while trying to kill the process %v : %v", currentProcess.Pid, err)
			processKillError = err
		} else {
			log.Infof("Successfully killed the process %v", currentProcess.Pid)
		}
	}
	if p.IsRunning() || processKillError != nil {
		log.Errorf("There was an error while killing Cloudwatch: %v", processKillError)
		return processKillError
	} else {
		log.Infof("All existing Cloudwatch processes killed successfully.")
	}
	return nil
}

// IsCloudWatchExeRunning runs a powershell script to determine if the given process is running
func (p *Plugin) IsCloudWatchExeRunning(workingDirectory, orchestrationDir string, cancelFlag task.CancelFlag) bool {
	/*
		Since most functions in "os" package in GoLang isn't implemented for Windows platform, we run a powershell
		script (using Get-Process) to get process details in Windows.
	*/
	log := p.Context.Log()
	//constructing the powershell command to execute
	var commandArguments []string
	var err error
	cloudwatchProcessName := CloudWatchProcessName
	cmdIsExeRunning := fmt.Sprintf(IsProcessRunning, cloudwatchProcessName)
	log.Debugf("Final cmd to check if process is still running is", cmdIsExeRunning)
	commandArguments = append(commandArguments, cmdIsExeRunning)

	// execute the command
	var commandOutput string
	if commandOutput, err = p.runPowerShell(workingDirectory, cancelFlag, commandArguments); err != nil {
		//TODO Returning false here because we are unsure if Cloudwatch is running. Trying to kill PID will lead to error. Handle this situation
		return false
	}

	log.Debugf("The output of IsCloudwatchExeRunning is %s", commandOutput)
	//Get-Process returned the Pid -> means it was not null
	if strings.Contains(commandOutput, "True") {
		log.Infof("Process %s is running", cloudwatchProcessName)
		return true
	} else if !strings.Contains(commandOutput, "False") {
		log.Infof("Multiple processes of %s running. Command output is ", cloudwatchProcessName, commandOutput)
		return true
	}

	log.Infof("Process %s is not running", cloudwatchProcessName)
	return false
}

// GetProcInfoOfCloudWatchExe runs a powershell script to determine the process ID of the Cloudwatch process. It should be called only after confirming that cloudwatch is running
func (p *Plugin) GetProcInfoOfCloudWatchExe(orchestrationDir, workingDirectory string, cancelFlag task.CancelFlag) (cwProcInfo []CloudwatchProcessInfo, err error) {
	log := p.Context.Log()
	//constructing the powershell command to execute
	var commandArguments []string
	cmdGetPidOfCW := fmt.Sprintf(GetPidOfExe, CloudWatchProcessName)
	log.Debugf("Command to get the PID info is ", cmdGetPidOfCW)
	commandArguments = append(commandArguments, cmdGetPidOfCW)

	// execute the command
	var commandOutput string
	if commandOutput, err = p.runPowerShell(workingDirectory, cancelFlag, commandArguments); err != nil {
		return cwProcInfo, err
	}

	//Since output is returned as a Json, checking to see if output is not in the form of an array
	//Output will be in the form of an array only in case of multiple Cloudwatch instances running
	if !strings.HasPrefix(commandOutput, "[") && !strings.HasSuffix(commandOutput, "]") {
		commandOutput = "[" + commandOutput + "]"
	}

	//Unmarshal the result into json obj.
	if err = jsonutil.Unmarshal(commandOutput, &cwProcInfo); err != nil {
		log.Errorf("Error unmarshalling Cloudwatch process information is %v", err)
		return cwProcInfo, err
	}

	return cwProcInfo, err
}

// runPowerShell is a wrapper around Execute command to run powershell script
func (p *Plugin) runPowerShell(workingDirectory string, cancelFlag task.CancelFlag, commandArguments []string) (commandOutput string, err error) {
	log := p.Context.Log()
	commandName := pluginutil.GetShellCommand()
	log.Infof("commandName: %s", commandName)
	log.Infof("arguments passed: %s", commandArguments)

	//If the stdoutFile and stderrFile path is empty, p.CommandExecuter.Execute return the output as a buffer
	stdoutFilePath := ""
	stderrFilePath := ""
	//executionTimeout -> determining if a process is running or not shouldn't take more than 60 seconds
	executionTimeout := pluginutil.ValidateExecutionTimeout(log, 60)

	//execute the command
	stdout, stderr, exitCode, errs := p.CommandExecuter.Execute(p.Context, workingDirectory, stdoutFilePath,
		stderrFilePath, cancelFlag, executionTimeout, commandName, commandArguments, make(map[string]string))

	stdOutBuf := new(bytes.Buffer)
	stdOutBuf.ReadFrom(stdout)
	commandOutput = stdOutBuf.String()
	stdErrBuf := new(bytes.Buffer)
	stdErrBuf.ReadFrom(stderr)
	commandOutputError := stdErrBuf.String()

	//We don't expect any errors because the powershell script that we run has error action set as SilentlyContinue
	if commandOutputError != "" {
		log.Errorf("Powershell script to get process ID of the Cloudwatch executable currently running failed with error - %v", commandOutputError)
	}

	log.Debugf("exitCode - %v", exitCode)
	log.Debugf("errs - %v", errs)

	return commandOutput, nil
}
