// 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 processor contains the methods for update ssm agent.
// It also provides methods for sendReply and updateInstanceInfo
package processor

import (
	"time"

	"github.com/aws/amazon-ssm-agent/agent/contracts"
	"github.com/aws/amazon-ssm-agent/agent/fileutil"
	logPkg "github.com/aws/amazon-ssm-agent/agent/log"
	"github.com/aws/amazon-ssm-agent/agent/log/logger"
	"github.com/aws/amazon-ssm-agent/agent/updateutil/updateconstants"
)

// inProgress sets update to inProgressing with given new UpdateState
func (u *updateManager) inProgress(updateDetail *UpdateDetail, log logPkg.T, state UpdateState) (err error) {
	defer func() {
		if err != nil {
			failedUpdateDetail := &UpdateDetail{
				State:         Completed,
				Result:        contracts.ResultStatusFailed,
				TargetVersion: updateDetail.TargetVersion,
				SourceVersion: updateDetail.SourceVersion,
			}
			errorCode := u.subStatus + string(state)
			log.WriteEvent(
				logger.AgentUpdateResultMessage,
				failedUpdateDetail.SourceVersion,
				PrepareHealthStatus(failedUpdateDetail, errorCode, failedUpdateDetail.TargetVersion))
			if err = u.svc.UpdateHealthCheck(log, failedUpdateDetail, errorCode); err != nil {
				log.Errorf(err.Error())
			}
		}
	}()
	updateDetail.State = state
	updateDetail.Result = contracts.ResultStatusInProgress

	if updateDetail.HasMessageID() && !updateDetail.SelfUpdate {
		if updateDetail.UpstreamServiceName == string(contracts.MessageGatewayService) {
			agentResult := prepareAgentResult(u.Context, updateDetail)
			if err = persistPayload(log, updateDetail, u.Context.Identity(), agentResult); err != nil {
				log.Errorf(err.Error())
			}
		} else {
			err = u.svc.SendReply(log, updateDetail)
			if err != nil {
				log.Errorf(err.Error())
			}
		}
	}

	if err = u.svc.UpdateHealthCheck(log, updateDetail, ""); err != nil {
		log.Errorf(err.Error())
	}
	return nil
}

func (u *updateManager) reportTestResultGenerator(updateDetail *UpdateDetail, log logPkg.T) func(contracts.ResultStatus, string) {
	return func(testStatus contracts.ResultStatus, testName string) {
		updateStatus := &UpdateDetail{
			State:         TestExecution,
			Result:        testStatus,
			TargetVersion: updateDetail.TargetVersion,
			SourceVersion: updateDetail.SourceVersion,
		}

		if err := u.svc.UpdateHealthCheck(log, updateStatus, testName); err != nil {
			log.Errorf("Error while sending test failure metric: %v", err.Error())
		}
	}
}

// reportIntermediateMetric reports initial (un)installation ErrorCode without setting update to failed and continue to rollback
func reportIntermediateMetric(u *updateManager, updateDetail *UpdateDetail, code updateconstants.ErrorCode) (err error) {
	log := u.Context.Log()

	updateStatus := &UpdateDetail{
		State:         UpdaterMetric,
		Result:        updateDetail.Result,
		TargetVersion: updateDetail.TargetVersion,
		SourceVersion: updateDetail.SourceVersion,
	}

	errorCode := u.subStatus + string(code)

	if err := u.svc.UpdateHealthCheck(log, updateStatus, errorCode); err != nil {
		log.Errorf("Error while reporting intermediate metric: %v", err.Error())
	}

	return nil
}

// succeeded sets update to completed
func (u *updateManager) succeeded(updateDetail *UpdateDetail, log logPkg.T) (err error) {
	updateDetail.State = Completed
	updateDetail.Result = contracts.ResultStatusSuccess
	updateDetail.AppendInfo(
		log,
		"%v updated successfully to %v",
		updateDetail.PackageName,
		updateDetail.TargetVersion)

	log.WriteEvent(
		logger.AgentUpdateResultMessage,
		updateDetail.SourceVersion,
		PrepareHealthStatus(updateDetail, "", updateDetail.TargetVersion))
	return u.finalize(u, updateDetail, "")
}

// failed sets update to failed with error messages
func (u *updateManager) failed(updateDetail *UpdateDetail, log logPkg.T, code updateconstants.ErrorCode, errMessage string, noRollbackMessage bool) (err error) {
	updateDetail.State = Completed
	updateDetail.Result = contracts.ResultStatusFailed
	updateDetail.AppendInfo(log, "%s", errMessage)
	updateDetail.AppendInfo(
		log,
		"Failed to update %v to %v",
		updateDetail.PackageName,
		updateDetail.TargetVersion)

	// Specify no rollback needed
	if noRollbackMessage {
		updateDetail.AppendInfo(log, "No rollback needed")
	}

	errorCode := u.subStatus + string(code)
	log.WriteEvent(
		logger.AgentUpdateResultMessage,
		updateDetail.SourceVersion,
		PrepareHealthStatus(updateDetail, errorCode, updateDetail.TargetVersion))
	return u.finalize(u, updateDetail, errorCode)
}

func (u *updateManager) inactive(updateDetail *UpdateDetail, log logPkg.T, errorWarnCode string) (err error) {
	updateDetail.State = Completed
	updateDetail.Result = contracts.ResultStatusSuccess
	updateDetail.AppendInfo(
		log,
		"%v version %v is deprecated/inactive, update skipped",
		updateDetail.PackageName,
		updateDetail.TargetVersion)
	errorWarnCode = u.subStatus + errorWarnCode
	log.WriteEvent(
		logger.AgentUpdateResultMessage,
		updateDetail.SourceVersion,
		PrepareHealthStatus(updateDetail, errorWarnCode, updateDetail.TargetVersion))
	return u.finalize(u, updateDetail, errorWarnCode)
}

func (u *updateManager) skipped(updateDetail *UpdateDetail, log logPkg.T) (err error) {
	updateDetail.State = Completed
	updateDetail.Result = contracts.ResultStatusSuccess
	updateDetail.AppendInfo(
		log,
		"update skipped")
	return u.finalize(u, updateDetail, "")
}

// finalizeUpdateAndSendReply completes the update and sends reply to message service, also uploads to S3 (if any)
func finalizeUpdateAndSendReply(u *updateManager, updateDetail *UpdateDetail, errorCode string) (err error) {
	log := u.Context.Log()
	updateDetail.EndDateTime = time.Now().UTC()

	if !updateDetail.SelfUpdate {
		orchestrationDirectory := getOrchestrationDir(u.Context.Identity(), log, updateDetail)
		var filePath string
		filePath, err = fileutil.AppendToFile(orchestrationDirectory, updateDetail.StdoutFileName, updateDetail.StandardOut)
		if err != nil {
			log.Errorf("Error while appending to file %v", filePath)
		}
		if updateDetail.StandardOut, err = fileutil.ReadAllText(filePath); err != nil {
			log.Errorf("Error reading contents from %v", filePath)
		}

		if filePath, err = fileutil.AppendToFile(orchestrationDirectory, updateDetail.StderrFileName, updateDetail.StandardError); err != nil {
			log.Errorf("Error while appending to file %v", filePath)
		}
		if updateDetail.StandardError, err = fileutil.ReadAllText(filePath); err != nil {
			log.Errorf("Error reading contents from %v", filePath)
		}
		// send reply except for self update, don't send any response back to service side for self update
		if updateDetail.HasMessageID() {
			if updateDetail.UpstreamServiceName == string(contracts.MessageGatewayService) {
				agentResult := prepareAgentResult(u.Context, updateDetail)
				if err = persistPayload(log, updateDetail, u.Context.Identity(), agentResult); err != nil {
					log.Errorf(err.Error())
				}
			} else {
				if err = u.svc.SendReply(log, updateDetail); err != nil {
					log.Errorf(err.Error())
				}

				if err = u.svc.DeleteMessage(log, updateDetail); err != nil {
					log.Errorf(err.Error())
				}
			}
		}

		// upload output to s3 bucket
		log.Debugf("output s3 bucket name is %v", updateDetail.OutputS3BucketName)
		if updateDetail.OutputS3BucketName != "" {
			u.ctxMgr.uploadOutput(log, updateDetail, orchestrationDirectory)
		}
	}

	// update health information
	if err = u.svc.UpdateHealthCheck(log, updateDetail, errorCode); err != nil {
		log.Errorf(err.Error())
	}

	u.clean(log, updateDetail)

	return nil
}
