// 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 refreshes association immediately
package processor

import (
	"fmt"
	"time"

	"github.com/aws/amazon-ssm-agent/agent/association/cache"
	"github.com/aws/amazon-ssm-agent/agent/association/model"
	"github.com/aws/amazon-ssm-agent/agent/association/schedulemanager"
	"github.com/aws/amazon-ssm-agent/agent/association/schedulemanager/signal"
	"github.com/aws/amazon-ssm-agent/agent/association/service"
	"github.com/aws/amazon-ssm-agent/agent/contracts"
	"github.com/aws/amazon-ssm-agent/agent/jsonutil"
	"github.com/aws/amazon-ssm-agent/agent/log"
	"github.com/aws/amazon-ssm-agent/agent/plugins/pluginutil"
	"github.com/aws/amazon-ssm-agent/agent/times"

	"github.com/aws/amazon-ssm-agent/agent/framework/processor/executer/iohandler"
	"github.com/aws/aws-sdk-go/aws"
)

// ProcessRefreshAssociation executes one set of commands and returns their output.
func (p *Processor) ProcessRefreshAssociation(log log.T, pluginRes *contracts.PluginResult, orchestrationDir string, apply bool) {
	var associationIds []string
	jsonutil.Remarshal(pluginRes.Output, &associationIds)

	ioConfig := contracts.IOConfiguration{
		OrchestrationDirectory: orchestrationDir,
		OutputS3BucketName:     pluginRes.OutputS3BucketName,
		OutputS3KeyPrefix:      pluginRes.OutputS3KeyPrefix,
	}
	out := iohandler.NewDefaultIOHandler(p.context, ioConfig)
	defer out.Close()
	out.Init(pluginRes.PluginName)

	//this is the current plugin run, trigger refresh
	if apply {
		p.refreshAssociation(log, associationIds, orchestrationDir, pluginRes.OutputS3BucketName, pluginRes.OutputS3KeyPrefix, out)
	} else {
		//operation is already done, fill success result as default
		out.MarkAsSucceeded()
		// if user provided empty list or "" in the document, we will run all the associations now
		applyAll := len(associationIds) == 0 || (len(associationIds) == 1 && associationIds[0] == "")
		if applyAll {
			out.AppendInfo("All associations have been requested to execute immediately")
		} else {
			out.AppendInfof("Associations %v have been requested to execute immediately", associationIds)
		}
	}
	out.Close()
	pluginConfig := iohandler.DefaultOutputConfig()

	pluginRes.Code = out.GetExitCode()
	pluginRes.Status = contracts.ResultStatusSuccess
	pluginRes.Output = out.GetOutput()
	pluginRes.StandardOutput = pluginutil.StringPrefix(out.GetStdout(), pluginConfig.MaxStdoutLength, pluginConfig.OutputTruncatedSuffix)
	pluginRes.StandardError = pluginutil.StringPrefix(out.GetStderr(), pluginConfig.MaxStderrLength, pluginConfig.OutputTruncatedSuffix)
}

// refreshAssociation executes one the command and returns their output.
func (p *Processor) refreshAssociation(log log.T, associationIds []string, orchestrationDirectory string, outputS3BucketName string, outputS3KeyPrefix string, out iohandler.IOHandler) {
	var err error
	var instanceID string
	associations := []*model.InstanceAssociation{}

	if instanceID, err = p.context.Identity().InstanceID(); err != nil {
		out.MarkAsFailed(fmt.Errorf("failed to load instance ID, %v", err))
		return
	}

	// Get associations
	if associations, err = p.assocSvc.ListInstanceAssociations(log, instanceID); err != nil {
		out.MarkAsFailed(fmt.Errorf("failed to list instance associations, %v", err))
		return
	}

	// evict the invalid cache first
	for _, assoc := range associations {
		cache.ValidateCache(assoc)
	}

	// if user provided empty list or "" in the document, we will run all the associations now
	applyAll := len(associationIds) == 0 || (len(associationIds) == 1 && associationIds[0] == "")

	// Default is success
	out.MarkAsSucceeded()

	// read from cache or load association details from service
	for _, assoc := range associations {
		if err = p.assocSvc.LoadAssociationDetail(log, assoc); err != nil {
			err = fmt.Errorf("Encountered error while loading association %v contents, %v",
				*assoc.Association.AssociationId,
				err)
			assoc.Errors = append(assoc.Errors, err)
			p.assocSvc.UpdateInstanceAssociationStatus(
				log,
				*assoc.Association.AssociationId,
				*assoc.Association.Name,
				*assoc.Association.InstanceId,
				contracts.AssociationStatusFailed,
				contracts.AssociationErrorCodeListAssociationError,
				times.ToIso8601UTC(time.Now()),
				err.Error(),
				service.NoOutputUrl)
			out.MarkAsFailed(err)
			continue
		}

		// validate association expression, fail association if expression cannot be passed
		// Note: we do not want to fail runcommand with out.MarkAsFailed
		if !assoc.IsRunOnceAssociation() {
			if err := assoc.ParseExpression(log); err != nil {
				message := fmt.Sprintf("Encountered error while parsing expression for association %v", *assoc.Association.AssociationId)
				log.Errorf("%v, %v", message, err)
				assoc.Errors = append(assoc.Errors, err)
				p.assocSvc.UpdateInstanceAssociationStatus(
					log,
					*assoc.Association.AssociationId,
					*assoc.Association.Name,
					*assoc.Association.InstanceId,
					contracts.AssociationStatusFailed,
					contracts.AssociationErrorCodeInvalidExpression,
					times.ToIso8601UTC(time.Now()),
					message,
					service.NoOutputUrl)
				out.MarkAsFailed(err)
				continue
			}
		}

		if applyAll || isAssociationQualifiedToRunNow(associationIds, assoc) {
			// If association is already InProgress, we don't want to run it again
			if assoc.Association.DetailedStatus == nil ||
				(*assoc.Association.DetailedStatus != contracts.AssociationStatusInProgress && *assoc.Association.DetailedStatus != contracts.AssociationStatusPending) {
				// Updates status to pending, which is the indicator for schedulemanager for immediate execution
				p.assocSvc.UpdateInstanceAssociationStatus(
					log,
					*assoc.Association.AssociationId,
					*assoc.Association.Name,
					*assoc.Association.InstanceId,
					contracts.AssociationStatusPending,
					contracts.AssociationErrorCodeNoError,
					times.ToIso8601UTC(time.Now()),
					contracts.AssociationPendingMessage,
					service.NoOutputUrl)
				assoc.Association.DetailedStatus = aws.String(contracts.AssociationStatusPending)
			}
		}
	}

	schedulemanager.Refresh(log, associations)

	if applyAll {
		out.AppendInfo("All associations have been requested to execute immediately")
	} else {
		out.AppendInfof("Associations %v have been requested to execute immediately", associationIds)
	}

	signal.ExecuteAssociation(log)

	return
}

// doesAssociationQualifiedToRunNow finds out if association is qualified to run now
func isAssociationQualifiedToRunNow(AssociationIds []string, assoc *model.InstanceAssociation) bool {
	for _, id := range AssociationIds {
		if *assoc.Association.AssociationId == id {
			return true
		}
	}

	return false
}
