package interpolate

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"text/template"
	"time"

	consulapi "github.com/hashicorp/consul/api"
	"github.com/hashicorp/packer/common/uuid"
	"github.com/hashicorp/packer/helper/common"
	awssmapi "github.com/hashicorp/packer/template/interpolate/aws/secretsmanager"
	"github.com/hashicorp/packer/version"
	vaultapi "github.com/hashicorp/vault/api"
	strftime "github.com/jehiah/go-strftime"
)

// InitTime is the UTC time when this package was initialized. It is
// used as the timestamp for all configuration templates so that they
// match for a single build.
var InitTime time.Time

func init() {
	InitTime = time.Now().UTC()
}

// Funcs are the interpolation funcs that are available within interpolations.
var FuncGens = map[string]interface{}{
	"build_name":         funcGenBuildName,
	"build_type":         funcGenBuildType,
	"env":                funcGenEnv,
	"isotime":            funcGenIsotime,
	"strftime":           funcGenStrftime,
	"pwd":                funcGenPwd,
	"split":              funcGenSplitter,
	"template_dir":       funcGenTemplateDir,
	"timestamp":          funcGenTimestamp,
	"uuid":               funcGenUuid,
	"user":               funcGenUser,
	"packer_version":     funcGenPackerVersion,
	"consul_key":         funcGenConsul,
	"vault":              funcGenVault,
	"sed":                funcGenSed,
	"build":              funcGenBuild,
	"aws_secretsmanager": funcGenAwsSecrets,

	"replace":     replace,
	"replace_all": replace_all,

	"upper": strings.ToUpper,
	"lower": strings.ToLower,
}

var ErrVariableNotSetString = "Error: variable not set:"

// FuncGenerator is a function that given a context generates a template
// function for the template.
type FuncGenerator func(*Context) interface{}

// Funcs returns the functions that can be used for interpolation given
// a context.
func Funcs(ctx *Context) template.FuncMap {
	result := make(map[string]interface{})
	for k, v := range FuncGens {
		switch v := v.(type) {
		case func(*Context) interface{}:
			result[k] = v(ctx)
		default:
			result[k] = v
		}
	}
	if ctx != nil {
		for k, v := range ctx.Funcs {
			result[k] = v
		}
	}

	return template.FuncMap(result)
}

func funcGenSplitter(ctx *Context) interface{} {
	return func(k string, s string, i int) (string, error) {
		// return func(s string) (string, error) {
		split := strings.Split(k, s)
		if len(split) <= i {
			return "", fmt.Errorf("the substring %d was unavailable using the separator value, %s, only %d values were found", i, s, len(split))
		}
		return split[i], nil
	}
}

func funcGenBuildName(ctx *Context) interface{} {
	return func() (string, error) {
		if ctx == nil || ctx.BuildName == "" {
			return "", errors.New("build_name not available")
		}

		return ctx.BuildName, nil
	}
}

func funcGenBuildType(ctx *Context) interface{} {
	return func() (string, error) {
		if ctx == nil || ctx.BuildType == "" {
			return "", errors.New("build_type not available")
		}

		return ctx.BuildType, nil
	}
}

func funcGenEnv(ctx *Context) interface{} {
	return func(k string) (string, error) {
		if !ctx.EnableEnv {
			// The error message doesn't have to be that detailed since
			// semantic checks should catch this.
			return "", errors.New("env vars are not allowed here")
		}

		return os.Getenv(k), nil
	}
}

func funcGenIsotime(ctx *Context) interface{} {
	return func(format ...string) (string, error) {
		if len(format) == 0 {
			return InitTime.Format(time.RFC3339), nil
		}

		if len(format) > 1 {
			return "", fmt.Errorf("too many values, 1 needed: %v", format)
		}

		return InitTime.Format(format[0]), nil
	}
}

func funcGenStrftime(ctx *Context) interface{} {
	return func(format string) string {
		return strftime.Format(format, InitTime)
	}
}

func funcGenPwd(ctx *Context) interface{} {
	return func() (string, error) {
		return os.Getwd()
	}
}

func funcGenTemplateDir(ctx *Context) interface{} {
	return func() (string, error) {
		if ctx == nil || ctx.TemplatePath == "" {
			return "", errors.New("template path not available")
		}

		path, err := filepath.Abs(filepath.Dir(ctx.TemplatePath))
		if err != nil {
			return "", err
		}

		return path, nil
	}
}

func passthroughOrInterpolate(data map[interface{}]interface{}, s string) (string, error) {
	if heldPlace, ok := data[s]; ok {
		if hp, ok := heldPlace.(string); ok {
			// If we're in the first interpolation pass, the goal is to
			// make sure that we pass the value through.
			// TODO match against an actual string constant
			if strings.Contains(hp, common.PlaceholderMsg) {
				return fmt.Sprintf("{{.%s}}", s), nil
			} else {
				return hp, nil
			}
		}
	}
	return "", fmt.Errorf("loaded data, but couldnt find %s in it.", s)

}
func funcGenBuild(ctx *Context) interface{} {
	// Depending on where the context data is coming from, it could take a few
	// different map types. The following switch standardizes the map types
	// so we can act on them correctly.
	return func(s string) (string, error) {
		switch data := ctx.Data.(type) {
		case map[interface{}]interface{}:
			return passthroughOrInterpolate(data, s)
		case map[string]interface{}:
			// convert to a map[interface{}]interface{} so we can use same
			// parsing on it
			passed := make(map[interface{}]interface{}, len(data))
			for k, v := range data {
				passed[k] = v
			}
			return passthroughOrInterpolate(passed, s)
		case map[string]string:
			// convert to a map[interface{}]interface{} so we can use same
			// parsing on it
			passed := make(map[interface{}]interface{}, len(data))
			for k, v := range data {
				passed[k] = v
			}
			return passthroughOrInterpolate(passed, s)
		default:
			return "", fmt.Errorf("Error validating build variable: the given "+
				"variable %s will not be passed into your plugin.", s)
		}
	}
}

func funcGenTimestamp(ctx *Context) interface{} {
	return func() string {
		return strconv.FormatInt(InitTime.Unix(), 10)
	}
}

func funcGenUser(ctx *Context) interface{} {
	return func(k string) (string, error) {
		if ctx == nil || ctx.UserVariables == nil {
			return "", errors.New("test")
		}

		val, ok := ctx.UserVariables[k]
		if ctx.EnableEnv {
			// error and retry if we're interpolating UserVariables. But if
			// we're elsewhere in the template, just return the empty string.
			if !ok {
				return "", fmt.Errorf("%s %s", ErrVariableNotSetString, k)
			}
		}
		return val, nil
	}
}

func funcGenUuid(ctx *Context) interface{} {
	return func() string {
		return uuid.TimeOrderedUUID()
	}
}

func funcGenPackerVersion(ctx *Context) interface{} {
	return func() string {
		return version.FormattedVersion()
	}
}

func funcGenConsul(ctx *Context) interface{} {
	return func(k string) (string, error) {
		if !ctx.EnableEnv {
			// The error message doesn't have to be that detailed since
			// semantic checks should catch this.
			return "", errors.New("consul_key is not allowed here")
		}

		consulConfig := consulapi.DefaultConfig()
		client, err := consulapi.NewClient(consulConfig)
		if err != nil {
			return "", fmt.Errorf("error getting consul client: %s", err)
		}

		q := &consulapi.QueryOptions{}
		kv, _, err := client.KV().Get(k, q)
		if err != nil {
			return "", fmt.Errorf("error reading consul key: %s", err)
		}
		if kv == nil {
			return "", fmt.Errorf("key does not exist at the given path: %s", k)
		}

		value := string(kv.Value)
		if value == "" {
			return "", fmt.Errorf("value is empty at path %s", k)
		}

		return value, nil
	}
}

func funcGenVault(ctx *Context) interface{} {
	return func(path string, key string) (string, error) {
		// Only allow interpolation from Vault when env vars are being read.
		if !ctx.EnableEnv {
			// The error message doesn't have to be that detailed since
			// semantic checks should catch this.
			return "", errors.New("Vault vars are only allowed in the variables section")
		}
		if token := os.Getenv("VAULT_TOKEN"); token == "" {
			return "", errors.New("Must set VAULT_TOKEN env var in order to " +
				"use vault template function")
		}
		// const EnvVaultAddress = "VAULT_ADDR"
		// const EnvVaultToken = "VAULT_TOKEN"
		vaultConfig := vaultapi.DefaultConfig()
		cli, err := vaultapi.NewClient(vaultConfig)
		if err != nil {
			return "", fmt.Errorf("Error getting Vault client: %s", err)
		}
		secret, err := cli.Logical().Read(path)
		if err != nil {
			return "", fmt.Errorf("Error reading vault secret: %s", err)
		}
		if secret == nil {
			return "", errors.New("Vault Secret does not exist at the given path")
		}

		data, ok := secret.Data["data"]
		if !ok {
			// maybe ths is v1, not v2 kv store
			value, ok := secret.Data[key]
			if ok {
				return value.(string), nil
			}

			// neither v1 nor v2 proudced a valid value
			return "", fmt.Errorf("Vault data was empty at the "+
				"given path. Warnings: %s", strings.Join(secret.Warnings, "; "))
		}

		value := data.(map[string]interface{})[key].(string)
		return value, nil
	}
}

func funcGenAwsSecrets(ctx *Context) interface{} {
	return func(secret ...string) (string, error) {
		if !ctx.EnableEnv {
			// The error message doesn't have to be that detailed since
			// semantic checks should catch this.
			return "", errors.New("AWS Secrets Manager vars are only allowed in the variables section")
		}

		// Check if at least 1 parameter has been used
		if len(secret) == 0 {
			return "", errors.New("At least one parameter must be used")
		}
		// client uses AWS SDK CredentialChain method. So,credentials can
		// be loaded from credential file, environment variables, or IAM
		// roles.
		client := awssmapi.New(
			&awssmapi.AWSConfig{},
		)

		var name, key string
		name = secret[0]
		// key is optional if not used we fetch the first
		// value stored in given secret. If more than two parameters
		// are passed we take second param and ignore the others
		if len(secret) > 1 {
			key = secret[1]
		}

		spec := &awssmapi.SecretSpec{
			Name: name,
			Key:  key,
		}

		s, err := client.GetSecret(spec)
		if err != nil {
			return "", fmt.Errorf("Error getting secret: %s", err)
		}
		return s, nil
	}
}

func funcGenSed(ctx *Context) interface{} {
	return func(expression string, inputString string) (string, error) {
		return "", errors.New("template function `sed` is deprecated " +
			"use `replace` or `replace_all` instead." +
			"Documentation: https://www.packer.io/docs/templates/engine")
	}
}

func replace_all(old, new, src string) string {
	return strings.ReplaceAll(src, old, new)
}

func replace(old, new string, n int, src string) string {
	return strings.Replace(src, old, new, n)
}
