// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package exprs

import (
	"context"
	"iter"
	"slices"

	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/ext/dynblock"
	"github.com/hashicorp/hcl/v2/hcldec"
	"github.com/zclconf/go-cty/cty"

	"github.com/opentofu/opentofu/internal/tfdiags"
)

// Evalable is implemented by types that encapsulate expressions that can be
// evaluated in some evaluation scope decided by a caller.
//
// An Evalable implementation must include any supporting metadata needed to
// analyze and evaluate the expressions inside. For example, an [Evalable]
// representing a HCL body must also include the expected schema for that body.
type Evalable interface {
	// References returns a sequence of references this Evalable makes to
	// values in its containing scope.
	References() iter.Seq[hcl.Traversal]

	// FunctionCalls returns a sequence of all of the function calls that
	// could be made if this were evaluated.
	//
	// TODO: Perhaps References and FunctionCalls should be combined together
	// somehow to return a tree that shows when a reference appears as part
	// of an argument to a function, to address the problem described in
	// this issue:
	//     https://github.com/opentofu/opentofu/issues/2630
	FunctionCalls() iter.Seq[*hcl.StaticCall]

	// Evaluate performs the actual expression evaluation, using the given
	// HCL evaluation context to satisfy any references.
	//
	// Callers must first use the References method to discover what the
	// wrapped expressions refer to, and make sure that the given evaluation
	// context contains at least the variables required to satisfy those
	// references.
	//
	// This method takes a [context.Context] because some implementations
	// may internally block on the completion of a potentially-time-consuming
	// operation, in which case they should respond gracefully to the
	// cancellation or deadline of the given context.
	//
	// If Evaluate returns diagnostics then it must also return a suitable
	// placeholder value that could be use for downstream expression evaluation
	// despite the error. Returning [cty.DynamicVal] is acceptable if all else
	// fails, but returning an unknown value with a more specific type
	// constraint can give more opportunities to proactively detect downstream
	// errors in a single evaluation pass.
	Evaluate(ctx context.Context, hclCtx *hcl.EvalContext) (cty.Value, tfdiags.Diagnostics)

	// ResultTypeConstraint returns a type constrant that all possible results
	// from method Evaluate would conform to.
	//
	// This is used for static type checking. Return [cty.DynamicPseudoType]
	// if it's impossible to predict any single type constraint for the
	// possible results.
	//
	// TODO: Some implementations of this would be able to do better if they
	// knew the types of everything that'd be passed in hclCtx when calling
	// Evaluate. Is there some way we can approximate that?
	ResultTypeConstraint() cty.Type

	// EvalableSourceRange returns a description of a source location that this
	// Evalable was derived from.
	EvalableSourceRange() tfdiags.SourceRange
}

// ForcedErrorEvalable returns an [Evalable] that always fails with
// [cty.DynamicVal] as its placeholder result and with the given diagnostics,
// which must include at least one error or this function will panic.
//
// This is primarily intended for unit testing purposes for creating
// placeholders for upstream objects that have failed, but might also be useful
// sometimes for handling early-detected error situations in "real" code.
func ForcedErrorEvalable(diags tfdiags.Diagnostics, sourceRange tfdiags.SourceRange) Evalable {
	if !diags.HasErrors() {
		panic("ForcedErrorEvalable without any error diagnostics")
	}
	// We reuse the same type as ForcedErrorValuer here because it can
	// implement both interfaces just fine with the information available here.
	return forcedErrorValuer{diags, &sourceRange}
}

func StaticCheckTraversal(traversal hcl.Traversal, evalable Evalable) tfdiags.Diagnostics {
	return StaticCheckTraversalThroughType(traversal, evalable.ResultTypeConstraint())
}

func StaticCheckTraversalThroughType(traversal hcl.Traversal, typeConstraint cty.Type) tfdiags.Diagnostics {
	// We perform a static check by attempting to apply the traversal to
	// an unknown value of the given type constraint, which will fail if
	// no possible value meeting that type constraint could possibly support
	// the traversal.
	var diags tfdiags.Diagnostics
	placeholder := cty.UnknownVal(typeConstraint)
	_, hclDiags := traversal.TraverseRel(placeholder)
	diags = diags.Append(hclDiags)
	return diags
}

// hclExpression implements [Evalable] for a standalone [hcl.Expression].
type hclExpression struct {
	expr hcl.Expression
}

// EvalableHCLExpression returns an [Evalable] that is just a thin wrapper
// around the given HCL expression.
func EvalableHCLExpression(expr hcl.Expression) Evalable {
	return hclExpression{expr}
}

// Evaluate implements Evalable.
func (h hclExpression) Evaluate(ctx context.Context, hclCtx *hcl.EvalContext) (cty.Value, tfdiags.Diagnostics) {
	var diags tfdiags.Diagnostics
	v, hclDiags := h.expr.Value(hclCtx)
	diags = diags.Append(hclDiags)
	if hclDiags.HasErrors() {
		v = cty.UnknownVal(h.ResultTypeConstraint()).WithSameMarks(v)
	}
	return v, diags
}

// FunctionCalls implements Evalable.
func (h hclExpression) FunctionCalls() iter.Seq[*hcl.StaticCall] {
	// FIXME: The underlying HCL API somewhat-misuses [hcl.Traversal] to
	// enumerate the names of the functions without providing any
	// information about their arguments. We can get away with leaving
	// the args unpopulated for now since we're not actually relying on
	// them but it would be nice to update HCL to return [hcl.StaticCall]
	// for the function analysis instead, for consistency with how
	// [hcl.ExprCall] works.
	expr, ok := h.expr.(hcl.ExpressionWithFunctions)
	if !ok {
		return func(yield func(*hcl.StaticCall) bool) {}
	}
	return func(yield func(*hcl.StaticCall) bool) {
		for _, traversal := range expr.Functions() {
			wantMore := yield(&hcl.StaticCall{
				Name:      traversal.RootName(),
				NameRange: traversal.SourceRange(),
			})
			if !wantMore {
				return
			}
		}
	}
}

// References implements Evalable.
func (h hclExpression) References() iter.Seq[hcl.Traversal] {
	return slices.Values(h.expr.Variables())
}

// ResultTypeConstraint implements Evalable.
func (h hclExpression) ResultTypeConstraint() cty.Type {
	// We can only predict the result type of the expression if it doesn't
	// include any references or function calls.
	v, hclDiags := h.expr.Value(nil)
	if hclDiags.HasErrors() {
		return cty.DynamicPseudoType
	}
	// For an expression that only uses constants, its type is guaranteed
	// to always be the same.
	return v.Type()
}

// SourceRange implements Evalable.
func (h hclExpression) EvalableSourceRange() tfdiags.SourceRange {
	return tfdiags.SourceRangeFromHCL(h.expr.Range())
}

// hclBody implements [Evalable] for a [hcl.Body] and associated [hcldec.Spec].
type hclBody struct {
	body     hcl.Body
	spec     hcldec.Spec
	dynblock bool
}

// EvalableHCLBody returns an [Evalable] that evaluates the given HCL body
// using the given [hcldec] specification.
func EvalableHCLBody(body hcl.Body, spec hcldec.Spec) Evalable {
	return &hclBody{
		body:     body,
		spec:     spec,
		dynblock: false,
	}
}

// EvalableHCLBodyWithDynamicBlocks is a variant of [EvalableHCLBody] that
// calls [dynblock.Expand] before evaluating the body so that "dynamic" blocks
// would be supported and expanded to their equivalent static blocks.
func EvalableHCLBodyWithDynamicBlocks(body hcl.Body, spec hcldec.Spec) Evalable {
	return &hclBody{
		body:     body,
		spec:     spec,
		dynblock: true,
	}
}

// Evaluate implements Evalable.
func (h *hclBody) Evaluate(ctx context.Context, hclCtx *hcl.EvalContext) (cty.Value, tfdiags.Diagnostics) {
	var diags tfdiags.Diagnostics
	body := h.body
	if h.dynblock {
		// [dynblock.Expand] wraps our body so that hcldec.Decode below will
		// indirectly cause the "dynamic" blocks to be expanded, using the
		// same evaluation context for the for_each expressions.
		body = dynblock.Expand(body, hclCtx)
	}
	v, hclDiags := hcldec.Decode(body, h.spec, hclCtx)
	diags = diags.Append(hclDiags)
	if hclDiags.HasErrors() {
		v = cty.UnknownVal(h.ResultTypeConstraint()).WithSameMarks(v)
	}
	return v, diags
}

// FunctionCalls implements Evalable.
func (h hclBody) FunctionCalls() iter.Seq[*hcl.StaticCall] {
	// FIXME: The underlying HCL API somewhat-misuses [hcl.Traversal] to
	// enumerate the names of the functions without providing any
	// information about their arguments. We can get away with leaving
	// the args unpopulated for now since we're not actually relying on
	// them but it would be nice to update HCL to return [hcl.StaticCall]
	// for the function analysis instead, for consistency with how
	// [hcl.ExprCall] works.
	return func(yield func(*hcl.StaticCall) bool) {
		for _, traversal := range hcldec.Functions(h.body, h.spec) {
			wantMore := yield(&hcl.StaticCall{
				Name:      traversal.RootName(),
				NameRange: traversal.SourceRange(),
			})
			if !wantMore {
				return
			}
		}
	}
}

// References implements Evalable.
func (h *hclBody) References() iter.Seq[hcl.Traversal] {
	if h.dynblock {
		// When we're doing dynblock expansion we need to do a little
		// more work to also detect references from the for_each
		// arguments in the "dynamic" blocks. The dynblock package
		// does this additional work for us as long as we ask its
		// wrapper function instead of hcldec.Variables directly.
		return slices.Values(dynblock.VariablesHCLDec(h.body, h.spec))
	}
	return slices.Values(hcldec.Variables(h.body, h.spec))
}

// ResultTypeConstraint implements Evalable.
func (h *hclBody) ResultTypeConstraint() cty.Type {
	return hcldec.ImpliedType(h.spec)
}

// SourceRange implements Evalable.
func (h *hclBody) EvalableSourceRange() tfdiags.SourceRange {
	// The "missing item range" is not necessarily a good range to use here,
	// but is the best we can do. At least in HCL native syntax this tends
	// to be in the header of the block that contained the body and so
	// is _close_ to the body being described.
	return tfdiags.SourceRangeFromHCL(h.body.MissingItemRange())
}

// hclBodyJustAttributes implements [Evalable] for a [hcl.Body] using HCL's
// "just attributes" evaluation mode.
type hclBodyJustAttributes struct {
	body hcl.Body
}

// EvalableHCLBody returns an [Evalable] that evaluates the given HCL body
// in HCL's "just attributes" mode, and then returns an object value whose
// attribute names and values are derived from the result.
func EvalableHCLBodyJustAttributes(body hcl.Body) Evalable {
	return &hclBodyJustAttributes{
		body: body,
	}
}

// EvalableSourceRange implements Evalable.
func (h *hclBodyJustAttributes) EvalableSourceRange() tfdiags.SourceRange {
	return tfdiags.SourceRangeFromHCL(h.body.MissingItemRange())
}

// Evaluate implements Evalable.
func (h *hclBodyJustAttributes) Evaluate(ctx context.Context, hclCtx *hcl.EvalContext) (cty.Value, tfdiags.Diagnostics) {
	var diags tfdiags.Diagnostics
	attrs, hclDiags := h.body.JustAttributes()
	diags = diags.Append(hclDiags)
	// If we have errors already then we might not have the full set of
	// attributes that were declared, so we'll return cty.DynamicVal below
	// to avoid overpromising that we know the result type. However,
	// we'll still visit all of the attributes we _did_ find in case
	// that allows us to collect up some more expression-errors to return so we
	// can tell the module author about as much as possible at once.
	typeKnown := !hclDiags.HasErrors()
	retAttrs := make(map[string]cty.Value, len(attrs))
	for name, attr := range attrs {
		val, hclDiags := attr.Expr.Value(hclCtx)
		diags = diags.Append(hclDiags)
		if hclDiags.HasErrors() {
			val = AsEvalError(cty.DynamicVal)
		}
		retAttrs[name] = val
	}
	if !typeKnown {
		return EvalResult(cty.DynamicVal, diags)
	}
	// We don't use a top-level EvalError here because we selectively
	// marked individual attribute values above, and we're confident
	// that the set of attribute names in this object is correct because
	// the original JustAttributes call succeeded.
	return cty.ObjectVal(retAttrs), diags
}

// FunctionCalls implements Evalable.
func (h *hclBodyJustAttributes) FunctionCalls() iter.Seq[*hcl.StaticCall] {
	// FIXME: The underlying HCL API somewhat-misuses [hcl.Traversal] to
	// enumerate the names of the functions without providing any
	// information about their arguments. We can get away with leaving
	// the args unpopulated for now since we're not actually relying on
	// them but it would be nice to update HCL to return [hcl.StaticCall]
	// for the function analysis instead, for consistency with how
	// [hcl.ExprCall] works.
	return func(yield func(*hcl.StaticCall) bool) {
		// We intentionally ignore diagnostics here since we're just making a
		// best-effort static analysis in preparation for a later call to
		// [hclBodyJustAttributes.Evaluate], which will then perform this
		// same operation and return its diagnostics. Whatever subset of
		// the attributes this function returns should all be valid enough
		// for the subsequent analysis.
		attrs, _ := h.body.JustAttributes()

		for _, attr := range attrs {
			withFuncs, ok := attr.Expr.(hcl.ExpressionWithFunctions)
			if !ok {
				continue
			}
			for _, traversal := range withFuncs.Functions() {
				wantMore := yield(&hcl.StaticCall{
					Name:      traversal.RootName(),
					NameRange: traversal.SourceRange(),
				})
				if !wantMore {
					return
				}
			}
		}
	}
}

// References implements Evalable.
func (h *hclBodyJustAttributes) References() iter.Seq[hcl.Traversal] {
	// This case is annoying because we need to perform the shallow
	// JustAttributes call to get the expressions to analyze, but then
	// we'll need to call it again in Evaluate once the scope has
	// been built. But only if we find this being a performance problem
	// should we consider trying to cache this result.
	//
	// We ignore the diagnostics here because we're just making a best
	// effort to learn what traversals we might need and then we'll
	// return the same set of diagnostics from Evaluate.
	attrs, _ := h.body.JustAttributes()
	return func(yield func(hcl.Traversal) bool) {
		for _, attr := range attrs {
			for _, traversal := range attr.Expr.Variables() {
				if !yield(traversal) {
					return
				}
			}
		}
	}
}

// ResultTypeConstraint implements Evalable.
func (h *hclBodyJustAttributes) ResultTypeConstraint() cty.Type {
	// We cannot predict a result type in "just attributes" mode because
	// the type depends on the results of the expressions in the body.
	return cty.DynamicPseudoType
}
