// 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 downloadcontent implements the aws:downloadContent plugin
package downloadcontent

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"reflect"
	"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/fileutil"
	"github.com/aws/amazon-ssm-agent/agent/fileutil/filemanager"
	"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/plugins/downloadcontent/gitresource/github"
	"github.com/aws/amazon-ssm-agent/agent/plugins/downloadcontent/gitresource/github/privategithub"
	"github.com/aws/amazon-ssm-agent/agent/plugins/downloadcontent/gitresource/privategit"
	"github.com/aws/amazon-ssm-agent/agent/plugins/downloadcontent/httpresource"
	"github.com/aws/amazon-ssm-agent/agent/plugins/downloadcontent/remoteresource"
	"github.com/aws/amazon-ssm-agent/agent/plugins/downloadcontent/s3resource"
	"github.com/aws/amazon-ssm-agent/agent/plugins/downloadcontent/ssmdocresource"
	"github.com/aws/amazon-ssm-agent/agent/ssm/ssmparameterresolver"
	"github.com/aws/amazon-ssm-agent/agent/task"
)

const (
	Git         = "Git"         //Git represents any arbitrary "Git" repository from where the resource can be downloaded
	HTTP        = "HTTP"        //HTTP represents any arbitrary URL from where the resource can be downloaded
	GitHub      = "GitHub"      //Github represents the source type "GitHub" from where the resource can be downloaded
	S3          = "S3"          //S3 represents the source type "S3" from where the resource is being downloaded
	SSMDocument = "SSMDocument" //SSMDocument represents the source type as SSM Document

	downloadsDir = "downloads" //Directory under the orchestration directory where the downloaded resource resides

	FailExitCode = 1
	PassExitCode = 0
)

var sourceTypes = map[string]bool{
	Git:         true,
	HTTP:        true,
	GitHub:      true,
	S3:          true,
	SSMDocument: true,
}

var SetPermission = SetFilePermissions

// NewPlugin returns a new instance of the plugin.
func NewPlugin(context context.T) (*Plugin, error) {
	return &Plugin{
		context:               context,
		remoteResourceCreator: newRemoteResource,
	}, nil
}

// Plugin is the type for the aws:downloadContent plugin.
type Plugin struct {
	context               context.T
	remoteResourceCreator func(context context.T, sourceType string, SourceInfo string) (remoteresource.RemoteResource, error)
	filesys               filemanager.FileSystem
}

// ExecutePluginInput is a struct that holds the parameters sent through send command
type DownloadContentPlugin struct {
	contracts.PluginInput
	SourceType      string `json:"sourceType"`
	SourceInfo      string `json:"sourceInfo"`
	DestinationPath string `json:"destinationPath"`
	// TODO: 08/25/2017 meloniam@ Change the type of SourceInfo and documentParameters to map[string]interface{}
	// TODO: https://amazon.awsapps.com/workdocs/index.html#/document/7d56a42ea5b040a7c33548d77dc98040f0fb380bbbfb2fd580c861225e2ee1c7
}

// newRemoteResource switches between the source type and returns a struct of the source type that implements remoteresource
func newRemoteResource(context context.T, SourceType string, SourceInfo string) (resource remoteresource.RemoteResource, err error) {
	switch SourceType {
	case GitHub:
		// TODO: meloniam@ 08/24/2017 Replace string type to map[string]inteface{} type once Runcommand supports string maps
		// TODO: https://amazon.awsapps.com/workdocs/index.html#/document/7d56a42ea5b040a7c33548d77dc98040f0fb380bbbfb2fd580c861225e2ee1c7
		token := privategithub.NewTokenInfoImpl(context)
		return github.NewGitHubResource(context, SourceInfo, token)
	case S3:
		return s3resource.NewS3Resource(context, SourceInfo)
	case SSMDocument:
		return ssmdocresource.NewSSMDocResource(context, SourceInfo)
	case HTTP:
		ssmParameterResolverBridge := ssmparameterresolver.NewSsmParameterResolverBridge(ssmparameterresolver.NewService(context))
		return httpresource.NewHTTPResource(context, SourceInfo, ssmParameterResolverBridge)
	case Git:
		ssmParameterResolverBridge := ssmparameterresolver.NewSsmParameterResolverBridge(ssmparameterresolver.NewService(context))
		return privategit.NewGitResource(context, SourceInfo, ssmParameterResolverBridge)
	default:
		return nil, fmt.Errorf("Invalid SourceType - %v", SourceType)
	}
}

// Execute runs multiple sets of commands and returns their outputs.
// res.Output will contain a slice of RunCommandPluginOutput.
func (p *Plugin) Execute(config contracts.Configuration, cancelFlag task.CancelFlag, output iohandler.IOHandler) {
	p.filesys = filemanager.FileSystemImpl{}
	p.execute(config, cancelFlag, output)
}

func (p *Plugin) execute(config contracts.Configuration, cancelFlag task.CancelFlag, output iohandler.IOHandler) {
	log := p.context.Log()
	log.Info("Plugin aws:downloadContent started with configuration", config)

	if cancelFlag.ShutDown() {
		output.MarkAsShutdown()
	} else if cancelFlag.Canceled() {
		output.MarkAsCancelled()
	} else if input, err := parseAndValidateInput(config.Properties); err != nil {
		output.MarkAsFailed(err)
	} else {
		p.runCopyContent(log, input, config, output)
	}
}

// runCopyContent figures out the type of source, downloads the resource, saves it on disk and returns information required for it
func (p *Plugin) runCopyContent(log log.T, input *DownloadContentPlugin, config contracts.Configuration, output iohandler.IOHandler) {

	//Run aws:downloadContent plugin
	log.Debug("Inside run downloadcontent function")

	// remoteResourceCreator makes a call to a function that creates a new remote resource based on the source type
	log.Debug("Creating resource of type - ", input.SourceType)
	remoteResource, err := p.remoteResourceCreator(p.context, input.SourceType, input.SourceInfo)
	if err != nil {
		output.MarkAsFailed(err)
		return
	}
	var destinationPath string

	// If path is absolute, then download to the path,
	// else download to orchestrationDir/<downloads dir>/relative path
	if filepath.IsAbs(input.DestinationPath) {
		destinationPath = input.DestinationPath
	} else {
		log.Debugf("PluginId, plugin name, orch dir  - %v, %v, %v ", config.PluginID, config.PluginName, config.OrchestrationDirectory)
		orchestrationDir := strings.TrimSuffix(config.OrchestrationDirectory, config.PluginID)

		// The reason for not using Join or Buildpath here is so that the trailing "\" in case of windows is not dropped.
		destinationPath = filepath.Join(orchestrationDir, downloadsDir) + string(os.PathSeparator) + input.DestinationPath
	}

	log.Debug("About to validate source info")
	if valid, err := remoteResource.ValidateLocationInfo(); !valid {
		output.MarkAsFailed(err)
		return
	}

	var result *remoteresource.DownloadResult
	log.Debug("Downloading resource")

	if err, result = remoteResource.DownloadRemoteResource(p.filesys, destinationPath); err != nil {
		output.MarkAsFailed(err)
		return
	}

	if err := setPermissions(log, result); err != nil {
		output.MarkAsFailed(fmt.Errorf("Failed to set right permissions to the content. Error - %v", err))
		return
	}

	output.AppendInfof("Content downloaded to %v", destinationPath)
	output.MarkAsSucceeded()
	return
}

func setPermissions(log log.T, result *remoteresource.DownloadResult) error {
	for _, path := range result.Files {
		log.Infof("Setting permission for file %v", path)
		if fileutil.IsDirectory(path) {
			return fmt.Errorf("Internal error - file is expected, but found directory - %v", path)
		}
		if err := SetPermission(log, path); err != nil {
			return fmt.Errorf("Failed to set right permissions to the content. Error - %v", err)
		}
	}

	return nil
}

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

// parseAndValidateInput parses the input json file and also validates its inputs
func parseAndValidateInput(rawPluginInput interface{}) (*DownloadContentPlugin, error) {
	var input DownloadContentPlugin
	var err error
	// the below code is added to fix the issue when sourceInfo json is parsed as map instead of string
	pluginInputMap := make(map[string]interface{})
	if err = jsonutil.Remarshal(rawPluginInput, &pluginInputMap); err != nil {
		return nil, fmt.Errorf("problem while remarshalling %v; \nerror %v", rawPluginInput, err)
	}

	// the below conversion is done for handling the change in parameter structure when sent from CLI and Console
	for paramName, paramValue := range pluginInputMap {
		if strings.ToLower(paramName) == "sourceinfo" {
			if reflect.ValueOf(paramValue).Kind() == reflect.Map {
				if sourceInfoBytes, err := json.Marshal(paramValue); err == nil {
					pluginInputMap[paramName] = string(sourceInfoBytes)
				}
			}
		}
	}

	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 *DownloadContentPlugin) (valid bool, err error) {
	// ensure non-empty source type
	if input.SourceType == "" {
		return false, errors.New("SourceType must be specified")
	}
	//ensure all entries are valid
	if !sourceTypes[input.SourceType] {
		return false, errors.New("Unsupported source type")
	}
	// ensure non-empty source info
	if input.SourceInfo == "" {
		return false, errors.New("SourceInfo must be specified")
	}

	return true, nil
}

// SetFilePermissions applies execute permissions to the folder
func SetFilePermissions(log log.T, workingDir string) error {

	var permissionsWalk = func(path string, info os.FileInfo, e error) (err error) {
		log.Info("Changing permissions for ", path)
		return os.Chmod(path, appconfig.ReadWriteExecuteAccess)
	}

	err := filepath.Walk(workingDir, permissionsWalk)
	if err != nil {
		log.Errorf("Error while changing the permissions of files - %v", err.Error())
	}
	return err
}
