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

package terraform

import (
	"log"

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

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

// ValidateOpts are the various options the affect the details of how Terraform
// will validate a configuration.
type ValidateOpts struct {
	// ExternalProviders are clients for pre-configured providers that are
	// treated as being passed into the root module from the caller. This
	// is equivalent to writing a "providers" argument inside a "module"
	// block in the Terraform language, but for the root module the caller
	// is written in Go rather than the Terraform language.
	//
	// Note, that while Terraform Core will not call ValidateProviderConfig or
	// ConfigureProvider on any providers in this map, as with the other context
	// functions, the Validate function never calls ConfigureProvider anyway.
	//
	// Normally, the validate function would call the ValidateProviderConfig
	// function on the provider, but the config may rely on variables that are
	// not available to this function. Therefore, it is the responsibility of
	// the caller to ensure that the provider configurations are valid.
	ExternalProviders map[addrs.RootProviderConfig]providers.Interface

	// When true, query files will also be validated.
	Query bool

	// AllowRootEphemeralOutputs overrides a specific check made within the
	// output nodes that they cannot be ephemeral at within root modules. This
	// should be set to true for plans executing from within either the stacks
	// or test runtimes, where the root modules as Terraform sees them aren't
	// the actual root modules.
	AllowRootEphemeralOutputs bool
}

// Validate performs semantic validation of a configuration, and returns
// any warnings or errors.
//
// Syntax and structural checks are performed by the configuration loader,
// and so are not repeated here.
//
// Validate considers only the configuration and so it won't catch any
// errors caused by current values in the state, or other external information
// such as root module input variables. However, the Plan function includes
// all of the same checks as Validate, in addition to the other work it does
// to consider the previous run state and the planning options.
//
// The opts can be nil, and the ExternalProviders field of the opts can be nil.
func (c *Context) Validate(config *configs.Config, opts *ValidateOpts) tfdiags.Diagnostics {
	defer c.acquireRun("validate")()

	var diags tfdiags.Diagnostics

	if opts == nil {
		// Just make sure we don't get any nil pointer exceptions later.
		opts = &ValidateOpts{}
	}

	moreDiags := c.checkConfigDependencies(config)
	diags = diags.Append(moreDiags)
	// If required dependencies are not available then we'll bail early since
	// otherwise we're likely to just see a bunch of other errors related to
	// incompatibilities, which could be overwhelming for the user.
	if diags.HasErrors() {
		return diags
	}

	// There are some validation checks that happen when loading the provider
	// schemas, and we can catch them early to ensure we are in a position to
	// handle any errors.
	_, moreDiags = c.Schemas(config, nil)
	diags = diags.Append(moreDiags)
	if moreDiags.HasErrors() {
		return diags
	}

	log.Printf("[DEBUG] Building and walking validate graph")

	// Validate is to check if the given module is valid regardless of
	// input values, current state, etc. Therefore we populate all of the
	// input values with unknown values of the expected type, allowing us
	// to perform a type check without assuming any particular values.
	varValues := make(InputValues)
	for name, variable := range config.Module.Variables {
		ty := variable.Type
		if ty == cty.NilType {
			// Can't predict the type at all, so we'll just mark it as
			// cty.DynamicVal (unknown value of cty.DynamicPseudoType).
			ty = cty.DynamicPseudoType
		}
		varValues[name] = &InputValue{
			Value:      cty.UnknownVal(ty),
			SourceType: ValueFromUnknown,
		}
	}

	graph, moreDiags := (&PlanGraphBuilder{
		Config:                    config,
		Plugins:                   c.plugins,
		State:                     states.NewState(),
		RootVariableValues:        varValues,
		Operation:                 walkValidate,
		ExternalProviderConfigs:   opts.ExternalProviders,
		ImportTargets:             c.findImportTargets(config),
		queryPlan:                 opts.Query,
		AllowRootEphemeralOutputs: opts.AllowRootEphemeralOutputs,
	}).Build(addrs.RootModuleInstance)
	diags = diags.Append(moreDiags)
	if moreDiags.HasErrors() {
		return diags
	}

	walker, walkDiags := c.walk(graph, walkValidate, &graphWalkOpts{
		Config:                  config,
		FunctionResults:         lang.NewFunctionResultsTable(nil),
		ExternalProviderConfigs: opts.ExternalProviders,
	})
	diags = diags.Append(walker.NonFatalDiagnostics)
	diags = diags.Append(walkDiags)
	if walkDiags.HasErrors() {
		return diags
	}

	return diags
}
