// Copyright 2020 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 executor contains general purpose command executing objects.
package executor

import (
	"fmt"
	"os"
	"os/exec"

	"github.com/aws/amazon-ssm-agent/agent/log"
	"github.com/aws/amazon-ssm-agent/core/workerprovider/longrunningprovider/model"
)

// OsProcess represent the process information for the worker, such as pid and binary name
type OsProcess struct {
	Pid        int
	PPid       int
	Executable string
	State      string
}

// IExecutor is the interface type for ProcessExecutor.
type IExecutor interface {
	Start(*model.WorkerConfig) (*model.Process, error)
	Processes() ([]OsProcess, error)
	IsPidRunning(pid int) (bool, error)
	Kill(pid int) error
}

// ProcessExecutor is specially added for testing purposes
type ProcessExecutor struct {
	log log.T
}

// NewProcessExecutor returns process executor
func NewProcessExecutor(log log.T) IExecutor {
	return &ProcessExecutor{
		log: log,
	}
}

// Start starts a list of shell commands in the given working directory.
// Returns process started, an exit code (0 if successfully launch, 1 if error launching process), and a set of errors.
// The errors need not be fatal - the output streams may still have data
// even though some errors are reported. For example, if the command got killed while executing,
// the streams will have whatever data was printed up to the kill point, and the errors will
// indicate that the process got terminated.
func (exc *ProcessExecutor) Start(workerConfig *model.WorkerConfig) (*model.Process, error) {
	exc.log.Debugf("Starting process base on config %+v", workerConfig)
	command := exec.Command(workerConfig.Path, workerConfig.Args...)
	prepareProcess(command)
	// configure environment variables
	prepareEnvironment(command)

	if err := command.Start(); err != nil {
		return &model.Process{
			Pid:    0,
			Status: model.Unknown,
		}, err
	}

	//TODO: support environment variable
	return &model.Process{
		Pid:    command.Process.Pid,
		Status: model.Active,
	}, nil
}

// Processes returns running processes on the instance
func (exc *ProcessExecutor) Processes() ([]OsProcess, error) {
	return getProcess()
}

// IsPidRunning returns true if process with pid is running
func (exc *ProcessExecutor) IsPidRunning(pid int) (bool, error) {
	processes, err := getProcess()

	if err != nil {
		return false, err
	}

	for _, process := range processes {
		if process.Pid == pid {
			if process.State == "Z" {
				return false, nil
			}
			return true, nil
		}
	}

	return false, nil
}

func (exc *ProcessExecutor) Kill(pid int) error {
	osProcess, err := os.FindProcess(pid)
	if err != nil {
		return fmt.Errorf("failed to find process %v, %s", pid, err)
	}

	exc.log.Debugf("Found process %v, terminating", pid)
	if err = osProcess.Kill(); err != nil {
		return fmt.Errorf("failed to kill process %v, %s", pid, err)
	}

	exc.log.Debugf("Attempting to clean up process %v resources", pid)
	if _, err = osProcess.Wait(); err != nil {
		// Debug because agent can only clean up child processes
		exc.log.Debugf("Failed to clean up non-child process %v, %s", pid, err)
	}

	return nil
}

// prepareEnvironment adds ssm agent standard environment variables to the command
func prepareEnvironment(command *exec.Cmd) {
	env := os.Environ()
	command.Env = env

	// Running powershell on linux erquired the HOME env variable to be set and to remove the TERM env variable
	validateEnvironmentVariables(command)
}

// fmtEnvVariable creates the string to append to the current set of environment variables.
func fmtEnvVariable(name string, val string) string {
	return fmt.Sprintf("%s=%s", name, val)
}
