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

package stackconfig

import (
	"fmt"

	"github.com/hashicorp/hcl/v2"

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

// Declarations represent the various items that can be declared in a stack
// configuration.
//
// This just represents the fields that both [Stack] and [File] have in common,
// so we can share some code between the two.
type Declarations struct {
	// EmbeddedStacks are calls to other stack configurations that should
	// be treated as a part of the overall desired state produced from this
	// stack. These are declared with "stack" blocks in the stack language.
	EmbeddedStacks map[string]*EmbeddedStack

	// Components are calls to trees of Terraform modules that represent the
	// real infrastructure described by a stack.
	Components map[string]*Component

	// InputVariables, LocalValues, and OutputValues together represent all
	// of the "named values" in the stack configuration, which are just glue
	// to pass values between scopes or to factor out common expressions for
	// reuse in multiple locations.
	InputVariables map[string]*InputVariable
	LocalValues    map[string]*LocalValue
	OutputValues   map[string]*OutputValue

	// RequiredProviders represents the single required_providers block
	// that's allowed in any stack, declaring which providers this stack
	// depends on and which versions of those providers it is compatible with.
	RequiredProviders *ProviderRequirements

	// ProviderConfigs are the provider configurations declared in this
	// particular stack configuration. Other stack configurations in the
	// overall tree might have their own provider configurations.
	ProviderConfigs map[addrs.LocalProviderConfig]*ProviderConfig

	// RemovedComponents is the list of components that have been removed from
	// the configuration.
	RemovedComponents collections.Map[stackaddrs.ConfigComponent, []*Removed]

	// RemovedEmbeddedStacks is the list of embedded stacks that have been removed
	// from the configuration.
	RemovedEmbeddedStacks collections.Map[stackaddrs.ConfigStackCall, []*Removed]
}

func makeDeclarations() Declarations {
	return Declarations{
		EmbeddedStacks:        make(map[string]*EmbeddedStack),
		Components:            make(map[string]*Component),
		InputVariables:        make(map[string]*InputVariable),
		LocalValues:           make(map[string]*LocalValue),
		OutputValues:          make(map[string]*OutputValue),
		ProviderConfigs:       make(map[addrs.LocalProviderConfig]*ProviderConfig),
		RemovedComponents:     collections.NewMap[stackaddrs.ConfigComponent, []*Removed](),
		RemovedEmbeddedStacks: collections.NewMap[stackaddrs.ConfigStackCall, []*Removed](),
	}
}

func (d *Declarations) addComponent(decl *Component) tfdiags.Diagnostics {
	if decl == nil {
		return nil
	}
	var diags tfdiags.Diagnostics

	name := decl.Name
	if existing, exists := d.Components[name]; exists {
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Duplicate component declaration",
			Detail: fmt.Sprintf(
				"An component named %q was already declared at %s.",
				name, existing.DeclRange.ToHCL(),
			),
			Subject: decl.DeclRange.ToHCL().Ptr(),
		})
		return diags
	}

	if blocks, exists := d.RemovedComponents.GetOk(stackaddrs.ConfigComponent{
		Stack: nil,
		Item: stackaddrs.Component{
			Name: name,
		},
	}); exists {
		for _, removed := range blocks {
			if removed.From.Component.Index == nil {
				// If a component has been removed, we should not also find it
				// in the configuration.
				//
				// If the removed block has an index, then it's possible that
				// only a specific instance was removed and not the whole thing.
				// This is okay at this point, and will be validated more later.
				// See the addRemoved method for more information.
				diags = diags.Append(&hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Component exists for removed block",
					Detail: fmt.Sprintf(
						"A removed block for component %q was declared without an index, but a component block with the same name was declared at %s.\n\nA removed block without an index indicates that the component and all instances were removed from the configuration, and this is not the case.",
						name, decl.DeclRange.ToHCL(),
					),
					Subject: removed.DeclRange.ToHCL().Ptr(),
				})
				return diags
			}
		}
	}

	d.Components[name] = decl
	return diags
}

func (d *Declarations) addEmbeddedStack(decl *EmbeddedStack) tfdiags.Diagnostics {
	if decl == nil {
		return nil
	}
	var diags tfdiags.Diagnostics

	name := decl.Name
	if existing, exists := d.EmbeddedStacks[name]; exists {
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Duplicate embedded stack call",
			Detail: fmt.Sprintf(
				"An embedded stack call named %q was already declared at %s.",
				name, existing.DeclRange.ToHCL(),
			),
			Subject: decl.DeclRange.ToHCL().Ptr(),
		})
		return diags
	}

	if blocks, exists := d.RemovedEmbeddedStacks.GetOk(stackaddrs.ConfigStackCall{
		Stack: nil,
		Item: stackaddrs.StackCall{
			Name: name,
		},
	}); exists {
		for _, removed := range blocks {
			if removed.From.Stack[0].Index == nil {
				diags = diags.Append(&hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Stack exists for removed block",
					Detail: fmt.Sprintf(
						"A removed block for stack %q was declared without an index, but a stack block with the same name was declared at %s.\n\nA removed block without an index indicates that the stack and all instances were removed from the configuration, and this is not the case.",
						name, decl.DeclRange.ToHCL(),
					),
					Subject: removed.DeclRange.ToHCL().Ptr(),
				})
				return diags
			}
		}
	}

	d.EmbeddedStacks[name] = decl
	return diags
}

func (d *Declarations) addInputVariable(decl *InputVariable) tfdiags.Diagnostics {
	if decl == nil {
		return nil
	}
	var diags tfdiags.Diagnostics

	name := decl.Name
	if existing, exists := d.InputVariables[name]; exists {
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Duplicate input variable declaration",
			Detail: fmt.Sprintf(
				"An input variable named %q was already declared at %s.",
				name, existing.DeclRange.ToHCL(),
			),
			Subject: decl.DeclRange.ToHCL().Ptr(),
		})
		return diags
	}

	d.InputVariables[name] = decl
	return diags
}

func (d *Declarations) addLocalValue(decl *LocalValue) tfdiags.Diagnostics {
	if decl == nil {
		return nil
	}
	var diags tfdiags.Diagnostics

	name := decl.Name
	if existing, exists := d.LocalValues[name]; exists {
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Duplicate local value declaration",
			Detail: fmt.Sprintf(
				"A local value named %q was already declared at %s.",
				name, existing.DeclRange.ToHCL(),
			),
			Subject: decl.DeclRange.ToHCL().Ptr(),
		})
		return diags
	}

	d.LocalValues[name] = decl
	return diags
}

func (d *Declarations) addOutputValue(decl *OutputValue) tfdiags.Diagnostics {
	if decl == nil {
		return nil
	}
	var diags tfdiags.Diagnostics

	name := decl.Name
	if existing, exists := d.OutputValues[name]; exists {
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Duplicate output value declaration",
			Detail: fmt.Sprintf(
				"An output value named %q was already declared at %s.",
				name, existing.DeclRange.ToHCL(),
			),
			Subject: decl.DeclRange.ToHCL().Ptr(),
		})
		return diags
	}

	d.OutputValues[name] = decl
	return diags
}

func (d *Declarations) addRequiredProviders(decl *ProviderRequirements) tfdiags.Diagnostics {
	if decl == nil {
		return nil
	}
	var diags tfdiags.Diagnostics
	if d.RequiredProviders != nil {
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Duplicate provider requirements",
			Detail: fmt.Sprintf(
				"This stack's provider requirements were already declared at %s.",
				d.RequiredProviders.DeclRange.ToHCL(),
			),
			Subject: decl.DeclRange.ToHCL().Ptr(),
		})
		return diags
	}
	d.RequiredProviders = decl
	return diags
}

func (d *Declarations) addProviderConfig(decl *ProviderConfig) tfdiags.Diagnostics {
	if decl == nil {
		return nil
	}
	var diags tfdiags.Diagnostics

	addr := decl.LocalAddr
	if existing, exists := d.ProviderConfigs[addr]; exists {
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Duplicate provider configuration",
			Detail: fmt.Sprintf(
				"An configuration named %q for provider %q was already declared at %s.",
				addr.LocalName, addr.Alias, existing.DeclRange.ToHCL(),
			),
			Subject: decl.DeclRange.ToHCL().Ptr(),
		})
		return diags
	}

	d.ProviderConfigs[addr] = decl
	return diags
}

func (d *Declarations) addRemoved(decl *Removed) tfdiags.Diagnostics {
	var diags tfdiags.Diagnostics

	if decl == nil {
		return diags
	}

	if decl.From.Component != nil {
		addr := decl.From.TargetConfigComponent()

		if decl.From.Component.Index == nil && len(decl.From.Stack) == 0 {
			// If the removed block does not have an index, then we shouldn't also
			// have a component block with the same name. A removed block without
			// an index indicates that the component and all instances were removed
			// from the configuration.
			//
			// Note that a removed block with an index is allowed to coexist with a
			// component block with the same name, because it indicates that only
			// a specific instance was removed and not the whole thing. During the
			// validate and planning stages we will validate that the clashing
			// component and removed blocks are not both pointing to the same index.
			if component, exists := d.Components[decl.From.Component.Name]; exists {
				diags = diags.Append(&hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Component exists for removed block",
					Detail: fmt.Sprintf(
						"A removed block for component %q was declared without an index, but a component block with the same name was declared at %s.\n\nA removed block without an index indicates that the component and all instances were removed from the configuration, and this is not the case.",
						decl.From.Component.Name, component.DeclRange.ToHCL(),
					),
					Subject: decl.DeclRange.ToHCL().Ptr(),
				})
				return diags
			}
		}

		d.RemovedComponents.Put(addr, append(d.RemovedComponents.Get(addr), decl))
	} else {
		addr := decl.From.TargetStack().ToStackCall()

		if len(decl.From.Stack) == 1 && decl.From.Stack[0].Index == nil {
			// Same logic as for components, we can just error a bit earlier
			// here if the user is targeting a stack that definitely exists
			// in the configuration.
			if stack, exists := d.EmbeddedStacks[decl.From.Stack[0].Name]; exists {
				diags = diags.Append(&hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Stack exists for removed block",
					Detail: fmt.Sprintf(
						"A removed block for stack %q was declared without an index, but a stack block with the same name was declared at %s.\n\nA removed block without an index indicates that the stack and all instances were removed from the configuration, and this is not the case.",
						decl.From.Component.Name, stack.DeclRange.ToHCL(),
					),
					Subject: decl.DeclRange.ToHCL().Ptr(),
				})
				return diags
			}
		}

		d.RemovedEmbeddedStacks.Put(addr, append(d.RemovedEmbeddedStacks.Get(addr), decl))
	}

	return diags
}

func (d *Declarations) merge(other *Declarations) tfdiags.Diagnostics {
	var diags tfdiags.Diagnostics
	for _, decl := range other.EmbeddedStacks {
		diags = diags.Append(
			d.addEmbeddedStack(decl),
		)
	}
	for _, blocks := range other.RemovedEmbeddedStacks.All() {
		for _, decl := range blocks {
			diags = diags.Append(d.addRemoved(decl))
		}
	}
	for _, decl := range other.Components {
		diags = diags.Append(
			d.addComponent(decl),
		)
	}
	for _, decl := range other.InputVariables {
		diags = diags.Append(
			d.addInputVariable(decl),
		)
	}
	for _, decl := range other.LocalValues {
		diags = diags.Append(
			d.addLocalValue(decl),
		)
	}
	for _, decl := range other.OutputValues {
		diags = diags.Append(
			d.addOutputValue(decl),
		)
	}
	if other.RequiredProviders != nil {
		d.addRequiredProviders(other.RequiredProviders)
	}
	for _, decl := range other.ProviderConfigs {
		diags = diags.Append(
			d.addProviderConfig(decl),
		)
	}
	for _, blocks := range other.RemovedComponents.All() {
		for _, decl := range blocks {
			diags = diags.Append(
				d.addRemoved(decl),
			)
		}
	}

	return diags
}
