// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1

package terraform

import (
	"fmt"
	"slices"

	"github.com/hashicorp/hcl/v2"

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

// nodeVariableValidation checks the author-specified validation rules against
// the final value of all expanded instances of a given input variable.
//
// A node of this type should always depend on another node that's responsible
// for deciding the final values for the nominated variable and registering
// them in the current "named values" state. [variableValidationTransformer]
// is the one responsible for inserting nodes of this type and ensuring that
// they each depend on the node that will register the final variable value.
type nodeVariableValidation struct {
	configAddr addrs.ConfigInputVariable
	rules      []*configs.CheckRule

	// defnRange is whatever source range we consider to best represent
	// the definition of the variable, which should ideally cover the
	// source code of the expression that was assigned to the variable.
	// When that's not possible -- for example, if the variable was
	// set from a non-configuration location like an environment variable --
	// it's acceptable to use the declaration location instead.
	defnRange hcl.Range

	// validateWalk is set to true during a validation walk, where any input
	// variables are set to unknown values. Since we may have unknown values
	// which will be known during plan, we need to be more lenient about what
	// can be unknown in variable validation expressions.
	validateWalk bool
}

var _ GraphNodeModulePath = (*nodeVariableValidation)(nil)
var _ GraphNodeReferenceable = (*nodeVariableValidation)(nil)
var _ GraphNodeReferencer = (*nodeVariableValidation)(nil)
var _ GraphNodeExecutable = (*nodeVariableValidation)(nil)
var _ graphNodeTemporaryValue = (*nodeVariableValidation)(nil)

func (n *nodeVariableValidation) Name() string {
	return fmt.Sprintf("%s (validation)", n.configAddr.String())
}

// ModulePath implements [GraphNodeModulePath].
func (n *nodeVariableValidation) ModulePath() addrs.Module {
	return n.configAddr.Module
}

// ReferenceableAddrs implements [GraphNodeReferenceable], announcing that
// this node contributes to the value for the input variable that it's
// validating, and must therefore run before any nodes that refer to it.
func (n *nodeVariableValidation) ReferenceableAddrs() []addrs.Referenceable {
	return []addrs.Referenceable{n.configAddr.Variable}
}

// nodeVariableValidation must act as if it's part of the associated variable
// node, and that means mirroring all that node's graph behavior. Root module
// variable are not temporary however, but because during a destroy we can't
// ensure that all references can be evaluated, we must skip validation unless
// absolutely necessary to avoid blocking the destroy from proceeding.
func (n *nodeVariableValidation) temporaryValue() bool {
	return true
}

// References implements [GraphNodeReferencer], announcing anything that
// the check rules refer to, other than the variable that's being validated
// (which gets its dependency connected by [variableValidationTransformer]
// instead).
func (n *nodeVariableValidation) References() []*addrs.Reference {
	var ret []*addrs.Reference
	for _, rule := range n.rules {
		// We ignore all diagnostics here because if an expression contains
		// invalid references then we'll catch them once we visit the
		// node (method Execute).
		condRefs, _ := langrefs.ReferencesInExpr(addrs.ParseRef, rule.Condition)
		msgRefs, _ := langrefs.ReferencesInExpr(addrs.ParseRef, rule.ErrorMessage)
		ret = n.appendRefsFilterSelf(ret, condRefs...)
		ret = n.appendRefsFilterSelf(ret, msgRefs...)
	}
	return ret
}

// appendRefsFilterSelf is a specialized version of builtin [append] that
// ignores any new references to the input variable represented by the
// reciever.
func (n *nodeVariableValidation) appendRefsFilterSelf(to []*addrs.Reference, new ...*addrs.Reference) []*addrs.Reference {
	// We need to filter out any self-references, because those would
	// make the resulting graph invalid and we don't need them because
	// variableValidationTransformer should've arranged for us to
	// already depend on whatever node provides the final value for
	// this variable.
	ret := slices.Grow(to, len(new))
	ourAddr := n.configAddr.Variable
	for _, ref := range new {
		if refAddr, ok := ref.Subject.(addrs.InputVariable); ok {
			if refAddr == ourAddr {
				continue
			}
		}
		ret = append(ret, ref)
	}
	return ret
}

func (n *nodeVariableValidation) Execute(globalCtx EvalContext, op walkOperation) tfdiags.Diagnostics {
	var diags tfdiags.Diagnostics

	// We need to perform validation work separately for each instance of
	// the variable across expanded modules, because each one could potentially
	// have a different value assigned to it and other different data in scope.
	expander := globalCtx.InstanceExpander()
	for _, modInst := range expander.ExpandModule(n.configAddr.Module, false) {
		addr := n.configAddr.Variable.Absolute(modInst)
		moduleCtx := globalCtx.withScope(evalContextModuleInstance{Addr: addr.Module})
		diags = diags.Append(evalVariableValidations(
			addr,
			moduleCtx,
			n.rules,
			n.defnRange,
			op,
		))
	}

	return diags
}
