package config

import (
	"bytes"
	"encoding/gob"
	"errors"
	"strconv"
	"sync"

	hcl2 "github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hil"
	"github.com/hashicorp/hil/ast"
	"github.com/mitchellh/copystructure"
	"github.com/mitchellh/reflectwalk"
)

// RawConfig is a structure that holds a piece of configuration
// where the overall structure is unknown since it will be used
// to configure a plugin or some other similar external component.
//
// RawConfigs can be interpolated with variables that come from
// other resources, user variables, etc.
//
// RawConfig supports a query-like interface to request
// information from deep within the structure.
type RawConfig struct {
	Key string

	// Only _one_ of Raw and Body may be populated at a time.
	//
	// In the normal case, Raw is populated and Body is nil.
	//
	// When the experimental HCL2 parsing mode is enabled, "Body"
	// is populated and RawConfig serves only to transport the hcl2.Body
	// through the rest of Terraform core so we can ultimately decode it
	// once its schema is known.
	//
	// Once we transition to HCL2 as the primary representation, RawConfig
	// should be removed altogether and the hcl2.Body should be passed
	// around directly.

	Raw  map[string]interface{}
	Body hcl2.Body

	Interpolations []ast.Node
	Variables      map[string]InterpolatedVariable

	lock        sync.Mutex
	config      map[string]interface{}
	unknownKeys []string
}

// NewRawConfig creates a new RawConfig structure and populates the
// publicly readable struct fields.
func NewRawConfig(raw map[string]interface{}) (*RawConfig, error) {
	result := &RawConfig{Raw: raw}
	if err := result.init(); err != nil {
		return nil, err
	}

	return result, nil
}

// NewRawConfigHCL2 creates a new RawConfig that is serving as a capsule
// to transport a hcl2.Body. In this mode, the publicly-readable struct
// fields are not populated since all operations should instead be diverted
// to the HCL2 body.
//
// For a RawConfig object constructed with this function, the only valid use
// is to later retrieve the Body value and call its own methods. Callers
// may choose to set and then later handle the Key field, in a manner
// consistent with how it is handled by the Value method, but the Value
// method itself must not be used.
//
// This is an experimental codepath to be used only by the HCL2 config loader.
// Non-experimental parsing should _always_ use NewRawConfig to produce a
// fully-functional RawConfig object.
func NewRawConfigHCL2(body hcl2.Body) *RawConfig {
	return &RawConfig{
		Body: body,
	}
}

// RawMap returns a copy of the RawConfig.Raw map.
func (r *RawConfig) RawMap() map[string]interface{} {
	r.lock.Lock()
	defer r.lock.Unlock()

	m := make(map[string]interface{})
	for k, v := range r.Raw {
		m[k] = v
	}
	return m
}

// Copy returns a copy of this RawConfig, uninterpolated.
func (r *RawConfig) Copy() *RawConfig {
	if r == nil {
		return nil
	}

	r.lock.Lock()
	defer r.lock.Unlock()

	if r.Body != nil {
		return NewRawConfigHCL2(r.Body)
	}

	newRaw := make(map[string]interface{})
	for k, v := range r.Raw {
		newRaw[k] = v
	}

	result, err := NewRawConfig(newRaw)
	if err != nil {
		panic("copy failed: " + err.Error())
	}

	result.Key = r.Key
	return result
}

// Value returns the value of the configuration if this configuration
// has a Key set. If this does not have a Key set, nil will be returned.
func (r *RawConfig) Value() interface{} {
	if c := r.Config(); c != nil {
		if v, ok := c[r.Key]; ok {
			return v
		}
	}

	r.lock.Lock()
	defer r.lock.Unlock()
	return r.Raw[r.Key]
}

// Config returns the entire configuration with the variables
// interpolated from any call to Interpolate.
//
// If any interpolated variables are unknown (value set to
// UnknownVariableValue), the first non-container (map, slice, etc.) element
// will be removed from the config. The keys of unknown variables
// can be found using the UnknownKeys function.
//
// By pruning out unknown keys from the configuration, the raw
// structure will always successfully decode into its ultimate
// structure using something like mapstructure.
func (r *RawConfig) Config() map[string]interface{} {
	r.lock.Lock()
	defer r.lock.Unlock()
	return r.config
}

// Interpolate uses the given mapping of variable values and uses
// those as the values to replace any variables in this raw
// configuration.
//
// Any prior calls to Interpolate are replaced with this one.
//
// If a variable key is missing, this will panic.
func (r *RawConfig) Interpolate(vs map[string]ast.Variable) error {
	r.lock.Lock()
	defer r.lock.Unlock()

	// Create the evaluation configuration we use to execute
	config := &hil.EvalConfig{
		GlobalScope: &ast.BasicScope{
			VarMap: vs,
		},
	}
	return r.interpolate(func(root ast.Node) (interface{}, error) {
		// None of the variables we need are computed, meaning we should
		// be able to properly evaluate.
		result, err := hil.Eval(root, config)
		if err != nil {
			return "", err
		}

		return result.Value, nil
	})
}

// Merge merges another RawConfig into this one (overriding any conflicting
// values in this config) and returns a new config. The original config
// is not modified.
func (r *RawConfig) Merge(other *RawConfig) *RawConfig {
	r.lock.Lock()
	defer r.lock.Unlock()

	// Merge the raw configurations
	raw := make(map[string]interface{})
	for k, v := range r.Raw {
		raw[k] = v
	}
	for k, v := range other.Raw {
		raw[k] = v
	}

	// Create the result
	result, err := NewRawConfig(raw)
	if err != nil {
		panic(err)
	}

	// Merge the interpolated results
	result.config = make(map[string]interface{})
	for k, v := range r.config {
		result.config[k] = v
	}
	for k, v := range other.config {
		result.config[k] = v
	}

	// Build the unknown keys
	if len(r.unknownKeys) > 0 || len(other.unknownKeys) > 0 {
		unknownKeys := make(map[string]struct{})
		for _, k := range r.unknownKeys {
			unknownKeys[k] = struct{}{}
		}
		for _, k := range other.unknownKeys {
			unknownKeys[k] = struct{}{}
		}

		result.unknownKeys = make([]string, 0, len(unknownKeys))
		for k, _ := range unknownKeys {
			result.unknownKeys = append(result.unknownKeys, k)
		}
	}

	return result
}

func (r *RawConfig) init() error {
	r.lock.Lock()
	defer r.lock.Unlock()

	r.config = r.Raw
	r.Interpolations = nil
	r.Variables = nil

	fn := func(node ast.Node) (interface{}, error) {
		r.Interpolations = append(r.Interpolations, node)
		vars, err := DetectVariables(node)
		if err != nil {
			return "", err
		}

		for _, v := range vars {
			if r.Variables == nil {
				r.Variables = make(map[string]InterpolatedVariable)
			}

			r.Variables[v.FullKey()] = v
		}

		return "", nil
	}

	walker := &interpolationWalker{F: fn}
	if err := reflectwalk.Walk(r.Raw, walker); err != nil {
		return err
	}

	return nil
}

func (r *RawConfig) interpolate(fn interpolationWalkerFunc) error {
	if r.Body != nil {
		// For RawConfigs created for the HCL2 experiement, callers must
		// use the HCL2 Body API directly rather than interpolating via
		// the RawConfig.
		return errors.New("this feature is not yet supported under the HCL2 experiment")
	}

	config, err := copystructure.Copy(r.Raw)
	if err != nil {
		return err
	}
	r.config = config.(map[string]interface{})

	w := &interpolationWalker{F: fn, Replace: true}
	err = reflectwalk.Walk(r.config, w)
	if err != nil {
		return err
	}

	r.unknownKeys = w.unknownKeys
	return nil
}

func (r *RawConfig) merge(r2 *RawConfig) *RawConfig {
	if r == nil && r2 == nil {
		return nil
	}

	if r == nil {
		r = &RawConfig{}
	}

	rawRaw, err := copystructure.Copy(r.Raw)
	if err != nil {
		panic(err)
	}

	raw := rawRaw.(map[string]interface{})
	if r2 != nil {
		for k, v := range r2.Raw {
			raw[k] = v
		}
	}

	result, err := NewRawConfig(raw)
	if err != nil {
		panic(err)
	}

	return result
}

// couldBeInteger is a helper that determines if the represented value could
// result in an integer.
//
// This function only works for RawConfigs that have "Key" set, meaning that
// a single result can be produced. Calling this function will overwrite
// the Config and Value results to be a test value.
//
// This function is conservative. If there is some doubt about whether the
// result could be an integer -- for example, if it depends on a variable
// whose type we don't know yet -- it will still return true.
func (r *RawConfig) couldBeInteger() bool {
	if r.Key == "" {
		// un-keyed RawConfigs can never produce numbers
		return false
	}
	if r.Body == nil {
		// Normal path: using the interpolator in this package
		// Interpolate with a fixed number to verify that its a number.
		r.interpolate(func(root ast.Node) (interface{}, error) {
			// Execute the node but transform the AST so that it returns
			// a fixed value of "5" for all interpolations.
			result, err := hil.Eval(
				hil.FixedValueTransform(
					root, &ast.LiteralNode{Value: "5", Typex: ast.TypeString}),
				nil)
			if err != nil {
				return "", err
			}

			return result.Value, nil
		})
		_, err := strconv.ParseInt(r.Value().(string), 0, 0)
		return err == nil
	} else {
		// We briefly tried to gradually implement HCL2 support by adding a
		// branch here, but that experiment was not successful.
		panic("HCL2 experimental path no longer supported")
	}
}

// UnknownKeys returns the keys of the configuration that are unknown
// because they had interpolated variables that must be computed.
func (r *RawConfig) UnknownKeys() []string {
	r.lock.Lock()
	defer r.lock.Unlock()
	return r.unknownKeys
}

// See GobEncode
func (r *RawConfig) GobDecode(b []byte) error {
	var data gobRawConfig
	err := gob.NewDecoder(bytes.NewReader(b)).Decode(&data)
	if err != nil {
		return err
	}

	r.Key = data.Key
	r.Raw = data.Raw

	return r.init()
}

// GobEncode is a custom Gob encoder to use so that we only include the
// raw configuration. Interpolated variables and such are lost and the
// tree of interpolated variables is recomputed on decode, since it is
// referentially transparent.
func (r *RawConfig) GobEncode() ([]byte, error) {
	r.lock.Lock()
	defer r.lock.Unlock()

	data := gobRawConfig{
		Key: r.Key,
		Raw: r.Raw,
	}

	var buf bytes.Buffer
	if err := gob.NewEncoder(&buf).Encode(data); err != nil {
		return nil, err
	}

	return buf.Bytes(), nil
}

type gobRawConfig struct {
	Key string
	Raw map[string]interface{}
}
