// 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 runpluginutil run plugin utility functions without referencing the actually plugin impl packages
package runpluginutil

import (
	"fmt"
	"runtime/debug"
	"strconv"
	"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/framework/processor/executer/iohandler"
	"github.com/aws/amazon-ssm-agent/agent/jsonutil"
	"github.com/aws/amazon-ssm-agent/agent/log"
	"github.com/aws/amazon-ssm-agent/agent/platform"
	"github.com/aws/amazon-ssm-agent/agent/plugins/pluginutil"
	"github.com/aws/amazon-ssm-agent/agent/ssm/ssmparameterresolver"
	"github.com/aws/amazon-ssm-agent/agent/task"
)

const (
	executeStep string = "execute"
	skipStep    string = "skip"
	failStep    string = "fail"
)

// TODO: rename to RCPlugin, this represents RCPlugin interface.
type T interface {
	Execute(config contracts.Configuration, cancelFlag task.CancelFlag, output iohandler.IOHandler)
}

type PluginFactory interface {
	Create(context context.T) (T, error)
}

// PluginRegistry stores a set of plugins (both worker and long running plugins), indexed by ID.
type PluginRegistry map[string]PluginFactory

var (
	SSMPluginRegistry PluginRegistry

	deleteDirectoryRef = fileutil.DeleteDirectory
)

// allPlugins is the list of all known plugins.
// This allows us to differentiate between the case where a document asks for a plugin that exists but isn't supported on this platform
// and the case where a plugin name isn't known at all to this version of the agent (and the user should probably upgrade their agent)
var allPlugins = map[string]struct{}{
	appconfig.PluginNameAwsAgentUpdate:         {},
	appconfig.PluginNameAwsApplications:        {},
	appconfig.PluginNameAwsConfigureDaemon:     {},
	appconfig.PluginNameAwsConfigurePackage:    {},
	appconfig.PluginNameAwsPowerShellModule:    {},
	appconfig.PluginNameAwsRunPowerShellScript: {},
	appconfig.PluginNameAwsRunShellScript:      {},
	appconfig.PluginNameAwsSoftwareInventory:   {},
	appconfig.PluginNameCloudWatch:             {},
	appconfig.PluginNameConfigureDocker:        {},
	appconfig.PluginNameDockerContainer:        {},
	appconfig.PluginNameDomainJoin:             {},
	appconfig.PluginEC2ConfigUpdate:            {},
	appconfig.PluginNameRefreshAssociation:     {},
	appconfig.PluginDownloadContent:            {},
	appconfig.PluginRunDocument:                {},
}

// allSessionPlugins is the list of all known session plugins.
var allSessionPlugins = map[string]struct{}{
	appconfig.PluginNameStandardStream:         {},
	appconfig.PluginNameInteractiveCommands:    {},
	appconfig.PluginNamePort:                   {},
	appconfig.PluginNameNonInteractiveCommands: {},
}

// Assign method to global variables to allow unittest to override
var isSupportedPlugin = IsPluginSupportedForCurrentPlatform

// TODO remove executionID and creation date
// RunPlugins executes a set of plugins. The plugin configurations are given in a map with pluginId as key.
// Outputs the results of running the plugins, indexed by pluginId.
// Make this function private in case everybody tries to reference it everywhere, this is a private member of Executer
func RunPlugins(
	context context.T,
	plugins []contracts.PluginState,
	ioConfig contracts.IOConfiguration,
	upstreamServiceName contracts.UpstreamServiceName,
	registry PluginRegistry,
	resChan chan contracts.PluginResult,
	cancelFlag task.CancelFlag,
) (pluginOutputs map[string]*contracts.PluginResult) {

	pluginOutputs = make(map[string]*contracts.PluginResult)

	//Contains the logStreamPrefix without the pluginID
	logStreamPrefix := ioConfig.CloudWatchConfig.LogStreamPrefix
	log := context.Log()

	defer func() {
		if r := recover(); r != nil {
			log.Errorf("Run plugins panic: \n%v", r)
			log.Errorf("Stacktrace:\n%s", debug.Stack())
		}
	}()

	for pluginIndex, pluginState := range plugins {
		pluginID := pluginState.Id     // the identifier of the plugin
		pluginName := pluginState.Name // the name of the plugin
		pluginOutput := pluginState.Result
		pluginOutput.PluginID = pluginID
		pluginOutput.PluginName = pluginName
		pluginOutputs[pluginID] = &pluginOutput
		log.Debugf("Checking Status for plugin %s - %s", pluginName, pluginOutput.Status)
		switch pluginOutput.Status {
		//TODO properly initialize the plugin status
		case "":
			log.Debugf("plugin - %v has empty state, initialize as NotStarted",
				pluginName)
			pluginOutput.StartDateTime = time.Now()
			pluginOutput.Status = contracts.ResultStatusNotStarted

		case contracts.ResultStatusNotStarted, contracts.ResultStatusInProgress:
			log.Debugf("plugin - %v status %v",
				pluginName,
				pluginOutput.Status)
			pluginOutput.StartDateTime = time.Now()

		case contracts.ResultStatusSuccessAndReboot:
			log.Debugf("plugin - %v just experienced reboot, reset to InProgress...",
				pluginName)
			pluginOutput.Status = contracts.ResultStatusInProgress
		case contracts.ResultStatusFailed:
			log.Debugf("plugin - %v already executed with failed status, skipping...",
				pluginName)
			resChan <- *pluginOutputs[pluginID]
			continue
		default:
			log.Debugf("plugin - %v already executed, skipping...",
				pluginName)
			continue
		}

		log.Debugf("Executing plugin - %v", pluginName)

		// populate plugin start time, status, and upstream service name
		configuration := pluginState.Configuration
		configuration.UpstreamServiceName = upstreamServiceName

		if ioConfig.OutputS3BucketName != "" {
			pluginOutputs[pluginID].OutputS3BucketName = ioConfig.OutputS3BucketName
			if ioConfig.OutputS3KeyPrefix != "" {
				pluginOutputs[pluginID].OutputS3KeyPrefix = fileutil.BuildS3Path(ioConfig.OutputS3KeyPrefix, pluginName)

			}
		}
		//Append pluginID to logStreamPrefix. Replace ':' or '*' with '-' since LogStreamNames cannot have those characters
		if ioConfig.CloudWatchConfig.LogGroupName != "" {
			ioConfig.CloudWatchConfig.LogStreamPrefix = fmt.Sprintf("%s/%s", logStreamPrefix, pluginID)
			ioConfig.CloudWatchConfig.LogStreamPrefix = strings.Replace(ioConfig.CloudWatchConfig.LogStreamPrefix, ":", "-", -1)
			ioConfig.CloudWatchConfig.LogStreamPrefix = strings.Replace(ioConfig.CloudWatchConfig.LogStreamPrefix, "*", "-", -1)
		}

		var (
			r                  contracts.PluginResult
			pluginFactory      PluginFactory
			pluginHandlerFound bool
			isKnown            bool
			isSupported        bool
		)

		pluginFactory, pluginHandlerFound = registry[pluginName]
		isKnown, isSupported, _ = isSupportedPlugin(log, pluginName)
		// checking if a prior step returned exit codes 168 or 169 to exit document.
		// If so we need to skip every other step
		shouldSkipStepDueToPriorFailedStep := getShouldPluginSkipBasedOnControlFlow(
			context,
			plugins,
			pluginIndex,
			pluginOutputs,
		)

		operation, logMessage := getStepExecutionOperation(
			log,
			pluginName,
			pluginID,
			isKnown,
			isSupported,
			pluginHandlerFound,
			configuration.IsPreconditionEnabled,
			configuration.Preconditions,
			shouldSkipStepDueToPriorFailedStep)

		switch operation {
		case executeStep:
			log.Infof("Running plugin %s %s", pluginName, pluginID)
			r = runPlugin(context, pluginFactory, pluginName, configuration, cancelFlag, ioConfig)
			pluginOutputs[pluginID].Code = r.Code
			pluginOutputs[pluginID].Status = r.Status
			pluginOutputs[pluginID].Error = r.Error
			pluginOutputs[pluginID].StandardError = r.StandardError
			pluginOutputs[pluginID].StandardOutput = r.StandardOutput
			pluginOutputs[pluginID].Output = r.Output
			pluginOutputs[pluginID].StepName = r.StepName

			onFailureProp := getStringPropByName(pluginState.Configuration.Properties, contracts.OnFailureModifier)
			hasOnFailureProp := onFailureProp == contracts.ModifierValueExit || onFailureProp == contracts.ModifierValueSuccessAndExit
			outputAddition := ""
			if pluginOutputs[pluginID].Code == contracts.ExitWithSuccess {
				outputAddition = "\nStep exited with code 168. Therefore, marking step as succeeded. Further document steps will be skipped."
				pluginOutputs[pluginID].Status = contracts.ResultStatusSuccess
				pluginOutputs[pluginID].Error = ""
				pluginOutputs[pluginID].StandardError = ""
				pluginOutputs[pluginID].StandardOutput = r.StandardOutput + outputAddition
			} else if pluginOutputs[pluginID].Code == contracts.ExitWithFailure {
				outputAddition = "\nStep exited with code 169. Therefore, marking step as Failed. Further document steps will be skipped."
				pluginOutputs[pluginID].StandardError = r.StandardError + outputAddition
				pluginOutputs[pluginID].StandardOutput = r.StandardOutput + outputAddition
			} else if pluginOutputs[pluginID].Status == contracts.ResultStatusFailed && hasOnFailureProp {
				outputAddition = "\nStep was found to have onFailure property. Further document steps will be skipped."
				pluginOutputs[pluginID].StandardError = r.StandardError + outputAddition
				pluginOutputs[pluginID].StandardOutput = r.StandardOutput + outputAddition
				if onFailureProp == contracts.ModifierValueSuccessAndExit {
					pluginOutputs[pluginID].Status = contracts.ResultStatusSuccess
					pluginOutputs[pluginID].Code = contracts.ExitWithSuccess
				}
			}

		case skipStep:
			log.Info(logMessage)
			pluginOutputs[pluginID].Status = contracts.ResultStatusSkipped
			pluginOutputs[pluginID].Code = 0
			pluginOutputs[pluginID].Output = logMessage
		case failStep:
			err := fmt.Errorf(logMessage)
			pluginOutputs[pluginID].Status = contracts.ResultStatusFailed
			pluginOutputs[pluginID].Error = err.Error()
			log.Error(err)
		default:
			err := fmt.Errorf("Unknown error, Operation: %s, Plugin name: %s", operation, pluginName)
			pluginOutputs[pluginID].Status = contracts.ResultStatusFailed
			pluginOutputs[pluginID].Error = err.Error()
			log.Error(err)
		}

		// set end time.
		pluginOutputs[pluginID].EndDateTime = time.Now()
		log.Infof("Sending plugin %v completion message", pluginID)

		// truncate the result and send it back to buffer channel.
		result := *pluginOutputs[pluginID]
		pluginConfig := iohandler.DefaultOutputConfig()
		result.StandardOutput = pluginutil.StringPrefix(result.StandardOutput, pluginConfig.MaxStdoutLength, pluginConfig.OutputTruncatedSuffix)
		result.StandardError = pluginutil.StringPrefix(result.StandardError, pluginConfig.MaxStdoutLength, pluginConfig.OutputTruncatedSuffix)
		// send to buffer channel, guaranteed to not block since buffer size is plugin number
		resChan <- result

		//TODO handle cancelFlag here
		if pluginHandlerFound && r.Status == contracts.ResultStatusSuccessAndReboot {
			// do not execute the the next plugin
			break
		}
	}
	// this will clean the orchestration folder for the successful and failed document executions only when the agent is configured
	orchestrationDirCleanup(context, len(plugins), pluginOutputs, ioConfig.OrchestrationDirectory)
	return
}

// orchestrationDirCleanup will clean orchestration folder for the successful and failed document executions. Cleaned only when the agent is configured to do so
func orchestrationDirCleanup(context context.T, pluginsCount int, pluginOutputs map[string]*contracts.PluginResult, orchestrationDir string) {
	log := context.Log()
	if orchestrationDir == "" {
		log.Info("orchestration directory is empty")
		return
	}

	if pluginsCount == len(pluginOutputs) {
		// this will clean the orchestration folder for the successful and failed document executions only when the agent is configured
		orchestrationDirectoryCleanupConfig := context.AppConfig().Ssm.OrchestrationDirectoryCleanup
		documentResult, _, _, _ := contracts.DocumentResultAggregator(log, "", pluginOutputs)
		statusWithCleanupConfig := map[contracts.ResultStatus]map[string]interface{}{
			contracts.ResultStatusSuccess: {appconfig.OrchestrationDirCleanupForSuccessCommand: nil, appconfig.OrchestrationDirCleanupForSuccessFailedCommand: nil},
			contracts.ResultStatusFailed:  {appconfig.OrchestrationDirCleanupForSuccessFailedCommand: nil},
		}
		if _, ok := statusWithCleanupConfig[documentResult][orchestrationDirectoryCleanupConfig]; ok {
			log.Infof("orchestration cleanup started for the command with status %v - deleting orchestration directory: %v", documentResult, orchestrationDir)
			if err := deleteDirectoryRef(orchestrationDir); err != nil {
				log.Warnf("error deleting the directory %v", orchestrationDir)
			}
		}
	}
}

var runPlugin = func(
	context context.T,
	factory PluginFactory,
	pluginName string,
	config contracts.Configuration,
	cancelFlag task.CancelFlag,
	ioConfig contracts.IOConfiguration) (res contracts.PluginResult) {
	// create a new context that includes plugin ID
	context = context.With("[pluginName=" + pluginName + "]")

	log := context.Log()
	var stepName string

	defer func() {
		// recover in case the plugin panics
		// this should handle some kind of seg fault errors.
		if err := recover(); err != nil {
			res.Status = contracts.ResultStatusFailed
			res.Code = 1
			res.Error = fmt.Errorf("Plugin crashed with message %v!", err).Error()
			log.Error(res.Error)
			log.Errorf("Stacktrace:\n%s", debug.Stack())
		}
	}()

	var err error
	plugin, err := factory.Create(context)

	if err != nil {
		res.Status = contracts.ResultStatusFailed
		res.Code = 1
		res.Error = fmt.Errorf("failed to create plugin %v", err).Error()
		log.Error(res.Error)
		return
	}

	res.StartDateTime = time.Now()
	defer func() { res.EndDateTime = time.Now() }()

	output := iohandler.NewDefaultIOHandler(context, ioConfig)
	//check if properties is a list. If true, then unroll
	switch config.Properties.(type) {
	case []interface{}:
		// Load each property as a list.
		var properties []interface{}
		if properties = pluginutil.LoadParametersAsList(log, config.Properties, &res); res.Code != 0 {
			return
		}
		for _, prop := range properties {
			config.Properties = prop
			propOutput := iohandler.NewDefaultIOHandler(context, ioConfig)
			stepName, err = getStepName(pluginName, config)
			if err != nil {
				errorString := fmt.Errorf("Invalid format in plugin properties %v;\nerror %v", config.Properties, err)
				output.MarkAsFailed(errorString)
			} else {
				executePlugin(plugin, pluginName, stepName, config, cancelFlag, propOutput)
			}

			output.Merge(propOutput)
		}

	default:
		stepName, err = getStepName(pluginName, config)
		if err != nil {
			errorString := fmt.Errorf("Invalid format in plugin properties %v;\nerror %v", config.Properties, err)
			output.MarkAsFailed(errorString)
		} else {
			executePlugin(plugin, pluginName, stepName, config, cancelFlag, output)
		}
	}

	if ioConfig.OutputS3BucketName != "" {
		if stepName != "" {
			// Colons are removed from s3 url's before uploading. Removing from here so worker can generate same url.
			stepName = strings.Replace(stepName, ":", "", -1)
		}

		res.StepName = stepName
	}
	res.Code = output.GetExitCode()
	res.Status = output.GetStatus()
	res.Output = output.GetOutput()
	res.StandardOutput = output.GetStdout()
	res.StandardError = output.GetStderr()

	return
}

// executePlugin executes the plugin that's passed in and initializes the necessary writers
func executePlugin(
	plugin T,
	pluginName string,
	stepName string,
	config contracts.Configuration,
	cancelFlag task.CancelFlag,
	output iohandler.IOHandler) {

	// Create the output object and execute the plugin
	defer output.Close()
	output.Init(pluginName, stepName)
	plugin.Execute(config, cancelFlag, output)
}

// GetPropertyName returns the ID field of property in a v1.2 SSM Document
func GetPropertyName(rawPluginInput interface{}) (propertyName string, err error) {
	pluginInput := struct{ ID string }{}
	err = jsonutil.Remarshal(rawPluginInput, &pluginInput)
	propertyName = pluginInput.ID
	return
}

// Checks plugin compatibility and step precondition and returns if it should be executed, skipped or failed
func getStepExecutionOperation(
	log log.T,
	pluginName string,
	pluginId string,
	isKnown bool,
	isSupported bool,
	isPluginHandlerFound bool,
	isPreconditionEnabled bool,
	preconditions map[string][]contracts.PreconditionArgument,
	shouldSkipStepDueToPriorFailedStep bool,
) (string, string) {
	log.Debugf("isSupported flag = %t", isSupported)
	log.Debugf("isPluginHandlerFound flag = %t", isPluginHandlerFound)
	log.Debugf("isPreconditionEnabled flag = %t", isPreconditionEnabled)

	if shouldSkipStepDueToPriorFailedStep {
		return skipStep, fmt.Sprintf(
			"Plugin with name %s and id %s skipped due to prior step with an exit condition",
			pluginName,
			pluginId)
	}

	if !isPreconditionEnabled {
		// 1.x or 2.0 document
		if !isKnown {
			return failStep, fmt.Sprintf(
				"Plugin with name %s is not supported by this version of ssm agent, please update to latest version. Step name: %s",
				pluginName,
				pluginId)
		} else if !isSupported {
			return failStep, fmt.Sprintf(
				"Plugin with name %s is not supported in current platform. Step name: %s",
				pluginName,
				pluginId)
		} else if len(preconditions) > 0 {
			// if 1.x or 2.0 document contains precondition or plugin not found, failStep
			return failStep, fmt.Sprintf(
				"Precondition is not supported for document schema version prior to 2.2. Step name: %s",
				pluginId)
		} else if !isPluginHandlerFound {
			return failStep, fmt.Sprintf(
				"Plugin with name %s not found. Step name: %s",
				pluginName,
				pluginId)
		} else {
			return executeStep, ""
		}
	} else {
		// 2.2 or higher (cross-platform) document
		if len(preconditions) == 0 {
			log.Debug("Cross-platform Precondition is not present")

			// precondition is not present - if pluginFound executeStep, else skipStep
			if !isKnown {
				return failStep, fmt.Sprintf(
					"Plugin with name %s is not supported by this version of ssm agent, please update to latest version. Step name: %s",
					pluginName,
					pluginId)
			} else if isSupported && isPluginHandlerFound {
				return executeStep, ""
			} else {
				return skipStep, fmt.Sprintf(
					"Step execution skipped due to unsupported plugin: %s. Step name: %s",
					pluginName,
					pluginId)
			}
		} else {
			log.Debugf("Cross-platform Precondition is present, precondition = %v", preconditions)

			isAllowed, unrecognizedPreconditionList := evaluatePreconditions(log, preconditions)

			if isAllowed && !isKnown {
				return failStep, fmt.Sprintf(
					"Plugin with name %s is not supported by this version of ssm agent, please update to latest version. Step name: %s",
					pluginName,
					pluginId)
			} else if !isSupported || !isPluginHandlerFound {
				return skipStep, fmt.Sprintf(
					"Step execution skipped due to unsupported plugin: %s. Step name: %s",
					pluginName,
					pluginId)
			} else if !isAllowed {
				return skipStep, fmt.Sprintf(
					"Step execution skipped due to unsatisfied preconditions: '%s'. Step name: %s",
					strings.Join(unrecognizedPreconditionList, ", "),
					pluginId)
			} else if len(unrecognizedPreconditionList) > 0 {
				return failStep, fmt.Sprintf(
					"Unrecognized precondition(s): '%s', please update agent to latest version. Step name: %s",
					strings.Join(unrecognizedPreconditionList, ", "),
					pluginId)
			} else {
				return executeStep, ""
			}
		}
	}
}

// Evaluate precondition and return precondition result and unrecognized preconditions (if any)
func evaluatePreconditions(
	log log.T,
	preconditions map[string][]contracts.PreconditionArgument,
) (bool, []string) {

	var isAllowed = true
	var unrecognizedPreconditionList []string

	// For current release, we only support "StringEquals" operator and "platformType"
	// operand, so explicitly checking for those and number of operands must be 2
	for key, value := range preconditions {
		switch key {
		case "StringEquals":
			if len(value) != 2 {
				unrecognizedPreconditionList = append(unrecognizedPreconditionList, fmt.Sprintf("\"%s\": operator accepts exactly 2 arguments", key))
			} else {
				if strings.Compare(value[0].InitialArgumentValue, value[1].InitialArgumentValue) == 0 {
					// StringEquals preconditions with identical arguments are not allowed
					if strings.Compare(value[0].InitialArgumentValue, "platformType") == 0 {
						unrecognizedPreconditionList = append(unrecognizedPreconditionList, fmt.Sprintf("\"%s\": [%v %v]", key, value[0].InitialArgumentValue, value[1].InitialArgumentValue))
					} else {
						// hide customer's parameters and constants
						unrecognizedPreconditionList = append(unrecognizedPreconditionList, fmt.Sprintf("\"%s\": operator's arguments can't be identical", key))
					}
				} else if ssmparameterresolver.TextContainsSsmParameters(value[0].InitialArgumentValue) || ssmparameterresolver.TextContainsSsmParameters(value[1].InitialArgumentValue) {
					unrecognizedPreconditionList = append(unrecognizedPreconditionList, fmt.Sprintf("\"%s\": operator's arguments can't contain SSM parameters", key))
				} else if ssmparameterresolver.TextContainsSecureSsmParameters(value[0].InitialArgumentValue) || ssmparameterresolver.TextContainsSecureSsmParameters(value[1].InitialArgumentValue) {
					unrecognizedPreconditionList = append(unrecognizedPreconditionList, fmt.Sprintf("\"%s\": operator's arguments can't contain secure SSM parameters", key))
				} else if strings.Compare(value[0].InitialArgumentValue, "platformType") == 0 || strings.Compare(value[1].InitialArgumentValue, "platformType") == 0 {
					// keep original logic for platformType variable
					// Platform type of OS on the instance
					instancePlatformType, _ := platform.PlatformType(log)
					log.Debugf("OS platform type of this instance = %s", instancePlatformType)

					// Variable and value can be in any order, i.e. both "StringEquals": ["platformType", "Windows"]
					// and "StringEquals": ["Windows", "platformType"] are valid
					var initialPlatformTypeValue string
					var resolvedPlatformTypeValue string
					if strings.Compare(value[0].InitialArgumentValue, "platformType") == 0 {
						initialPlatformTypeValue = value[1].InitialArgumentValue
						resolvedPlatformTypeValue = value[1].ResolvedArgumentValue
					} else {
						initialPlatformTypeValue = value[0].InitialArgumentValue
						resolvedPlatformTypeValue = value[0].ResolvedArgumentValue
					}

					if strings.Compare(strings.ToLower(initialPlatformTypeValue), strings.ToLower(resolvedPlatformTypeValue)) != 0 {
						unrecognizedPreconditionList = append(unrecognizedPreconditionList, fmt.Sprintf("\"%s\": the second argument for the platformType variable can't contain document parameters", key))
					} else if strings.Compare(instancePlatformType, strings.ToLower(initialPlatformTypeValue)) != 0 {
						// if precondition doesn't match for platformType, mark step for skip
						isAllowed = false
						unrecognizedPreconditionList = append(unrecognizedPreconditionList, fmt.Sprintf("\"%s\": [%v, %v]", key, value[0].InitialArgumentValue, value[1].InitialArgumentValue))
					}
				} else if strings.Compare(value[0].InitialArgumentValue, value[0].ResolvedArgumentValue) == 0 && strings.Compare(value[1].InitialArgumentValue, value[1].ResolvedArgumentValue) == 0 {
					unrecognizedPreconditionList = append(unrecognizedPreconditionList, fmt.Sprintf("\"%s\": at least one of operator's arguments must contain a valid document parameter", key))
				} else {
					if strings.Compare(value[0].ResolvedArgumentValue, value[1].ResolvedArgumentValue) != 0 {
						// if arbitrary StringEquals precondition is not satisfied, mark step for skip
						isAllowed = false
						unrecognizedPreconditionList = append(unrecognizedPreconditionList, fmt.Sprintf("\"%s\": [%v, %v]", key, value[0].InitialArgumentValue, value[1].InitialArgumentValue))
					}
				}
			}
		default:
			// mark for unrecognizedPrecondition (which is a form of failure)
			unrecognizedPreconditionList = append(unrecognizedPreconditionList, fmt.Sprintf("unrecognized operator: \"%s\"", key))
		}
	}

	return isAllowed, unrecognizedPreconditionList
}

// Returns the Property's ID field from v1.2 documents or the Name field of a Step in v2.x documents.
// This is required to generate the correct stdout/stderr s3 url
func getStepName(pluginName string, config contracts.Configuration) (stepName string, err error) {
	if config.PluginName == config.PluginID {
		if pluginName == appconfig.PluginNameCloudWatch {
			stepName = appconfig.PluginNameCloudWatch
		} else {
			stepName, err = GetPropertyName(config.Properties) //V10 Schema
		}
	} else {
		stepName = config.PluginID //V20 Schema
	}

	return
}

// Gets a property by name out of the plugin's inputs map, returns it as a string
// Supports bool and string only
func getStringPropByName(pluginProperties interface{}, propName string) string {
	// type cast
	pluginPropsMap, ok := pluginProperties.(map[string]interface{})
	if !ok {
		return ""
	}
	// get value from map
	propValueInterface, okm := pluginPropsMap[propName]
	if !okm {
		return ""
	}
	//type cast to string
	propValueStr, okString := propValueInterface.(string)
	propValueBool, okBool := propValueInterface.(bool)
	if !okString && !okBool {
		return ""
	}
	if !okString {
		return strconv.FormatBool(propValueBool)
	}
	return propValueStr
}

// This function handles deciding whether the current plugin should be skipped due to a prior plugin with onFailure
// or onSuccess modifiers. It also handles the finally modifier.
func getShouldPluginSkipBasedOnControlFlow(
	context context.T,
	plugins []contracts.PluginState,
	pluginIndex int,
	pluginOutputs map[string]*contracts.PluginResult,
) bool {
	log := context.Log()
	pluginState := plugins[pluginIndex]
	finallyProp := getStringPropByName(pluginState.Configuration.Properties, contracts.FinallyStepModifier)
	if finallyProp == contracts.ModifierValueTrue && pluginIndex == len(plugins)-1 {
		log.Infof(
			"Finally step detected for plugin %v",
			pluginState.Id,
		)
		// finally step, do not skip:
		return false
	}
	if finallyProp == contracts.ModifierValueTrue && pluginIndex < len(plugins)-1 {
		log.Infof(
			"FinallyStep detected for plugin %v, which is not the last plugin in list. Ignoring FinallyStep.",
			pluginState.Id,
		)
	}
	for prvPluginStateIdx := 0; prvPluginStateIdx < pluginIndex; prvPluginStateIdx++ {
		prevPluginId := plugins[prvPluginStateIdx].Id
		prvPluginResultCode := pluginOutputs[prevPluginId].Code
		onFailureProp := getStringPropByName(plugins[prvPluginStateIdx].Configuration.Properties, contracts.OnFailureModifier)
		isFailedStep := pluginOutputs[prevPluginId].Status == contracts.ResultStatusFailed
		isFailedAndExitStep := isFailedStep && onFailureProp == contracts.ModifierValueExit
		onSuccessProp := getStringPropByName(plugins[prvPluginStateIdx].Configuration.Properties, contracts.OnSuccessModifier)
		isSuccessStep := pluginOutputs[prevPluginId].Status == contracts.ResultStatusSuccess
		isSuccessAndExitStep := isSuccessStep && onSuccessProp == contracts.ModifierValueExit
		if prvPluginResultCode == contracts.ExitWithSuccess ||
			prvPluginResultCode == contracts.ExitWithFailure ||
			isFailedAndExitStep ||
			isSuccessAndExitStep {
			return true
		}
	}
	return false
}
