// 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/
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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 configurepackage implements the ConfigurePackage plugin.
package configurepackage

import (
	"encoding/json"
	"errors"
	"fmt"
	"reflect"
	"regexp"
	"strings"

	"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/framework/processor/executer/iohandler"
	"github.com/aws/amazon-ssm-agent/agent/jsonutil"
	"github.com/aws/amazon-ssm-agent/agent/plugins/configurepackage/birdwatcher/birdwatcherservice"
	"github.com/aws/amazon-ssm-agent/agent/plugins/configurepackage/birdwatcher/facade"
	"github.com/aws/amazon-ssm-agent/agent/plugins/configurepackage/installer"
	"github.com/aws/amazon-ssm-agent/agent/plugins/configurepackage/localpackages"
	"github.com/aws/amazon-ssm-agent/agent/plugins/configurepackage/packageservice"
	"github.com/aws/amazon-ssm-agent/agent/plugins/configurepackage/trace"
	"github.com/aws/amazon-ssm-agent/agent/task"
	"github.com/aws/aws-sdk-go/service/ssm"
)

const (
	// InstallAction represents the json command to install package
	InstallAction = "Install"
	// UninstallAction represents the json command to uninstall package
	UninstallAction = "Uninstall"
	// InstallationType specifies whether the
	InstallationTypeLegacy  = "Uninstall and reinstall"
	InstallationTypeInPlace = "In-place update"
)

const resourceNotFoundException = "ResourceNotFoundException"
const birdwatcherVersionPattern = "^[A-Za-z0-9.]+$"
const documentArnPattern = "^arn:[a-z0-9][-.a-z0-9]{0,62}:[a-z0-9][-.a-z0-9]{0,62}:([a-z0-9][-.a-z0-9]{0,62})?:([a-z0-9][-.a-z0-9]{0,62})?:document\\/[a-zA-Z0-9/:.\\-_]{1,128}$"

var getConfig = appconfig.Config

// Plugin is the type for the configurepackage plugin.
type Plugin struct {
	context                context.T
	packageServiceSelector func(context context.T, tracer trace.Tracer, input *ConfigurePackagePluginInput, localrepo localpackages.Repository, appCfg *appconfig.SsmagentConfig, bwfacade facade.BirdwatcherFacade, isDocumentArchive *bool) (packageservice.PackageService, error)
	localRepository        localpackages.Repository
	birdwatcherfacade      facade.BirdwatcherFacade
	isDocumentArchive      bool
}

// ConfigurePackagePluginInput represents one set of commands executed by the ConfigurePackage plugin.
type ConfigurePackagePluginInput struct {
	contracts.PluginInput
	Name             string `json:"name"`
	Version          string `json:"version"`
	Action           string `json:"action"`
	InstallationType string `json:"installationType"`
	// TODO: Replace "additionalArguments" from string type to map[string]inteface{} type once Runcommand supports string maps
	AdditionalArguments string `json:"additionalArguments"`
	Source              string `json:"source"`
	Repository          string `json:"repository"`
}

// NewPlugin returns a new instance of the plugin.
func NewPlugin(context context.T) (*Plugin, error) {
	var plugin Plugin

	plugin.context = context
	plugin.birdwatcherfacade = facade.NewBirdwatcherFacade(context)
	plugin.localRepository = localpackages.NewRepository()
	plugin.packageServiceSelector = selectService
	plugin.isDocumentArchive = false

	return &plugin, nil
}

// prepareConfigurePackage ensures the packages are present with the right version for the scenario requested and returns their installers
func prepareConfigurePackage(
	tracer trace.Tracer,
	config contracts.Configuration,
	repository localpackages.Repository,
	packageService packageservice.PackageService,
	input *ConfigurePackagePluginInput,
	packageArn string,
	version string,
	isSameAsCache bool,
	output contracts.PluginOutputter) (inst installer.Installer, uninst installer.Installer, isUpdateInPlace bool, installState localpackages.InstallState, installedVersion string) {

	prepareTrace := tracer.BeginSection(fmt.Sprintf("prepare %s", input.Action))
	defer prepareTrace.End()

	// Default update type is non-in-place update for backwards compatibility
	isUpdateInPlace = false
	additionalArguments := input.AdditionalArguments

	switch input.Action {
	case InstallAction:
		trace := tracer.BeginSection("determine update type")
		if len(input.InstallationType) > 0 && input.InstallationType == InstallationTypeInPlace {
			isUpdateInPlace = true
		}
		trace.AppendDebugf("update is in place: %v", isUpdateInPlace).End()

		// get version information
		trace = tracer.BeginSection("determine version to install")
		installedVersion, installState = getVersionToInstall(tracer, repository, packageArn)
		trace.AppendDebugf("installed: %v in state %v, to install: %v", installedVersion, installState, version).End()

		// ensure manifest file and package
		var err error
		trace = tracer.BeginSection("ensure package is locally available")
		inst, err = ensurePackage(tracer, repository, packageService, packageArn, version, additionalArguments, isSameAsCache, config)
		if err != nil {
			trace.WithError(err).End()
			output.MarkAsFailed(nil, nil)
			return
		}
		trace.End()

		// Get the "uninstaller" for two reasons
		// 1) If the update type is legacy update (uninstall then install), then uninstall the existing version
		// if the requested version is different from existing version
		// * Check if the version is already installed using the packageArn
		// * If the version exists, but the local manifest is different, reinstall the package
		// * Return success if the package is already installed
		// 2) If the update type is in-place update, then the uninstaller is used in case of rollback
		trace = tracer.BeginSection("ensure old package is locally available")
		if !(installedVersion == "" || installState == localpackages.None) && (installedVersion != version || !isSameAsCache) {
			uninst, err = ensurePackage(tracer, repository, packageService, packageArn, installedVersion, additionalArguments, isSameAsCache, config)
			if err != nil {
				trace.WithError(err)
			}
		}
		trace.End()

	case UninstallAction:
		// get version information
		var err error
		trace := tracer.BeginSection("determine version to uninstall")
		defer trace.End()
		installedVersion, installState = getVersionToUninstall(tracer, repository, packageArn)

		// if the input.Version is not specified, or is "latest", uninstall the installedVersion
		if input.Version == "" || packageservice.IsLatest(input.Version) {
			version = installedVersion
		}

		//return success if the version is already uninstalled
		//return success if the installState is None or Uninstalled
		if (installedVersion == "" || version != installedVersion) ||
			(installState == localpackages.None || installState == localpackages.Uninstalled) {
			trace.AppendInfof("version: %v is not installed", version)
			installState = localpackages.None
			output.MarkAsSucceeded()
			trace.WithExitcode(1)
			return
		}

		trace.AppendDebugf("installed: %v in state: %s", installedVersion, installState).End()

		// ensure manifest file and package
		trace = tracer.BeginSection("ensure package is locally available")
		uninst, err = ensurePackage(tracer, repository, packageService, packageArn, installedVersion, additionalArguments, isSameAsCache, config)
		if err != nil {
			trace.WithError(err)
			output.MarkAsFailed(nil, nil)
			return
		}

	default:
		prepareTrace.AppendErrorf("unsupported action: %v", input.Action)
		output.MarkAsFailed(nil, nil)
		return
	}

	return inst, uninst, isUpdateInPlace, installState, installedVersion
}

// ensurePackage validates local copy of the manifest and package and downloads if needed, returning the installer
func ensurePackage(
	tracer trace.Tracer,
	repository localpackages.Repository,
	packageService packageservice.PackageService,
	packageName string,
	version string,
	additionalArguments string,
	isSameAsCache bool,
	config contracts.Configuration) (installer.Installer, error) {

	pkgTrace := tracer.BeginSection("ensure package is available locally")

	currentState, currentVersion := repository.GetInstallState(tracer, packageName)
	if err := repository.ValidatePackage(tracer, packageName, version); err != nil ||
		(currentVersion == version && (currentState == localpackages.Failed || !isSameAsCache)) {
		pkgTrace.AppendDebugf("Current %v Target %v State %v", currentVersion, version, currentState).End()
		pkgTrace.AppendDebugf("Refreshing package content for %v %v", packageName, version).End()
		if err = repository.RefreshPackage(tracer, packageName, version, packageService.PackageServiceName(), buildDownloadDelegate(tracer, packageService, packageName, version)); err != nil {
			pkgTrace.WithError(err).End()
			return nil, err
		}
		if err = repository.ValidatePackage(tracer, packageName, version); err != nil {
			// TODO: Remove from repository?
			pkgTrace.WithError(err).End()
			return nil, err
		}
	}

	pkgTrace.End()
	return repository.GetInstaller(tracer, config, packageName, version, additionalArguments), nil
}

// buildDownloadDelegate constructs the delegate used by the repository to download a package from the service
func buildDownloadDelegate(tracer trace.Tracer, packageService packageservice.PackageService, packageName string, version string) func(trace.Tracer, string) error {
	return func(tracer trace.Tracer, targetDirectory string) error {
		trace := tracer.BeginSection("download artifact")
		filePath, err := packageService.DownloadArtifact(tracer, packageName, version)
		if err != nil {
			trace.WithError(err).End()
			return err
		}

		// TODO: Consider putting uncompress into the ssminstaller new and not deleting it (since the zip is the repository-validatable artifact)
		if uncompressErr := filesysdep.Uncompress(filePath, targetDirectory); uncompressErr != nil {
			trace.WithError(uncompressErr).End()
			return fmt.Errorf("failed to extract package installer package %v from %v, %v", filePath, targetDirectory, uncompressErr.Error())
		}

		// NOTE: this could be considered a warning - it likely points to a real problem, but if uncompress succeeded, we could continue
		// delete compressed package after using
		if cleanupErr := filesysdep.RemoveAll(filePath); cleanupErr != nil {
			trace.WithError(cleanupErr).End()
			return fmt.Errorf("failed to delete compressed package %v, %v", filePath, cleanupErr.Error())
		}

		trace.End()
		return nil
	}
}

// getVersionToInstall decides which version to install and whether there is an existing version (that is not in the process of installing)
func getVersionToInstall(
	tracer trace.Tracer,
	repository localpackages.Repository,
	packageArn string) (installedVersion string, installState localpackages.InstallState) {

	installedVersion = repository.GetInstalledVersion(tracer, packageArn)
	currentState, currentVersion := repository.GetInstallState(tracer, packageArn)
	if currentState == localpackages.Failed {
		// This will only happen if install failed with no previous successful install or if rollback failed
		installedVersion = currentVersion
	}

	return installedVersion, currentState
}

// getVersionToUninstall decides which version to uninstall
func getVersionToUninstall(
	tracer trace.Tracer,
	repository localpackages.Repository,
	packageArn string) (string, localpackages.InstallState) {

	installedVersion := repository.GetInstalledVersion(tracer, packageArn)
	currentState, _ := repository.GetInstallState(tracer, packageArn)

	if installedVersion == "" {
		return "", localpackages.None
	}

	return installedVersion, currentState
}

// parseAndValidateInput marshals raw JSON and returns the result of input validation or an error
func parseAndValidateInput(rawPluginInput interface{}) (*ConfigurePackagePluginInput, error) {
	var err error
	var input ConfigurePackagePluginInput

	pluginInputMap := make(map[string]interface{})
	b, err := json.Marshal(rawPluginInput)
	if err != nil {
		return nil, fmt.Errorf("cannot marshal plugin properties %v; \nerror %v", rawPluginInput, err)
	}
	err = json.Unmarshal(b, &pluginInputMap)
	if err != nil {
		return nil, fmt.Errorf("cannot unmarshal plugin properties %v; \nerror %v", string(b), err)
	}
	for k, v := range pluginInputMap {
		// Runcommand passes StringMap (i.e "additionalArguments" parameter) as string type as of 8/3/2020/.
		// It can be parsed as an empty map after json serialization only if it is not defined in the input.
		// Convert to empty string if the "additionalArguments" is a map; otherwise, use the defined value from input.
		if "additionalArguments" == k && reflect.ValueOf(v).Kind() == reflect.Map {
			pluginInputMap[k] = ""
		}
	}
	if err = jsonutil.Remarshal(pluginInputMap, &input); err != nil {
		return nil, fmt.Errorf("invalid format in plugin properties %v; \nerror %v", pluginInputMap, err)
	}

	if valid, err := validateInput(&input); !valid {
		return nil, fmt.Errorf("invalid input: %v", err)
	}

	return &input, nil
}

// validateInput ensures the plugin input matches the defined schema
func validateInput(input *ConfigurePackagePluginInput) (valid bool, err error) {
	// source not yet supported
	if input.Source != "" {
		return false, errors.New("source parameter is not supported in this version")
	}

	// ensure non-empty name
	if input.Name == "" {
		return false, errors.New("empty name field")
	}

	// dump any unsupported value for Repository
	if input.Repository != "beta" && input.Repository != "gamma" {
		input.Repository = ""
	}

	if input.AdditionalArguments != "" {
		// ensure additional arguments is a valid map of string to string pairs
		var argumentMap map[string]string
		if err = jsonutil.Unmarshal(input.AdditionalArguments, &argumentMap); err != nil {
			return false, fmt.Errorf("unable to unmarshal additional arguments from input; error %v", err)
		}
		if _, containsEmpty := argumentMap[""]; containsEmpty {
			return false, fmt.Errorf("empty key is not allowed in additional arguments from input")
		}
		for key := range argumentMap {
			if !strings.HasPrefix(key, "SSM_") {
				return false, fmt.Errorf(
					"keys need to start with \"SSM_\" prefix in additional arguments from input")
			}
		}
		var argumentString string
		if argumentString, err = jsonutil.Marshal(argumentMap); err != nil {
			// this will unlikely happen. handle it just in case
			return false, fmt.Errorf("unable to marshal additional arguments in input; error %v", err)
		}
		input.AdditionalArguments = argumentString
	}

	return true, nil
}

// checkAlreadyInstalled returns true if the version being installed is already in a valid installed state
// To do this, it uses the inst and uninst objects retreived from previous step and validates them
// and compares them with the installState.
func checkAlreadyInstalled(
	tracer trace.Tracer,
	context context.T,
	repository localpackages.Repository,
	installedVersion string,
	installState localpackages.InstallState,
	inst installer.Installer,
	uninst installer.Installer,
	output contracts.PluginOutputter) bool {

	checkTrace := tracer.BeginSection("check if already installed")
	defer checkTrace.End()

	if inst != nil {
		targetVersion := inst.Version()
		packageName := inst.PackageName()
		var instToCheck installer.Installer

		// TODO: When existing packages have idempotent installers and no reboot loops, remove this check for installing packages and allow the install to continue until it reports success without reboot
		if uninst != nil && installState == localpackages.RollbackInstall {
			// This supports rollback to a version whose installer contains an unconditional reboot
			instToCheck = uninst
		}
		if (targetVersion == installedVersion &&
			(installState == localpackages.Installed || installState == localpackages.Unknown)) ||
			installState == localpackages.Installing || installState == localpackages.Updating {
			instToCheck = inst
		}
		if instToCheck != nil {
			validateTrace := tracer.BeginSection(fmt.Sprintf("run validate for %s/%s", instToCheck.PackageName(), instToCheck.Version()))

			validateOutput := instToCheck.Validate(tracer, context)
			validateTrace.WithExitcode(int64(validateOutput.GetExitCode()))

			if validateOutput.GetStatus() == contracts.ResultStatusSuccess {
				if installState == localpackages.Installing || installState == localpackages.Updating {
					validateTrace.AppendInfof("Successfully installed %v %v", packageName, targetVersion)
					if uninst != nil {
						cleanupAfterUninstall(tracer, repository, uninst, output)
					}
					output.MarkAsSucceeded()
				} else if installState == localpackages.RollbackInstall {
					validateTrace.AppendInfof("Failed to install %v %v, successfully rolled back to %v %v", uninst.PackageName(), uninst.Version(), inst.PackageName(), inst.Version())
					cleanupAfterUninstall(tracer, repository, inst, output)
					output.MarkAsFailed(nil, nil)
				} else if installState == localpackages.Unknown {
					validateTrace.AppendInfof("The package install state is Unknown. Continue to check if there are package files already downloaded.")
					if err := repository.ValidatePackage(tracer, packageName, targetVersion); err != nil {
						// If the install state is Unkown and there's no package files downloaded previously, need to return false here so that the package can be downloaded and installed again.
						// This scenario happens when the installation of a package fails because package download fails due to lack of permissions (s3 bucket policy etc.)
						validateTrace.AppendInfof("There are no package files downloaded.")
						validateTrace.End()
						checkTrace.WithExitcode(1)
						return false
					} else {
						validateTrace.AppendInfof("There are package files already downloaded. Considering the package has already been installed.")
					}
				} else {
					validateTrace.AppendInfof("%v %v is already installed", packageName, targetVersion).End()
					output.MarkAsSucceeded()
				}
				if installState != localpackages.Installed && installState != localpackages.Unknown {
					repository.SetInstallState(tracer, packageName, instToCheck.Version(), localpackages.Installed)
				}

				validateTrace.End()
				return true
			}

			validateTrace.AppendInfo(validateOutput.GetStdout())
			validateTrace.AppendError(validateOutput.GetStderr())
			validateTrace.End()
		}
	}

	checkTrace.WithExitcode(1)
	return false
}

// selectService chooses the implementation of PackageService to use for a given execution of the plugin
func selectService(context context.T, tracer trace.Tracer, input *ConfigurePackagePluginInput, localrepo localpackages.Repository, appCfg *appconfig.SsmagentConfig, birdwatcherFacade facade.BirdwatcherFacade, isDocumentArchive *bool) (packageservice.PackageService, error) {
	response := &ssm.GetManifestOutput{}
	var err error

	// Before creating an object of type birdwatcher here, check if the name is of document arn. If it is, return with a Document type service
	if regexp.MustCompile(documentArnPattern).MatchString(input.Name) {
		*isDocumentArchive = true
		// return a new object of type document
		return birdwatcherservice.NewDocumentArchive(context, birdwatcherFacade, localrepo), nil
	}
	if input.Version != "" {
		// Birdwatcher version pattern and document version name pattern is different. If the pattern doesn't match Birdwatcher,
		// we assume document and continue, since birdwatcher will error out with ValidationException.
		// This could also happen if there is a typo in the birdwatcher version, but we assume Document and continue.
		if !regexp.MustCompile(birdwatcherVersionPattern).MatchString(input.Version) {
			*isDocumentArchive = true
			// return a new object of type document
			return birdwatcherservice.NewDocumentArchive(context, birdwatcherFacade, localrepo), nil
		}
	}

	// If not, make a call to GetManifest and try to figure out if it is birdwatcher or document archive.
	version := input.Version
	if packageservice.IsLatest(version) {
		version = packageservice.Latest
	}
	response, err = birdwatcherFacade.GetManifest(
		&ssm.GetManifestInput{
			PackageName:    &input.Name,
			PackageVersion: &version,
		},
	)

	// If the error returned is the "ResourceNotFoundException", create a service with document archive
	// if any other response, create a service of birdwatcher type
	if err != nil {
		if strings.Contains(err.Error(), resourceNotFoundException) {
			*isDocumentArchive = true
			// return a new object of type document
			return birdwatcherservice.NewDocumentArchive(context, birdwatcherFacade, localrepo), nil
		} else {
			tracer.CurrentTrace().AppendErrorf("Error returned for GetManifest - %v.", err.Error())
			return nil, err
		}
	}

	*isDocumentArchive = false

	// return a new object of type birdwatcher
	birdWatcherArchiveContext := make(map[string]string)
	birdWatcherArchiveContext["packageName"] = input.Name
	birdWatcherArchiveContext["packageVersion"] = input.Version
	birdWatcherArchiveContext["manifest"] = *response.Manifest
	return birdwatcherservice.NewBirdwatcherArchive(context, birdwatcherFacade, localrepo, birdWatcherArchiveContext), nil
}

// Execute runs the plugin operation and returns output
func (p *Plugin) Execute(config contracts.Configuration, cancelFlag task.CancelFlag, output iohandler.IOHandler) {
	p.execute(config, cancelFlag, output)
	return
}

func (p *Plugin) execute(config contracts.Configuration, cancelFlag task.CancelFlag, output iohandler.IOHandler) {
	log := p.context.Log()
	log.Info("RunCommand started with configuration ", config)
	tracer := trace.NewTracer(log)
	defer tracer.BeginSection("configurePackage").End()

	out := trace.PluginOutputTrace{Tracer: tracer}

	if cancelFlag.ShutDown() {
		out.MarkAsShutdown()
	} else if cancelFlag.Canceled() {
		out.MarkAsCancelled()
	} else if input, err := parseAndValidateInput(config.Properties); err != nil {
		tracer.CurrentTrace().WithError(err).End()
		out.MarkAsFailed(nil, nil)
	} else {
		appConfig := p.context.AppConfig()
		packageService, err := p.packageServiceSelector(p.context, tracer, input, p.localRepository, &appConfig, p.birdwatcherfacade, &p.isDocumentArchive)
		if err != nil {
			tracer.CurrentTrace().WithError(err).End()
			out.MarkAsFailed(nil, nil)
		}
		if out.GetStatus() != contracts.ResultStatusFailed {
			//Return failure if the manifest cannot be accessed
			//Return failure if the package version is installed, but the manifest is no longer available
			packageName, packageVersion := packageService.GetPackageArnAndVersion(input.Name, input.Version)

			//always download the manifest before acting upon the request
			trace := tracer.BeginSection("download manifest")
			packageArn, manifestVersion, isSameAsCache, err := packageService.DownloadManifest(tracer, packageName, packageVersion)
			trace.AppendDebugf("got manifest for package %v version %v isSameAsCache %v", packageArn, manifestVersion, isSameAsCache)

			trace.End()

			if err != nil {
				tracer.CurrentTrace().WithError(err).End()
				out.MarkAsFailed(nil, nil)
			} else if err := p.localRepository.LockPackage(tracer, packageArn, input.Action); err != nil {
				// do not allow multiple actions to be performed at the same time for the same package
				// this is possible with multiple concurrent runcommand documents
				tracer.CurrentTrace().WithError(err).End()
				out.MarkAsFailed(nil, nil)
			} else {
				defer p.localRepository.UnlockPackage(tracer, packageArn)

				log.Debugf("Prepare for %v %v %v", input.Action, input.Name, input.Version)
				inst, uninst, isUpdateInPlace, installState, installedVersion := prepareConfigurePackage(
					tracer,
					config,
					p.localRepository,
					packageService,
					input,
					packageArn,
					manifestVersion,
					isSameAsCache,
					&out)
				log.Debugf("HasInst %v, HasUninst %v, IsInplaceUpdate %v, InstallState %v, PackageName %v, InstalledVersion %v", inst != nil, uninst != nil, isUpdateInPlace, installState, packageArn, installedVersion)

				//if the status is already decided as failed or succeeded, do not execute anything
				if out.GetStatus() != contracts.ResultStatusFailed && out.GetStatus() != contracts.ResultStatusSuccess {
					alreadyInstalled := checkAlreadyInstalled(tracer, p.context, p.localRepository, installedVersion, installState, inst, uninst, &out)
					// if package is not installed, set isUpdateInPlace to false so as to execute install script to install
					if installedVersion == "" || installState == localpackages.None {
						isUpdateInPlace = false
					}
					// if already failed or already installed and valid, do not execute install
					// if it is already installed and the cache is the same, do not execute install
					if !alreadyInstalled || !isSameAsCache {
						log.Debugf("Calling execute, current status %v", out.GetStatus())
						executeConfigurePackage(
							tracer,
							p.context,
							p.localRepository,
							inst,
							uninst,
							isUpdateInPlace,
							installState,
							&out)
					}
				}
				if err := p.localRepository.LoadTraces(tracer, packageArn); err != nil {
					log.Errorf("Error loading prior traces: %v", err.Error())
				}
				if out.GetStatus().IsReboot() {
					err := p.localRepository.PersistTraces(tracer, packageArn)
					if err != nil {
						log.Errorf("Error persisting traces: %v", err.Error())
					}
				} else {
					version := manifestVersion
					if out.GetStatus() != contracts.ResultStatusFailed && out.GetStatus() != contracts.ResultStatusSuccess {
						if input.Action == InstallAction {
							version = inst.Version()
						} else if input.Action == UninstallAction {
							version = uninst.Version()
						}
					}
					startTime := tracer.Traces()[0].Start
					for _, trace := range tracer.Traces() {
						if trace.Start < startTime {
							startTime = trace.Start
						}
					}

					err := packageService.ReportResult(tracer, packageservice.PackageResult{
						Exitcode:               int64(out.GetExitCode()),
						Operation:              input.Action,
						PackageName:            input.Name,
						PreviousPackageVersion: installedVersion,
						Timing:                 startTime,
						Version:                version,
						Trace:                  packageservice.ConvertToPackageServiceTrace(tracer.Traces()),
					})
					if err != nil {
						if p.isDocumentArchive {
							log.Warnf("Error reporting results for Document archive: %v", err.Error())
						} else {
							out.AppendErrorf(log, "Error reporting results: %v", err.Error())
						}
					}
				}
			}
		}
	}

	output.SetExitCode(out.GetExitCode())
	output.SetStatus(out.GetStatus())

	// convert trace
	traceout := tracer.ToPluginOutput()
	output.AppendInfo(traceout.GetStdout())
	output.AppendError(traceout.GetStderr())

	return
}

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