// 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 parameterstore contains modules to resolve ssm parameters present in the document.
package parameterstore

import (
	"errors"
	"fmt"
	"regexp"
	"strings"

	"github.com/aws/amazon-ssm-agent/agent/jsonutil"
	"github.com/aws/amazon-ssm-agent/agent/log"
	"gopkg.in/yaml.v2"
)

// extractSSMParameters extracts parameters of the format {{ssm:*}} from input
func extractSSMParameters(log log.T, input interface{}, validSSMParam *regexp.Regexp) []string {
	switch input := input.(type) {
	case string:
		// find all matching strings
		ssmParams := validSSMParam.FindAllString(input, -1)
		return ssmParams

	case []string:
		ssmParams := []string{}
		// for slices, recursively replace parameters on each element of the slice
		for _, v := range input {
			ssmParams = append(ssmParams, extractSSMParameters(log, v, validSSMParam)...)
		}
		return ssmParams

	case []interface{}:
		ssmParams := []string{}
		// for slices, recursively replace parameters on each element of the slice
		for _, v := range input {
			ssmParams = append(ssmParams, extractSSMParameters(log, v, validSSMParam)...)
		}
		return ssmParams

	case []map[string]interface{}:
		ssmParams := []string{}
		// this case is not caught by the one above because map cannot be converted to interface{}
		for _, v := range input {
			ssmParams = append(ssmParams, extractSSMParameters(log, v, validSSMParam)...)
		}
		return ssmParams

	case map[string]interface{}:
		ssmParams := []string{}
		// for maps, recursively replace parameters on each value in the map
		for _, v := range input {
			ssmParams = append(ssmParams, extractSSMParameters(log, v, validSSMParam)...)
		}
		return ssmParams

	default:
		// return empty string array
		return []string{}
	}
}

// replaceSSMParameters replaces parameters of the format {{ssm:*}} with their actual values
func replaceSSMParameters(log log.T, input interface{}, ssmParameters map[string]Parameter) (interface{}, error) {
	switch input := input.(type) {
	case string:
		// replace param names with actual values
		for paramName, paramObj := range ssmParameters {
			if paramObj.Type == ParamTypeStringList && strings.Contains(input, paramName) {
				return nil, fmt.Errorf("Parameter %v of type %v cannot be used as a %v", paramObj.Name, paramObj.Type, ParamTypeString)
			}

			input = strings.Replace(input, paramName, paramObj.Value, -1)
		}
		return input, nil

	case []string:
		out, err := parseStringList(log, input, ssmParameters)
		if err != nil {
			return nil, err
		}
		return out, nil

	case []interface{}:
		// return the original input if it is an empty array.
		if len(input) == 0 {
			return input, nil
		}

		switch input[0].(type) {
		case string:
			out, err := parseStringList(log, input, ssmParameters)
			if err != nil {
				return nil, err
			}
			return out, nil

		default:
			var err error
			// for slices, recursively replace parameters on each element of the slice
			out := make([]interface{}, len(input))
			for i, v := range input {
				out[i], err = replaceSSMParameters(log, v, ssmParameters)
				if err != nil {
					return nil, err
				}
			}
			return out, nil
		}

	case []map[string]interface{}:
		// this case is not caught by the one above because map cannot be converted to interface{}
		out := make([]map[string]interface{}, len(input))
		for i, v := range input {
			temp, err := replaceSSMParameters(log, v, ssmParameters)
			if err != nil {
				return nil, err
			}
			out[i] = temp.(map[string]interface{})
		}
		return out, nil

	case map[string]interface{}:
		var err error
		// for maps, recursively replace parameters on each value in the map
		out := make(map[string]interface{})
		for k, v := range input {
			out[k], err = replaceSSMParameters(log, v, ssmParameters)
			if err != nil {
				return nil, err
			}
		}
		return out, nil

	case map[interface{}]interface{}:
		var err error
		out := make(map[string]interface{})
		for k, v := range input {
			switch k := k.(type) {
			case string:
				out[k], err = replaceSSMParameters(log, v, ssmParameters)
				if err != nil {
					return nil, err
				}
			default:
				return nil, errors.New("Unrecognized parameter type")
			}
		}
		return out, nil

	default:
		// any other type, return as is
		return input, nil
	}
}

func parseStringList(log log.T, input interface{}, ssmParameters map[string]Parameter) (interface{}, error) {
	/*
		This method parses the input and replaces ssm parameters of the format {{ssm:*}} with their
		actual values. It includes a special case where if the ssm parameter is of type StringList then
		split it into a string array and return.

		Sample:

		SSM parameter
		For {{ssm:commands}} = "ls,date,dir" output will be
		ls
		date
		dir

		For {{ssm:commands}} = "'ls,echo a',date,dir" output will be
		'ls
		echo a'
		date
		dir

		Irrespective of other special characters in the string, StringList type SSM Parameters will always be
		split on comma.
	*/

	var reformatInput []string
	err := jsonutil.Remarshal(input, &reformatInput)
	if err != nil {
		log.Debug(err)
		return nil, fmt.Errorf("%v", ErrorMsg)
	}

	if len(reformatInput) == 0 {
		// Parameter may be of yaml type. Try to remarshal using yaml
		yamlBytes, err := yaml.Marshal(input)
		if err != nil {
			log.Debug(err)
			return nil, fmt.Errorf("%v", ErrorMsg)
		}
		if err = yaml.Unmarshal(yamlBytes, &reformatInput); err != nil {
			log.Debug(err)
			return nil, fmt.Errorf("%v", ErrorMsg)
		}
	}

	out := []string{}
	for _, value := range reformatInput {
		temp := value
		found := false
		for paramName, paramObj := range ssmParameters {
			if paramObj.Type == ParamTypeStringList {
				// Check if the temp string contains only one SSM parameter element of type StringList
				if strings.Compare(paramName, strings.TrimSpace(temp)) == 0 {
					stringListValue := strings.Split(paramObj.Value, StringListDelimiter)
					out = append(out, stringListValue...)
					found = true
					break
				}

				// Check if SSM parameter of type StringList is being used as a String
				if strings.Contains(temp, paramName) {
					return nil, fmt.Errorf("Parameter %v of type %v cannot be used as a %v", paramObj.Name, paramObj.Type, ParamTypeString)
				}
			} else {
				temp = strings.Replace(temp, paramName, paramObj.Value, -1)
			}
		}

		// If value not found then add the string as it is
		if !found {
			out = append(out, temp)
		}
	}
	return out, nil
}
