// Copyright 2017 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 rundocument implements the aws:runDocument plugin
package rundocument

import (
	"encoding/json"
	"path/filepath"

	"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/docparser"
	"github.com/aws/amazon-ssm-agent/agent/framework/processor/executer"
	"github.com/aws/amazon-ssm-agent/agent/task"
	"gopkg.in/yaml.v2"
)

type ExecDocument interface {
	ParseDocument(context context.T, documentRaw []byte, orchestrationDir string,
		s3Bucket string, s3KeyPrefix string, messageID string, documentID string, defaultWorkingDirectory string,
		params map[string]interface{}) (pluginsInfo []contracts.PluginState, err error)
	ExecuteDocument(config contracts.Configuration, context context.T, pluginInput []contracts.PluginState, documentID string,
		documentCreatedDate string) (chan contracts.DocumentResult, error)
}

type ExecDocumentImpl struct {
	DocExecutor executer.Executer
}

// ParseDocument parses the remote document obtained to a format that the executor can use.
// This function is also responsible for all the validation of document and replacement of parameters
func (exec ExecDocumentImpl) ParseDocument(context context.T, documentRaw []byte, orchestrationDir string,
	s3Bucket string, s3KeyPrefix string, messageID string, documentID string, defaultWorkingDirectory string,
	params map[string]interface{}) (pluginsInfo []contracts.PluginState, err error) {
	log := context.Log()
	docContent := docparser.DocContent{
		InvokedPlugin: appconfig.PluginRunDocument,
	}
	if err := json.Unmarshal(documentRaw, &docContent); err != nil {
		if err := yaml.Unmarshal(documentRaw, &docContent); err != nil {
			log.Error("Unmarshaling remote resource document failed. Please make sure the document is in the correct JSON or YAML formal")
			return pluginsInfo, err
		}
	}
	parserInfo := docparser.DocumentParserInfo{
		OrchestrationDir:  orchestrationDir,
		S3Bucket:          s3Bucket,
		S3Prefix:          s3KeyPrefix,
		MessageId:         messageID,
		DocumentId:        documentID,
		DefaultWorkingDir: defaultWorkingDirectory,
	}

	pluginsInfo, err = docContent.ParseDocument(context, contracts.DocumentInfo{}, parserInfo, params)
	log.Debug("Parsed document - ", docContent)
	log.Debug("Plugins Info - ", pluginsInfo)
	return
}

// ExecuteDocument is responsible to execute the sub-documents that are created or downloaded by the executeCommand plugin
func (exec ExecDocumentImpl) ExecuteDocument(config contracts.Configuration, context context.T, pluginInput []contracts.PluginState, documentID string,
	documentCreatedDate string) (resultChannels chan contracts.DocumentResult, err error) {
	log := context.Log()
	log.Info("Running sub-document")

	// The full path of orchestrationDir should look like:
	// Linux: /var/lib/amazon/ssm/instance-id/document/orchestration/command-id/plugin-id
	// Windows: %PROGRAMDATA%\Amazon\SSM\InstanceData\instance-id\document\orchestration\command-id\plugin-id
	orchestrationDir := filepath.Join(config.OrchestrationDirectory, config.PluginID)

	docState := contracts.DocumentState{
		DocumentInformation: contracts.DocumentInfo{
			DocumentID: documentID,
		},
		IOConfig: contracts.IOConfiguration{
			OrchestrationDirectory: orchestrationDir,
			OutputS3BucketName:     "",
			OutputS3KeyPrefix:      "",
		},
		InstancePluginsInformation: pluginInput,
		UpstreamServiceName:        config.UpstreamServiceName,
	}

	docStore := executer.NewDocumentFileStore(documentID, appconfig.DefaultLocationOfCurrent, &docState,
		NewNoOpDocumentMgr(context), true)
	cancelFlag := task.NewChanneledCancelFlag()
	resultChannels = exec.DocExecutor.Run(cancelFlag, &docStore)

	return resultChannels, nil
}

// Workaround to keep this plugin from overwriting the DocumentState of the top-level document.
//
// Up until version 3.0.732.0, this plugin always failed to write the DocumentState to file due to a
// path calculation bug.  In 3.0.732.0, the path calculation bug was fixed, and the plugin
// started overwriting the top-level document's DocumentState, which sometimes interfered with
// plugins in the top-level document that executed after this one.  NoOpDocumentMgr is meant
// to keep this plugin from overwriting the state of the top-level document.  However, it does
// not persist the DocumentState over a reboot, and it does not track the status (e.g. "pending",
// "current", "corrupt") of the document.  Longer-term, we may want to implement proper DocumentState
// bookkeeping for the documents executed by this plugin.
type NoOpDocumentMgr struct {
	context context.T
	state   contracts.DocumentState
}

func NewNoOpDocumentMgr(ctx context.T) *NoOpDocumentMgr {
	return &NoOpDocumentMgr{
		ctx,
		contracts.DocumentState{},
	}
}

func (m *NoOpDocumentMgr) MoveDocumentState(fileName, srcLocationFolder, dstLocationFolder string) {
	// No-op
	m.context.Log().Debugf("NoOpDocumentMgr.MoveDocumentState(%s, %s, %s)", fileName, srcLocationFolder, dstLocationFolder)
}

func (m *NoOpDocumentMgr) PersistDocumentState(fileName, locationFolder string, state contracts.DocumentState) {
	m.context.Log().Debugf("NoOpDocumentMgr.PersistDocumentState(%s, %s, %+v)", fileName, locationFolder, state)
	m.state = state
}

func (m *NoOpDocumentMgr) GetDocumentState(fileName, locationFolder string) contracts.DocumentState {
	m.context.Log().Debugf("NoOpDocumentMgr.GetDocumentState(%s, %s)", fileName, locationFolder)
	m.context.Log().Warn("NoOpDocumentMgr.GetDocumentState() called, consider using a persistent DocumentMgr")
	return m.state
}

func (m *NoOpDocumentMgr) RemoveDocumentState(fileName, locationFolder string) {
	// No-op
	m.context.Log().Debugf("NoOpDocumentMgr.RemoveDocumentState(%s, %s)", fileName, locationFolder)
}
