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

package terraform

import (
	"fmt"

	"github.com/zclconf/go-cty/cty"
	"github.com/zclconf/go-cty/cty/gocty"

	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/terraform/internal/lang/marks"
	"github.com/hashicorp/terraform/internal/tfdiags"
)

// evaluateCountExpression is our standard mechanism for interpreting an
// expression given for a "count" argument on a resource or a module. This
// should be called during expansion in order to determine the final count
// value.
//
// evaluateCountExpression differs from evaluateCountExpressionValue by
// returning an error if the count value is not known, and converting the
// cty.Value to an integer.
//
// If allowUnknown is false then this function will return error diagnostics
// whenever the expression returns an unknown value. Setting allowUnknown to
// true instead permits unknown values, indicating them by returning the
// placeholder value -1. Callers can assume that a return value of -1 without
// any error diagnostics represents a valid unknown value.
func evaluateCountExpression(expr hcl.Expression, ctx EvalContext, allowUnknown bool) (int, tfdiags.Diagnostics) {
	countVal, diags := evaluateCountExpressionValue(expr, ctx)
	if !allowUnknown && !countVal.IsKnown() {
		// Currently this is a rather bad outcome from a UX standpoint, since we have
		// no real mechanism to deal with this situation and all we can do is produce
		// an error message.
		// FIXME: In future, implement a built-in mechanism for deferring changes that
		// can't yet be predicted, and use it to guide the user through several
		// plan/apply steps until the desired configuration is eventually reached.
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Invalid count argument",
			Detail:   `The "count" value depends on resource attributes that cannot be determined until apply, so Terraform cannot predict how many instances will be created. To work around this, use the -target argument to first apply only the resources that the count depends on.`,
			Subject:  expr.Range().Ptr(),

			// TODO: Also populate Expression and EvalContext in here, but
			// we can't easily do that right now because the hcl.EvalContext
			// (which is not the same as the ctx we have in scope here) is
			// hidden away inside evaluateCountExpressionValue.
			Extra: diagnosticCausedByUnknown(true),
		})
	}

	// Ephemeral values are not allowed in count expressions.
	if marks.Has(countVal, marks.Ephemeral) {
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Invalid count argument",
			Detail:   `The given "count" value is derived from an ephemeral value, which means that Terraform cannot persist it between plan/apply rounds. Use only non-ephemeral values here.`,
			Subject:  expr.Range().Ptr(),
			Extra:    DiagnosticCausedByEphemeral(true),
		})
		return -1, diags
	}

	if countVal.IsNull() || !countVal.IsKnown() {
		return -1, diags
	}

	count, _ := countVal.AsBigFloat().Int64()
	return int(count), diags
}

// evaluateCountExpressionValue is like evaluateCountExpression
// except that it returns a cty.Value which must be a cty.Number and can be
// unknown.
func evaluateCountExpressionValue(expr hcl.Expression, ctx EvalContext) (cty.Value, tfdiags.Diagnostics) {
	var diags tfdiags.Diagnostics
	nullCount := cty.NullVal(cty.Number)
	if expr == nil {
		return nullCount, nil
	}

	countVal, countDiags := ctx.EvaluateExpr(expr, cty.Number, nil)
	diags = diags.Append(countDiags)
	if diags.HasErrors() {
		return nullCount, diags
	}

	if marks.Has(countVal, marks.Ephemeral) {
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Invalid count argument",
			Detail:   `The given "count" is derived from an ephemeral value, which means that Terraform cannot persist it between plan/apply rounds. Use only non-ephemeral values to specify the number of resource instances.`,
			Subject:  expr.Range().Ptr(),

			// TODO: Also populate Expression and EvalContext in here, but
			// we can't easily do that right now because the hcl.EvalContext
			// (which is not the same as the ctx we have in scope here) is
			// hidden away inside ctx.EvaluateExpr.
			Extra: DiagnosticCausedByEphemeral(true),
		})
	}

	countVal, deprecationDiags := ctx.Deprecations().ValidateAndUnmark(countVal, ctx.Path().Module(), expr.Range().Ptr())
	diags = diags.Append(deprecationDiags)

	// Sensitive values are allowed in count but not for_each. This is a
	// somewhat-dubious decision because the number of instances planned
	// will disclose exactly what the value was, but in practice it's rare
	// for a number alone to be sensitive and so this is pragmatic, along with
	// being required for backward-compatibility.
	countVal, _ = countVal.Unmark()

	switch {
	case countVal.IsNull():
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Invalid count argument",
			Detail:   `The given "count" argument value is null. An integer is required.`,
			Subject:  expr.Range().Ptr(),
		})
		return nullCount, diags

	case !countVal.IsKnown():
		return cty.UnknownVal(cty.Number), diags
	}

	var count int
	err := gocty.FromCtyValue(countVal, &count)
	if err != nil {
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Invalid count argument",
			Detail:   fmt.Sprintf(`The given "count" argument value is unsuitable: %s.`, err),
			Subject:  expr.Range().Ptr(),
		})
		return nullCount, diags
	}
	if count < 0 {
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Invalid count argument",
			Detail:   `The given "count" argument value is unsuitable: must be greater than or equal to zero.`,
			Subject:  expr.Range().Ptr(),
		})
		return nullCount, diags
	}

	return countVal, diags
}
