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

package stackconfig

import (
	"github.com/apparentlymart/go-versions/versions/constraints"
	"github.com/hashicorp/go-slug/sourceaddrs"
	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/gohcl"

	"github.com/hashicorp/terraform/internal/addrs"
	"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
	"github.com/hashicorp/terraform/internal/tfdiags"
)

// Removed represents a component that was removed from the configuration.
//
// Removed blocks don't have labels associated with them, instead they have
// a "from" attribute that points directly to the old component that was
// removed. Removed blocks can also point to component instances specifically,
// using an index expression. The "for_each" attribute also means that the
// "from" attribute can't always be evaluated statically.
//
// Removed blocks are, therefore, represented by the FromComponent and FromIndex
// fields, which together represent the address of the removed component. The
// FromComponent field is the address of the component itself, and the FromIndex
// field is the index expression that will be evaluated to determine the
// specific instance of the component that was removed.
//
// FromIndex can be null if either the removed block is pointing to a component
// that was not instanced, or is pointing to all the instances of a removed
// component.
//
// For this reason, multiple Removed blocks can be associated with the same
// FromComponent, but with different FromIndex values. When the FromIndex values
// are evaluated, during the planning stage, we will validate that the FromIndex
// values are unique.
type Removed struct {
	From stackaddrs.RemovedFrom

	SourceAddr                               sourceaddrs.Source
	VersionConstraints                       constraints.IntersectionSpec
	SourceAddrRange, VersionConstraintsRange tfdiags.SourceRange

	// FinalSourceAddr is populated only when a configuration is loaded
	// through [LoadConfigDir], and in that case contains the finalized
	// address produced by resolving the SourceAddr field relative to
	// the address of the file where the component was declared. This
	// is the address to use if you intend to load the component's
	// root module from a source bundle.
	FinalSourceAddr sourceaddrs.FinalSource

	ForEach hcl.Expression

	// ProviderConfigs describes the mapping between the static provider
	// configuration slots declared in the component's root module and the
	// dynamic provider configuration objects in scope in the calling
	// stack configuration.
	//
	// This map deals with the slight schism between the stacks language's
	// treatment of provider configurations as regular values of a special
	// data type vs. the main Terraform language's treatment of provider
	// configurations as something special passed out of band from the
	// input variables. The overall structure and the map keys are fixed
	// statically during decoding, but the final provider configuration objects
	// are determined only at runtime by normal expression evaluation.
	//
	// The keys of this map refer to provider configuration slots inside
	// the module being called, but use the local names defined in the
	// calling stack configuration. The stacks language runtime will
	// translate the caller's local names into the callee's declared provider
	// configurations by using the stack configuration's table of local
	// provider names.
	//
	// This will only be populated if From points to a component.
	ProviderConfigs map[addrs.LocalProviderConfig]hcl.Expression

	// Inputs describes the inputs that will be used to destroy all components
	// within the target stack.
	//
	// This will only be populated if From points to a stack.
	Inputs hcl.Expression

	// Destroy controls whether this removed block will actually destroy all
	// instances of resources within this component, or just removed them from
	// the state. Defaults to true.
	Destroy bool

	DeclRange tfdiags.SourceRange
}

func decodeRemovedBlock(block *hcl.Block) (*Removed, tfdiags.Diagnostics) {
	var diags tfdiags.Diagnostics
	ret := &Removed{
		DeclRange: tfdiags.SourceRangeFromHCL(block.DefRange),
	}

	content, hclDiags := block.Body.Content(removedBlockSchema)
	diags = diags.Append(hclDiags)
	if hclDiags.HasErrors() {
		return nil, diags
	}

	// We're splitting out the component and the index now, as we can decode and
	// analyse the component now. The index might be referencing the for_each
	// variable, which we can't decode yet.
	from, moreDiags := stackaddrs.ParseRemovedFrom(content.Attributes["from"].Expr)
	diags = diags.Append(moreDiags)
	if moreDiags.HasErrors() {
		return nil, diags
	}
	ret.From = from

	sourceAddr, versionConstraints, moreDiags := decodeSourceAddrArguments(
		content.Attributes["source"],
		content.Attributes["version"],
	)
	diags = diags.Append(moreDiags)
	if moreDiags.HasErrors() {
		return nil, diags
	}

	ret.SourceAddr = sourceAddr
	ret.VersionConstraints = versionConstraints
	ret.SourceAddrRange = tfdiags.SourceRangeFromHCL(content.Attributes["source"].Range)
	if content.Attributes["version"] != nil {
		ret.VersionConstraintsRange = tfdiags.SourceRangeFromHCL(content.Attributes["version"].Range)
	}
	// Now that we've populated the mandatory source location fields we can
	// safely return a partial ret if we encounter any further errors, as
	// long as we leave the other fields either unset or in some other
	// reasonable state for careful partial analysis.

	if attr, ok := content.Attributes["for_each"]; ok {
		matches := false
		for _, variable := range ret.From.Variables() {
			if root, ok := variable[0].(hcl.TraverseRoot); ok {
				if root.Name == "each" {
					matches = true
					break
				}
			}
		}
		if !matches {
			// You have to refer to the for_each attribute somewhere in the
			// from attribute.
			diags = diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Invalid for_each expression",
				Detail:   "A removed block with a for_each expression must reference that expression within the `from` attribute.",
				Subject:  attr.NameRange.Ptr(),
			})
		}

		ret.ForEach = attr.Expr
	}
	if attr, ok := content.Attributes["providers"]; ok {
		if ret.From.Component == nil {
			diags = diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Invalid providers attribute",
				Detail:   "A removed block that does not target a component should not specify any providers.",
				Subject:  attr.NameRange.Ptr(),
			})
		}

		var providerDiags tfdiags.Diagnostics
		ret.ProviderConfigs, providerDiags = decodeProvidersAttribute(attr)
		diags = diags.Append(providerDiags)
	}

	if attr, ok := content.Attributes["inputs"]; ok {
		if ret.From.Component != nil {
			diags = diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Invalid inputs attribute",
				Detail:   "A removed block that does not target an embedded stack should not specify any inputs.",
				Subject:  attr.NameRange.Ptr(),
			})
		}

		ret.Inputs = attr.Expr
	}

	ret.Destroy = true // default to true
	for _, block := range content.Blocks {
		switch block.Type {
		case "lifecycle":
			lcContent, lcDiags := block.Body.Content(removedLifecycleBlockSchema)
			diags = diags.Append(lcDiags)

			if attr, ok := lcContent.Attributes["destroy"]; ok {
				valDiags := gohcl.DecodeExpression(attr.Expr, nil, &ret.Destroy)
				diags = diags.Append(valDiags)
			}
		}
	}

	return ret, diags
}

var removedBlockSchema = &hcl.BodySchema{
	Blocks: []hcl.BlockHeaderSchema{
		{Type: "lifecycle"},
	},
	Attributes: []hcl.AttributeSchema{
		{Name: "from", Required: true},
		{Name: "source", Required: true},
		{Name: "version", Required: false},
		{Name: "for_each", Required: false},
		{Name: "providers", Required: false},
		{Name: "inputs", Required: false},
	},
}

var removedLifecycleBlockSchema = &hcl.BodySchema{
	Attributes: []hcl.AttributeSchema{
		{Name: "destroy"},
	},
}
