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

package hcl

import (
	"github.com/hashicorp/hcl/v2"
	"github.com/zclconf/go-cty/cty"

	"github.com/hashicorp/terraform/internal/addrs"
	"github.com/hashicorp/terraform/internal/backend/backendrun"
	"github.com/hashicorp/terraform/internal/configs"
	"github.com/hashicorp/terraform/internal/lang/langrefs"
	"github.com/hashicorp/terraform/internal/terraform"
	"github.com/hashicorp/terraform/internal/tfdiags"
)

// VariableCaches contains a mapping between test run blocks and evaluated
// variables. This is used to cache the results of evaluating variables so that
// they are only evaluated once per run.
//
// Each run block has its own configuration and therefore its own set of
// evaluated variables.
type VariableCaches struct {
	GlobalVariables map[string]backendrun.UnparsedVariableValue
	FileVariables   map[string]hcl.Expression

	caches map[string]*VariableCache
}

// VariableCache contains the cache for a single run block. This cache contains
// the evaluated values for global and file-level variables.
type VariableCache struct {
	config *configs.Config

	globals terraform.InputValues
	files   terraform.InputValues

	values *VariableCaches // back reference so we can access the stored values
}

// GetCache returns the cache for the named run. If the cache does not exist, it
// is created and returned.
func (caches *VariableCaches) GetCache(name string, config *configs.Config) *VariableCache {
	if caches.caches == nil {
		caches.caches = make(map[string]*VariableCache)
	}

	cache, exists := caches.caches[name]
	if !exists {
		cache = &VariableCache{
			config:  config,
			globals: make(terraform.InputValues),
			files:   make(terraform.InputValues),
			values:  caches,
		}
		caches.caches[name] = cache
	}
	return cache
}

// GetGlobalVariable returns a value for the named global variable evaluated
// against the current run.
//
// This function caches the result of evaluating the variable so that it is
// only evaluated once per run.
//
// This function will return a valid input value if parsing fails for any reason
// so the caller can continue processing the configuration. The diagnostics
// returned will contain the error message that occurred during parsing and as
// such should be shown to the user.
func (cache *VariableCache) GetGlobalVariable(name string) (*terraform.InputValue, tfdiags.Diagnostics) {
	val, exists := cache.globals[name]
	if exists {
		return val, nil
	}

	variable, exists := cache.values.GlobalVariables[name]
	if !exists {
		return nil, nil
	}

	// TODO: We should also introduce a way to specify the mode in the test
	//   file itself. Suggestion, optional variable blocks.
	parsingMode := configs.VariableParseHCL

	if cfg, exists := cache.config.Module.Variables[name]; exists {
		parsingMode = cfg.ParsingMode
	}

	value, diags := variable.ParseVariableValue(parsingMode)
	if diags.HasErrors() {
		// In this case, the variable exists but we couldn't parse it. We'll
		// return a usable value so that we don't compound errors later by
		// claiming a variable doesn't exist when it does. We also return the
		// diagnostics explaining the error which will be shown to the user.
		value = &terraform.InputValue{
			Value: cty.DynamicVal,
		}
	}

	cache.globals[name] = value
	return value, diags
}

// GetFileVariable returns a value for the named file-level variable evaluated
// against the current run.
//
// This function caches the result of evaluating the variable so that it is
// only evaluated once per run.
//
// This function will return a valid input value if parsing fails for any reason
// so the caller can continue processing the configuration. The diagnostics
// returned will contain the error message that occurred during parsing and as
// such should be shown to the user.
func (cache *VariableCache) GetFileVariable(name string) (*terraform.InputValue, tfdiags.Diagnostics) {
	val, exists := cache.files[name]
	if exists {
		return val, nil
	}

	expr, exists := cache.values.FileVariables[name]
	if !exists {
		return nil, nil
	}

	var diags tfdiags.Diagnostics

	availableVariables := make(map[string]cty.Value)
	refs, refDiags := langrefs.ReferencesInExpr(addrs.ParseRefFromTestingScope, expr)
	for _, ref := range refs {
		if input, ok := ref.Subject.(addrs.InputVariable); ok {
			variable, variableDiags := cache.GetGlobalVariable(input.Name)
			diags = diags.Append(variableDiags)
			if variable != nil {
				availableVariables[input.Name] = variable.Value
			}
		}
	}
	diags = diags.Append(refDiags)

	if diags.HasErrors() {
		// There's no point trying to evaluate the variable as we know it will
		// fail. We'll just return a usable value so that we don't compound
		// errors later by claiming a variable doesn't exist when it does. We
		// also return the diagnostics explaining the error which will be shown
		// to the user.
		cache.files[name] = &terraform.InputValue{
			Value: cty.DynamicVal,
		}
		return cache.files[name], diags
	}

	ctx, ctxDiags := EvalContext(TargetFileVariable, map[string]hcl.Expression{name: expr}, availableVariables, nil)
	diags = diags.Append(ctxDiags)

	if ctxDiags.HasErrors() {
		// If we couldn't build the context, we won't actually process these
		// variables. Instead, we'll fill them with an empty value but still
		// make a note that the user did provide them.
		cache.files[name] = &terraform.InputValue{
			Value: cty.DynamicVal,
		}
		return cache.files[name], diags
	}

	value, valueDiags := expr.Value(ctx)
	diags = diags.Append(valueDiags)
	if diags.HasErrors() {
		// In this case, the variable exists but we couldn't parse it. We'll
		// return a usable value so that we don't compound errors later by
		// claiming a variable doesn't exist when it does. We also return the
		// diagnostics explaining the error which will be shown to the user.
		value = cty.DynamicVal
	}

	cache.files[name] = &terraform.InputValue{
		Value:       value,
		SourceType:  terraform.ValueFromConfig,
		SourceRange: tfdiags.SourceRangeFromHCL(expr.Range()),
	}
	return cache.files[name], diags
}
