// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package config

import (
	"context"
	"errors"
	"fmt"
	"io"
	"net"
	"os"
	"path/filepath"
	"strings"
	"syscall"
	"time"

	ctconfig "github.com/hashicorp/consul-template/config"
	ctsignals "github.com/hashicorp/consul-template/signals"
	"github.com/hashicorp/go-multierror"
	"github.com/hashicorp/go-secure-stdlib/parseutil"
	"github.com/hashicorp/hcl"
	"github.com/hashicorp/hcl/hcl/ast"
	"github.com/mitchellh/mapstructure"
	"k8s.io/utils/strings/slices"

	"github.com/hashicorp/vault/command/agentproxyshared"
	"github.com/hashicorp/vault/helper/namespace"
	"github.com/hashicorp/vault/internalshared/configutil"
	"github.com/hashicorp/vault/sdk/helper/pointerutil"
)

// Config is the configuration for Vault Agent.
type Config struct {
	*configutil.SharedConfig `hcl:"-"`

	AutoAuth                    *AutoAuth                  `hcl:"auto_auth"`
	ExitAfterAuth               bool                       `hcl:"exit_after_auth"`
	Cache                       *Cache                     `hcl:"cache"`
	APIProxy                    *APIProxy                  `hcl:"api_proxy"`
	Vault                       *Vault                     `hcl:"vault"`
	TemplateConfig              *TemplateConfig            `hcl:"template_config"`
	Templates                   []*ctconfig.TemplateConfig `hcl:"templates"`
	DisableIdleConns            []string                   `hcl:"disable_idle_connections"`
	DisableIdleConnsAPIProxy    bool                       `hcl:"-"`
	DisableIdleConnsTemplating  bool                       `hcl:"-"`
	DisableIdleConnsAutoAuth    bool                       `hcl:"-"`
	DisableKeepAlives           []string                   `hcl:"disable_keep_alives"`
	DisableKeepAlivesAPIProxy   bool                       `hcl:"-"`
	DisableKeepAlivesTemplating bool                       `hcl:"-"`
	DisableKeepAlivesAutoAuth   bool                       `hcl:"-"`
	Exec                        *ExecConfig                `hcl:"exec,optional"`
	EnvTemplates                []*ctconfig.TemplateConfig `hcl:"env_template,optional"`
}

const (
	DisableIdleConnsEnv  = "VAULT_AGENT_DISABLE_IDLE_CONNECTIONS"
	DisableKeepAlivesEnv = "VAULT_AGENT_DISABLE_KEEP_ALIVES"
)

func (c *Config) Prune() {
	for _, l := range c.Listeners {
		l.RawConfig = nil
		l.Profiling.UnusedKeys = nil
		l.Telemetry.UnusedKeys = nil
		l.CustomResponseHeaders = nil
	}
	c.FoundKeys = nil
	c.UnusedKeys = nil
	c.SharedConfig.FoundKeys = nil
	c.SharedConfig.UnusedKeys = nil
	if c.Telemetry != nil {
		c.Telemetry.FoundKeys = nil
		c.Telemetry.UnusedKeys = nil
	}
}

type Retry struct {
	NumRetries int `hcl:"num_retries"`
}

// Vault contains configuration for connecting to Vault servers
type Vault struct {
	Address          string      `hcl:"address"`
	CACert           string      `hcl:"ca_cert"`
	CAPath           string      `hcl:"ca_path"`
	TLSSkipVerify    bool        `hcl:"-"`
	TLSSkipVerifyRaw interface{} `hcl:"tls_skip_verify"`
	ClientCert       string      `hcl:"client_cert"`
	ClientKey        string      `hcl:"client_key"`
	TLSServerName    string      `hcl:"tls_server_name"`
	Retry            *Retry      `hcl:"retry"`
}

// transportDialer is an interface that allows passing a custom dialer function
// to an HTTP client's transport config
type transportDialer interface {
	// Dial is intended to match https://pkg.go.dev/net#Dialer.Dial
	Dial(network, address string) (net.Conn, error)

	// DialContext is intended to match https://pkg.go.dev/net#Dialer.DialContext
	DialContext(ctx context.Context, network, address string) (net.Conn, error)
}

// APIProxy contains any configuration needed for proxy mode
type APIProxy struct {
	UseAutoAuthTokenRaw interface{} `hcl:"use_auto_auth_token"`
	UseAutoAuthToken    bool        `hcl:"-"`
	ForceAutoAuthToken  bool        `hcl:"-"`
	EnforceConsistency  string      `hcl:"enforce_consistency"`
	WhenInconsistent    string      `hcl:"when_inconsistent"`
}

// Cache contains any configuration needed for Cache mode
type Cache struct {
	UseAutoAuthTokenRaw interface{}                     `hcl:"use_auto_auth_token"`
	UseAutoAuthToken    bool                            `hcl:"-"`
	ForceAutoAuthToken  bool                            `hcl:"-"`
	EnforceConsistency  string                          `hcl:"enforce_consistency"`
	WhenInconsistent    string                          `hcl:"when_inconsistent"`
	Persist             *agentproxyshared.PersistConfig `hcl:"persist"`
	InProcDialer        transportDialer                 `hcl:"-"`
}

// AutoAuth is the configured authentication method and sinks
type AutoAuth struct {
	Method *Method `hcl:"-"`
	Sinks  []*Sink `hcl:"sinks"`

	// NOTE: This is unsupported outside of testing and may disappear at any
	// time.
	EnableReauthOnNewCredentials bool `hcl:"enable_reauth_on_new_credentials"`
}

// Method represents the configuration for the authentication backend
type Method struct {
	Type          string
	MountPath     string        `hcl:"mount_path"`
	WrapTTLRaw    interface{}   `hcl:"wrap_ttl"`
	WrapTTL       time.Duration `hcl:"-"`
	MinBackoffRaw interface{}   `hcl:"min_backoff"`
	MinBackoff    time.Duration `hcl:"-"`
	MaxBackoffRaw interface{}   `hcl:"max_backoff"`
	MaxBackoff    time.Duration `hcl:"-"`
	Namespace     string        `hcl:"namespace"`
	ExitOnError   bool          `hcl:"exit_on_err"`
	Config        map[string]interface{}
}

// Sink defines a location to write the authenticated token
type Sink struct {
	Type       string
	WrapTTLRaw interface{}   `hcl:"wrap_ttl"`
	WrapTTL    time.Duration `hcl:"-"`
	DHType     string        `hcl:"dh_type"`
	DeriveKey  bool          `hcl:"derive_key"`
	DHPath     string        `hcl:"dh_path"`
	AAD        string        `hcl:"aad"`
	AADEnvVar  string        `hcl:"aad_env_var"`
	Config     map[string]interface{}
}

// TemplateConfig defines global behaviors around template
type TemplateConfig struct {
	ExitOnRetryFailure       bool          `hcl:"exit_on_retry_failure"`
	StaticSecretRenderIntRaw interface{}   `hcl:"static_secret_render_interval"`
	StaticSecretRenderInt    time.Duration `hcl:"-"`
}

type ExecConfig struct {
	Command                []string  `hcl:"command,attr" mapstructure:"command"`
	RestartOnSecretChanges string    `hcl:"restart_on_secret_changes,optional" mapstructure:"restart_on_secret_changes"`
	RestartStopSignal      os.Signal `hcl:"-" mapstructure:"restart_stop_signal"`
}

func NewConfig() *Config {
	return &Config{
		SharedConfig: new(configutil.SharedConfig),
	}
}

// Merge merges two Agent configurations.
func (c *Config) Merge(c2 *Config) *Config {
	if c2 == nil {
		return c
	}

	result := NewConfig()

	result.SharedConfig = c.SharedConfig
	if c2.SharedConfig != nil {
		result.SharedConfig = c.SharedConfig.Merge(c2.SharedConfig)
	}

	result.AutoAuth = c.AutoAuth
	if c2.AutoAuth != nil {
		result.AutoAuth = c2.AutoAuth
	}

	result.Cache = c.Cache
	if c2.Cache != nil {
		result.Cache = c2.Cache
	}

	result.APIProxy = c.APIProxy
	if c2.APIProxy != nil {
		result.APIProxy = c2.APIProxy
	}

	result.DisableMlock = c.DisableMlock
	if c2.DisableMlock {
		result.DisableMlock = c2.DisableMlock
	}

	// For these, ignore the non-specific one and overwrite them all
	result.DisableIdleConnsAutoAuth = c.DisableIdleConnsAutoAuth
	if c2.DisableIdleConnsAutoAuth {
		result.DisableIdleConnsAutoAuth = c2.DisableIdleConnsAutoAuth
	}

	result.DisableIdleConnsAPIProxy = c.DisableIdleConnsAPIProxy
	if c2.DisableIdleConnsAPIProxy {
		result.DisableIdleConnsAPIProxy = c2.DisableIdleConnsAPIProxy
	}

	result.DisableIdleConnsTemplating = c.DisableIdleConnsTemplating
	if c2.DisableIdleConnsTemplating {
		result.DisableIdleConnsTemplating = c2.DisableIdleConnsTemplating
	}

	result.DisableKeepAlivesAutoAuth = c.DisableKeepAlivesAutoAuth
	if c2.DisableKeepAlivesAutoAuth {
		result.DisableKeepAlivesAutoAuth = c2.DisableKeepAlivesAutoAuth
	}

	result.DisableKeepAlivesAPIProxy = c.DisableKeepAlivesAPIProxy
	if c2.DisableKeepAlivesAPIProxy {
		result.DisableKeepAlivesAPIProxy = c2.DisableKeepAlivesAPIProxy
	}

	result.DisableKeepAlivesTemplating = c.DisableKeepAlivesTemplating
	if c2.DisableKeepAlivesTemplating {
		result.DisableKeepAlivesTemplating = c2.DisableKeepAlivesTemplating
	}

	result.TemplateConfig = c.TemplateConfig
	if c2.TemplateConfig != nil {
		result.TemplateConfig = c2.TemplateConfig
	}

	for _, l := range c.Templates {
		result.Templates = append(result.Templates, l)
	}
	for _, l := range c2.Templates {
		result.Templates = append(result.Templates, l)
	}

	result.ExitAfterAuth = c.ExitAfterAuth
	if c2.ExitAfterAuth {
		result.ExitAfterAuth = c2.ExitAfterAuth
	}

	result.Vault = c.Vault
	if c2.Vault != nil {
		result.Vault = c2.Vault
	}

	result.PidFile = c.PidFile
	if c2.PidFile != "" {
		result.PidFile = c2.PidFile
	}

	result.Exec = c.Exec
	if c2.Exec != nil {
		result.Exec = c2.Exec
	}

	for _, envTmpl := range c.EnvTemplates {
		result.EnvTemplates = append(result.EnvTemplates, envTmpl)
	}

	for _, envTmpl := range c2.EnvTemplates {
		result.EnvTemplates = append(result.EnvTemplates, envTmpl)
	}

	return result
}

// IsDefaultListerDefined returns true if a default listener has been defined
// in this config
func (c *Config) IsDefaultListerDefined() bool {
	for _, l := range c.Listeners {
		if l.Role != "metrics_only" {
			return true
		}
	}
	return false
}

// ValidateConfig validates an Agent configuration after it has been fully merged together, to
// ensure that required combinations of configs are there
func (c *Config) ValidateConfig() error {
	if c.APIProxy != nil && c.Cache != nil {
		if c.Cache.UseAutoAuthTokenRaw != nil {
			if c.APIProxy.UseAutoAuthTokenRaw != nil {
				return fmt.Errorf("use_auto_auth_token defined in both api_proxy and cache config. Please remove this configuration from the cache block")
			} else {
				c.APIProxy.ForceAutoAuthToken = c.Cache.ForceAutoAuthToken
			}
		}
	}

	if c.Cache != nil {
		if len(c.Listeners) < 1 && len(c.Templates) < 1 && len(c.EnvTemplates) < 1 {
			return fmt.Errorf("enabling the cache requires at least 1 template or 1 listener to be defined")
		}

		if c.Cache.UseAutoAuthToken {
			if c.AutoAuth == nil {
				return fmt.Errorf("cache.use_auto_auth_token is true but auto_auth not configured")
			}
			if c.AutoAuth != nil && c.AutoAuth.Method != nil && c.AutoAuth.Method.WrapTTL > 0 {
				return fmt.Errorf("cache.use_auto_auth_token is true and auto_auth uses wrapping")
			}
		}
	}

	if c.APIProxy != nil {
		if len(c.Listeners) < 1 {
			return fmt.Errorf("configuring the api_proxy requires at least 1 listener to be defined")
		}

		if c.APIProxy.UseAutoAuthToken {
			if c.AutoAuth == nil {
				return fmt.Errorf("api_proxy.use_auto_auth_token is true but auto_auth not configured")
			}
			if c.AutoAuth != nil && c.AutoAuth.Method != nil && c.AutoAuth.Method.WrapTTL > 0 {
				return fmt.Errorf("api_proxy.use_auto_auth_token is true and auto_auth uses wrapping")
			}
		}
	}

	if c.AutoAuth != nil {
		if len(c.AutoAuth.Sinks) == 0 &&
			(c.APIProxy == nil || !c.APIProxy.UseAutoAuthToken) &&
			len(c.Templates) == 0 &&
			len(c.EnvTemplates) == 0 {
			return fmt.Errorf("auto_auth requires at least one sink or at least one template or api_proxy.use_auto_auth_token=true")
		}
	}

	if c.AutoAuth == nil && c.Cache == nil && len(c.Listeners) == 0 {
		return fmt.Errorf("no auto_auth, cache, or listener block found in config")
	}

	return c.validateEnvTemplateConfig()
}

func (c *Config) validateEnvTemplateConfig() error {
	// if we are not in env-template mode, exit early
	if c.Exec == nil && len(c.EnvTemplates) == 0 {
		return nil
	}

	if c.Exec == nil {
		return fmt.Errorf("a top-level 'exec' element must be specified with 'env_template' entries")
	}

	if len(c.EnvTemplates) == 0 {
		return fmt.Errorf("must specify at least one 'env_template' element with a top-level 'exec' element")
	}

	if c.APIProxy != nil {
		return fmt.Errorf("'api_proxy' cannot be specified with 'env_template' entries")
	}

	if len(c.Templates) > 0 {
		return fmt.Errorf("'template' cannot be specified with 'env_template' entries")
	}

	if len(c.Exec.Command) == 0 {
		return fmt.Errorf("'exec' requires a non-empty 'command' field")
	}

	if !slices.Contains([]string{"always", "never"}, c.Exec.RestartOnSecretChanges) {
		return fmt.Errorf("'exec.restart_on_secret_changes' unexpected value: %q", c.Exec.RestartOnSecretChanges)
	}

	uniqueKeys := make(map[string]struct{})

	for _, template := range c.EnvTemplates {
		// Required:
		//   - the key (environment variable name)
		//   - either "contents" or "source"
		// Optional / permitted:
		//   - error_on_missing_key
		//   - error_fatal
		//   - left_delimiter
		//   - right_delimiter
		//   - ExtFuncMap
		//   - function_denylist / function_blacklist

		if template.MapToEnvironmentVariable == nil {
			return fmt.Errorf("env_template: an environment variable name is required")
		}

		key := *template.MapToEnvironmentVariable

		if _, exists := uniqueKeys[key]; exists {
			return fmt.Errorf("env_template: duplicate environment variable name: %q", key)
		}

		uniqueKeys[key] = struct{}{}

		if template.Contents == nil && template.Source == nil {
			return fmt.Errorf("env_template[%s]: either 'contents' or 'source' must be specified", key)
		}

		if template.Contents != nil && template.Source != nil {
			return fmt.Errorf("env_template[%s]: 'contents' and 'source' cannot be specified together", key)
		}

		if template.Backup != nil {
			return fmt.Errorf("env_template[%s]: 'backup' is not allowed", key)
		}

		if template.Command != nil {
			return fmt.Errorf("env_template[%s]: 'command' is not allowed", key)
		}

		if template.CommandTimeout != nil {
			return fmt.Errorf("env_template[%s]: 'command_timeout' is not allowed", key)
		}

		if template.CreateDestDirs != nil {
			return fmt.Errorf("env_template[%s]: 'create_dest_dirs' is not allowed", key)
		}

		if template.Destination != nil {
			return fmt.Errorf("env_template[%s]: 'destination' is not allowed", key)
		}

		if template.Exec != nil {
			return fmt.Errorf("env_template[%s]: 'exec' is not allowed", key)
		}

		if template.Perms != nil {
			return fmt.Errorf("env_template[%s]: 'perms' is not allowed", key)
		}

		if template.User != nil {
			return fmt.Errorf("env_template[%s]: 'user' is not allowed", key)
		}

		if template.Uid != nil {
			return fmt.Errorf("env_template[%s]: 'uid' is not allowed", key)
		}

		if template.Group != nil {
			return fmt.Errorf("env_template[%s]: 'group' is not allowed", key)
		}

		if template.Gid != nil {
			return fmt.Errorf("env_template[%s]: 'gid' is not allowed", key)
		}

		if template.Wait != nil {
			return fmt.Errorf("env_template[%s]: 'wait' is not allowed", key)
		}

		if template.SandboxPath != nil {
			return fmt.Errorf("env_template[%s]: 'sandbox_path' is not allowed", key)
		}
	}

	return nil
}

// LoadConfig loads the configuration at the given path, regardless if
// it's a file or directory.
func LoadConfig(path string) (*Config, error) {
	fi, err := os.Stat(path)
	if err != nil {
		return nil, err
	}

	if fi.IsDir() {
		return LoadConfigDir(path)
	}
	return LoadConfigFile(path)
}

// LoadConfigDir loads the configuration at the given path if it's a directory
func LoadConfigDir(dir string) (*Config, error) {
	f, err := os.Open(dir)
	if err != nil {
		return nil, err
	}
	defer f.Close()

	fi, err := f.Stat()
	if err != nil {
		return nil, err
	}
	if !fi.IsDir() {
		return nil, fmt.Errorf("configuration path must be a directory: %q", dir)
	}

	var files []string
	err = nil
	for err != io.EOF {
		var fis []os.FileInfo
		fis, err = f.Readdir(128)
		if err != nil && err != io.EOF {
			return nil, err
		}

		for _, fi := range fis {
			// Ignore directories
			if fi.IsDir() {
				continue
			}

			// Only care about files that are valid to load.
			name := fi.Name()
			skip := true
			if strings.HasSuffix(name, ".hcl") {
				skip = false
			} else if strings.HasSuffix(name, ".json") {
				skip = false
			}
			if skip || isTemporaryFile(name) {
				continue
			}

			path := filepath.Join(dir, name)
			files = append(files, path)
		}
	}

	result := NewConfig()
	for _, f := range files {
		config, err := LoadConfigFile(f)
		if err != nil {
			return nil, fmt.Errorf("error loading %q: %w", f, err)
		}

		if result == nil {
			result = config
		} else {
			result = result.Merge(config)
		}
	}

	return result, nil
}

// isTemporaryFile returns true or false depending on whether the
// provided file name is a temporary file for the following editors:
// emacs or vim.
func isTemporaryFile(name string) bool {
	return strings.HasSuffix(name, "~") || // vim
		strings.HasPrefix(name, ".#") || // emacs
		(strings.HasPrefix(name, "#") && strings.HasSuffix(name, "#")) // emacs
}

// LoadConfigFile loads the configuration at the given path if it's a file
func LoadConfigFile(path string) (*Config, error) {
	fi, err := os.Stat(path)
	if err != nil {
		return nil, err
	}

	if fi.IsDir() {
		return nil, fmt.Errorf("location is a directory, not a file")
	}

	// Read the file
	d, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}

	// Parse!
	obj, err := hcl.Parse(string(d))
	if err != nil {
		return nil, err
	}

	// Attribute
	ast.Walk(obj, func(n ast.Node) (ast.Node, bool) {
		if k, ok := n.(*ast.ObjectKey); ok {
			k.Token.Pos.Filename = path
		}
		return n, true
	})

	// Start building the result
	result := NewConfig()
	if err := hcl.DecodeObject(result, obj); err != nil {
		return nil, err
	}

	sharedConfig, err := configutil.ParseConfig(string(d))
	if err != nil {
		return nil, err
	}

	// Pruning custom headers for Agent for now
	for _, ln := range sharedConfig.Listeners {
		ln.CustomResponseHeaders = nil
	}

	result.SharedConfig = sharedConfig

	list, ok := obj.Node.(*ast.ObjectList)
	if !ok {
		return nil, fmt.Errorf("error parsing: file doesn't contain a root object")
	}

	if err := parseAutoAuth(result, list); err != nil {
		return nil, fmt.Errorf("error parsing 'auto_auth': %w", err)
	}

	if err := parseCache(result, list); err != nil {
		return nil, fmt.Errorf("error parsing 'cache':%w", err)
	}

	if err := parseAPIProxy(result, list); err != nil {
		return nil, fmt.Errorf("error parsing 'api_proxy':%w", err)
	}

	if err := parseTemplateConfig(result, list); err != nil {
		return nil, fmt.Errorf("error parsing 'template_config': %w", err)
	}

	if err := parseTemplates(result, list); err != nil {
		return nil, fmt.Errorf("error parsing 'template': %w", err)
	}

	if err := parseExec(result, list); err != nil {
		return nil, fmt.Errorf("error parsing 'exec': %w", err)
	}

	if err := parseEnvTemplates(result, list); err != nil {
		return nil, fmt.Errorf("error parsing 'env_template': %w", err)
	}

	if result.Cache != nil && result.APIProxy == nil && (result.Cache.UseAutoAuthToken || result.Cache.ForceAutoAuthToken) {
		result.APIProxy = &APIProxy{
			UseAutoAuthToken:   result.Cache.UseAutoAuthToken,
			ForceAutoAuthToken: result.Cache.ForceAutoAuthToken,
		}
	}

	err = parseVault(result, list)
	if err != nil {
		return nil, fmt.Errorf("error parsing 'vault':%w", err)
	}

	if result.Vault != nil {
		// Set defaults
		if result.Vault.Retry == nil {
			result.Vault.Retry = &Retry{}
		}
		switch result.Vault.Retry.NumRetries {
		case 0:
			result.Vault.Retry.NumRetries = ctconfig.DefaultRetryAttempts
		case -1:
			result.Vault.Retry.NumRetries = 0
		}
	}

	if disableIdleConnsEnv := os.Getenv(DisableIdleConnsEnv); disableIdleConnsEnv != "" {
		result.DisableIdleConns, err = parseutil.ParseCommaStringSlice(strings.ToLower(disableIdleConnsEnv))
		if err != nil {
			return nil, fmt.Errorf("error parsing environment variable %s: %v", DisableIdleConnsEnv, err)
		}
	}

	for _, subsystem := range result.DisableIdleConns {
		switch subsystem {
		case "auto-auth":
			result.DisableIdleConnsAutoAuth = true
		case "caching", "proxying":
			result.DisableIdleConnsAPIProxy = true
		case "templating":
			result.DisableIdleConnsTemplating = true
		case "":
			continue
		default:
			return nil, fmt.Errorf("unknown disable_idle_connections value: %s", subsystem)
		}
	}

	if disableKeepAlivesEnv := os.Getenv(DisableKeepAlivesEnv); disableKeepAlivesEnv != "" {
		result.DisableKeepAlives, err = parseutil.ParseCommaStringSlice(strings.ToLower(disableKeepAlivesEnv))
		if err != nil {
			return nil, fmt.Errorf("error parsing environment variable %s: %v", DisableKeepAlivesEnv, err)
		}
	}

	for _, subsystem := range result.DisableKeepAlives {
		switch subsystem {
		case "auto-auth":
			result.DisableKeepAlivesAutoAuth = true
		case "caching", "proxying":
			result.DisableKeepAlivesAPIProxy = true
		case "templating":
			result.DisableKeepAlivesTemplating = true
		case "":
			continue
		default:
			return nil, fmt.Errorf("unknown disable_keep_alives value: %s", subsystem)
		}
	}

	return result, nil
}

func parseVault(result *Config, list *ast.ObjectList) error {
	name := "vault"

	vaultList := list.Filter(name)
	if len(vaultList.Items) == 0 {
		return nil
	}

	if len(vaultList.Items) > 1 {
		return fmt.Errorf("one and only one %q block is required", name)
	}

	item := vaultList.Items[0]

	var v Vault
	err := hcl.DecodeObject(&v, item.Val)
	if err != nil {
		return err
	}

	if v.TLSSkipVerifyRaw != nil {
		v.TLSSkipVerify, err = parseutil.ParseBool(v.TLSSkipVerifyRaw)
		if err != nil {
			return err
		}
	}

	result.Vault = &v

	subs, ok := item.Val.(*ast.ObjectType)
	if !ok {
		return fmt.Errorf("could not parse %q as an object", name)
	}

	if err := parseRetry(result, subs.List); err != nil {
		return fmt.Errorf("error parsing 'retry': %w", err)
	}

	return nil
}

func parseRetry(result *Config, list *ast.ObjectList) error {
	name := "retry"

	retryList := list.Filter(name)
	if len(retryList.Items) == 0 {
		return nil
	}

	if len(retryList.Items) > 1 {
		return fmt.Errorf("one and only one %q block is required", name)
	}

	item := retryList.Items[0]

	var r Retry
	err := hcl.DecodeObject(&r, item.Val)
	if err != nil {
		return err
	}

	result.Vault.Retry = &r

	return nil
}

func parseAPIProxy(result *Config, list *ast.ObjectList) error {
	name := "api_proxy"

	apiProxyList := list.Filter(name)
	if len(apiProxyList.Items) == 0 {
		return nil
	}

	if len(apiProxyList.Items) > 1 {
		return fmt.Errorf("one and only one %q block is required", name)
	}

	item := apiProxyList.Items[0]

	var apiProxy APIProxy
	err := hcl.DecodeObject(&apiProxy, item.Val)
	if err != nil {
		return err
	}

	if apiProxy.UseAutoAuthTokenRaw != nil {
		apiProxy.UseAutoAuthToken, err = parseutil.ParseBool(apiProxy.UseAutoAuthTokenRaw)
		if err != nil {
			// Could be a value of "force" instead of "true"/"false"
			switch apiProxy.UseAutoAuthTokenRaw.(type) {
			case string:
				v := apiProxy.UseAutoAuthTokenRaw.(string)

				if !strings.EqualFold(v, "force") {
					return fmt.Errorf("value of 'use_auto_auth_token' can be either true/false/force, %q is an invalid option", apiProxy.UseAutoAuthTokenRaw)
				}
				apiProxy.UseAutoAuthToken = true
				apiProxy.ForceAutoAuthToken = true

			default:
				return err
			}
		}
	}
	result.APIProxy = &apiProxy

	return nil
}

func parseCache(result *Config, list *ast.ObjectList) error {
	name := "cache"

	cacheList := list.Filter(name)
	if len(cacheList.Items) == 0 {
		return nil
	}

	if len(cacheList.Items) > 1 {
		return fmt.Errorf("one and only one %q block is required", name)
	}

	item := cacheList.Items[0]

	var c Cache
	err := hcl.DecodeObject(&c, item.Val)
	if err != nil {
		return err
	}

	if c.UseAutoAuthTokenRaw != nil {
		c.UseAutoAuthToken, err = parseutil.ParseBool(c.UseAutoAuthTokenRaw)
		if err != nil {
			// Could be a value of "force" instead of "true"/"false"
			switch c.UseAutoAuthTokenRaw.(type) {
			case string:
				v := c.UseAutoAuthTokenRaw.(string)

				if !strings.EqualFold(v, "force") {
					return fmt.Errorf("value of 'use_auto_auth_token' can be either true/false/force, %q is an invalid option", c.UseAutoAuthTokenRaw)
				}
				c.UseAutoAuthToken = true
				c.ForceAutoAuthToken = true

			default:
				return err
			}
		}
	}
	result.Cache = &c

	subs, ok := item.Val.(*ast.ObjectType)
	if !ok {
		return fmt.Errorf("could not parse %q as an object", name)
	}
	subList := subs.List
	if err := parsePersist(result, subList); err != nil {
		return fmt.Errorf("error parsing persist: %w", err)
	}

	return nil
}

func parsePersist(result *Config, list *ast.ObjectList) error {
	name := "persist"

	persistList := list.Filter(name)
	if len(persistList.Items) == 0 {
		return nil
	}

	if len(persistList.Items) > 1 {
		return fmt.Errorf("only one %q block is required", name)
	}

	item := persistList.Items[0]

	var p agentproxyshared.PersistConfig
	err := hcl.DecodeObject(&p, item.Val)
	if err != nil {
		return err
	}

	if p.Type == "" {
		if len(item.Keys) == 1 {
			p.Type = strings.ToLower(item.Keys[0].Token.Value().(string))
		}
		if p.Type == "" {
			return errors.New("persist type must be specified")
		}
	}

	result.Cache.Persist = &p

	return nil
}

func parseAutoAuth(result *Config, list *ast.ObjectList) error {
	name := "auto_auth"

	autoAuthList := list.Filter(name)
	if len(autoAuthList.Items) == 0 {
		return nil
	}
	if len(autoAuthList.Items) > 1 {
		return fmt.Errorf("at most one %q block is allowed", name)
	}

	// Get our item
	item := autoAuthList.Items[0]

	var a AutoAuth
	if err := hcl.DecodeObject(&a, item.Val); err != nil {
		return err
	}

	result.AutoAuth = &a

	subs, ok := item.Val.(*ast.ObjectType)
	if !ok {
		return fmt.Errorf("could not parse %q as an object", name)
	}
	subList := subs.List

	if err := parseMethod(result, subList); err != nil {
		return fmt.Errorf("error parsing 'method': %w", err)
	}
	if a.Method == nil {
		return fmt.Errorf("no 'method' block found")
	}

	if err := parseSinks(result, subList); err != nil {
		return fmt.Errorf("error parsing 'sink' stanzas: %w", err)
	}

	if result.AutoAuth.Method.WrapTTL > 0 {
		if len(result.AutoAuth.Sinks) != 1 {
			return fmt.Errorf("error parsing auto_auth: wrapping enabled on auth method and 0 or many sinks defined")
		}

		if result.AutoAuth.Sinks[0].WrapTTL > 0 {
			return fmt.Errorf("error parsing auto_auth: wrapping enabled both on auth method and sink")
		}
	}

	if result.AutoAuth.Method.MaxBackoffRaw != nil {
		var err error
		if result.AutoAuth.Method.MaxBackoff, err = parseutil.ParseDurationSecond(result.AutoAuth.Method.MaxBackoffRaw); err != nil {
			return err
		}
		result.AutoAuth.Method.MaxBackoffRaw = nil
	}

	if result.AutoAuth.Method.MinBackoffRaw != nil {
		var err error
		if result.AutoAuth.Method.MinBackoff, err = parseutil.ParseDurationSecond(result.AutoAuth.Method.MinBackoffRaw); err != nil {
			return err
		}
		result.AutoAuth.Method.MinBackoffRaw = nil
	}

	return nil
}

func parseMethod(result *Config, list *ast.ObjectList) error {
	name := "method"

	methodList := list.Filter(name)
	if len(methodList.Items) != 1 {
		return fmt.Errorf("one and only one %q block is required", name)
	}

	// Get our item
	item := methodList.Items[0]

	var m Method
	if err := hcl.DecodeObject(&m, item.Val); err != nil {
		return err
	}

	if m.Type == "" {
		if len(item.Keys) == 1 {
			m.Type = strings.ToLower(item.Keys[0].Token.Value().(string))
		}
		if m.Type == "" {
			return errors.New("method type must be specified")
		}
	}

	// Default to Vault's default
	if m.MountPath == "" {
		m.MountPath = fmt.Sprintf("auth/%s", m.Type)
	}
	// Standardize on no trailing slash
	m.MountPath = strings.TrimSuffix(m.MountPath, "/")

	if m.WrapTTLRaw != nil {
		var err error
		if m.WrapTTL, err = parseutil.ParseDurationSecond(m.WrapTTLRaw); err != nil {
			return err
		}
		m.WrapTTLRaw = nil
	}

	// Canonicalize namespace path if provided
	m.Namespace = namespace.Canonicalize(m.Namespace)

	result.AutoAuth.Method = &m
	return nil
}

func parseSinks(result *Config, list *ast.ObjectList) error {
	name := "sink"

	sinkList := list.Filter(name)
	if len(sinkList.Items) < 1 {
		return nil
	}

	var ts []*Sink

	for _, item := range sinkList.Items {
		var s Sink
		if err := hcl.DecodeObject(&s, item.Val); err != nil {
			return err
		}

		if s.Type == "" {
			if len(item.Keys) == 1 {
				s.Type = strings.ToLower(item.Keys[0].Token.Value().(string))
			}
			if s.Type == "" {
				return errors.New("sink type must be specified")
			}
		}

		if s.WrapTTLRaw != nil {
			var err error
			if s.WrapTTL, err = parseutil.ParseDurationSecond(s.WrapTTLRaw); err != nil {
				return multierror.Prefix(err, fmt.Sprintf("sink.%s", s.Type))
			}
			s.WrapTTLRaw = nil
		}

		switch s.DHType {
		case "":
		case "curve25519":
		default:
			return multierror.Prefix(errors.New("invalid value for 'dh_type'"), fmt.Sprintf("sink.%s", s.Type))
		}

		if s.AADEnvVar != "" {
			s.AAD = os.Getenv(s.AADEnvVar)
			s.AADEnvVar = ""
		}

		switch {
		case s.DHPath == "" && s.DHType == "":
			if s.AAD != "" {
				return multierror.Prefix(errors.New("specifying AAD data without 'dh_type' does not make sense"), fmt.Sprintf("sink.%s", s.Type))
			}
			if s.DeriveKey {
				return multierror.Prefix(errors.New("specifying 'derive_key' data without 'dh_type' does not make sense"), fmt.Sprintf("sink.%s", s.Type))
			}
		case s.DHPath != "" && s.DHType != "":
		default:
			return multierror.Prefix(errors.New("'dh_type' and 'dh_path' must be specified together"), fmt.Sprintf("sink.%s", s.Type))
		}

		ts = append(ts, &s)
	}

	result.AutoAuth.Sinks = ts
	return nil
}

func parseTemplateConfig(result *Config, list *ast.ObjectList) error {
	name := "template_config"

	templateConfigList := list.Filter(name)
	if len(templateConfigList.Items) == 0 {
		return nil
	}

	if len(templateConfigList.Items) > 1 {
		return fmt.Errorf("at most one %q block is allowed", name)
	}

	// Get our item
	item := templateConfigList.Items[0]

	var cfg TemplateConfig
	if err := hcl.DecodeObject(&cfg, item.Val); err != nil {
		return err
	}

	result.TemplateConfig = &cfg

	if result.TemplateConfig.StaticSecretRenderIntRaw != nil {
		var err error
		if result.TemplateConfig.StaticSecretRenderInt, err = parseutil.ParseDurationSecond(result.TemplateConfig.StaticSecretRenderIntRaw); err != nil {
			return err
		}
		result.TemplateConfig.StaticSecretRenderIntRaw = nil
	}

	return nil
}

func parseTemplates(result *Config, list *ast.ObjectList) error {
	name := "template"

	templateList := list.Filter(name)
	if len(templateList.Items) < 1 {
		return nil
	}

	var tcs []*ctconfig.TemplateConfig

	for _, item := range templateList.Items {
		var shadow interface{}
		if err := hcl.DecodeObject(&shadow, item.Val); err != nil {
			return fmt.Errorf("error decoding config: %s", err)
		}

		// Convert to a map and flatten the keys we want to flatten
		parsed, ok := shadow.(map[string]interface{})
		if !ok {
			return errors.New("error converting config")
		}

		// flatten the wait or exec fields. The initial "wait" or "exec" value, if given, is a
		// []map[string]interface{}, but we need it to be map[string]interface{}.
		// Consul Template has a method flattenKeys that walks all of parsed and
		// flattens every key. For Vault Agent, we only care about the wait input.
		// Only one wait/exec stanza is supported, however Consul Template does not error
		// with multiple instead it flattens them down, with last value winning.
		// Here we take the last element of the parsed["wait"] or parsed["exec"] slice to keep
		// consistency with Consul Template behavior.
		wait, ok := parsed["wait"].([]map[string]interface{})
		if ok {
			parsed["wait"] = wait[len(wait)-1]
		}

		exec, ok := parsed["exec"].([]map[string]interface{})
		if ok {
			parsed["exec"] = exec[len(exec)-1]
		}

		var tc ctconfig.TemplateConfig

		// Use mapstructure to populate the basic config fields
		var md mapstructure.Metadata
		decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
			DecodeHook: mapstructure.ComposeDecodeHookFunc(
				ctconfig.StringToFileModeFunc(),
				ctconfig.StringToWaitDurationHookFunc(),
				mapstructure.StringToSliceHookFunc(","),
				mapstructure.StringToTimeDurationHookFunc(),
			),
			ErrorUnused: true,
			Metadata:    &md,
			Result:      &tc,
		})
		if err != nil {
			return errors.New("mapstructure decoder creation failed")
		}
		if err := decoder.Decode(parsed); err != nil {
			return err
		}
		tcs = append(tcs, &tc)
	}
	result.Templates = tcs
	return nil
}

func parseExec(result *Config, list *ast.ObjectList) error {
	name := "exec"

	execList := list.Filter(name)
	if len(execList.Items) == 0 {
		return nil
	}

	if len(execList.Items) > 1 {
		return fmt.Errorf("at most one %q block is allowed", name)
	}

	item := execList.Items[0]
	var shadow interface{}
	if err := hcl.DecodeObject(&shadow, item.Val); err != nil {
		return fmt.Errorf("error decoding config: %s", err)
	}

	parsed, ok := shadow.(map[string]interface{})
	if !ok {
		return errors.New("error converting config")
	}

	var execConfig ExecConfig
	var md mapstructure.Metadata
	decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
		DecodeHook: mapstructure.ComposeDecodeHookFunc(
			ctconfig.StringToFileModeFunc(),
			ctconfig.StringToWaitDurationHookFunc(),
			mapstructure.StringToSliceHookFunc(","),
			mapstructure.StringToTimeDurationHookFunc(),
			ctsignals.StringToSignalFunc(),
		),
		ErrorUnused: true,
		Metadata:    &md,
		Result:      &execConfig,
	})
	if err != nil {
		return errors.New("mapstructure decoder creation failed")
	}
	if err := decoder.Decode(parsed); err != nil {
		return err
	}

	// if the user does not specify a restart signal, default to SIGTERM
	if execConfig.RestartStopSignal == nil {
		execConfig.RestartStopSignal = syscall.SIGTERM
	}

	if execConfig.RestartOnSecretChanges == "" {
		execConfig.RestartOnSecretChanges = "always"
	}

	result.Exec = &execConfig
	return nil
}

func parseEnvTemplates(result *Config, list *ast.ObjectList) error {
	name := "env_template"

	envTemplateList := list.Filter(name)

	if len(envTemplateList.Items) < 1 {
		return nil
	}

	envTemplates := make([]*ctconfig.TemplateConfig, 0, len(envTemplateList.Items))

	for _, item := range envTemplateList.Items {
		var shadow interface{}
		if err := hcl.DecodeObject(&shadow, item.Val); err != nil {
			return fmt.Errorf("error decoding config: %s", err)
		}

		// Convert to a map and flatten the keys we want to flatten
		parsed, ok := shadow.(map[string]any)
		if !ok {
			return errors.New("error converting config")
		}

		var templateConfig ctconfig.TemplateConfig
		var md mapstructure.Metadata
		decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
			DecodeHook: mapstructure.ComposeDecodeHookFunc(
				ctconfig.StringToFileModeFunc(),
				ctconfig.StringToWaitDurationHookFunc(),
				mapstructure.StringToSliceHookFunc(","),
				mapstructure.StringToTimeDurationHookFunc(),
				ctsignals.StringToSignalFunc(),
			),
			ErrorUnused: true,
			Metadata:    &md,
			Result:      &templateConfig,
		})
		if err != nil {
			return errors.New("mapstructure decoder creation failed")
		}
		if err := decoder.Decode(parsed); err != nil {
			return err
		}

		// parse the keys in the item for the environment variable name
		if numberOfKeys := len(item.Keys); numberOfKeys != 1 {
			return fmt.Errorf("expected one and only one environment variable name, got %d", numberOfKeys)
		}

		// hcl parses this with extra quotes if quoted in config file
		environmentVariableName := strings.Trim(item.Keys[0].Token.Text, `"`)

		templateConfig.MapToEnvironmentVariable = pointerutil.StringPtr(environmentVariableName)

		envTemplates = append(envTemplates, &templateConfig)
	}

	result.EnvTemplates = envTemplates
	return nil
}
