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

package stackplan

import (
	"iter"
	"time"

	"github.com/zclconf/go-cty/cty"
	"google.golang.org/protobuf/types/known/anypb"

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

// Plan is the main type in this package, representing an entire stack plan,
// or at least the subset of the information that Terraform needs to reliably
// apply the plan and detect any inconsistencies during the apply process.
//
// However, the process of _creating_ a plan doesn't actually produce a single
// object of this type, and instead produces fragments of it gradually as the
// planning process proceeds. The caller of the stack runtime must retain
// all of the raw parts in the order they were emitted and provide them back
// during the apply phase, and then we will finally construct a single instance
// of Plan covering the entire set of changes before we begin applying it.
type Plan struct {
	// Applyable is true for a plan that was successfully created in full and
	// is sufficient to be applied, or false if the plan is incomplete for
	// some reason, such as if an error occurred during planning and so
	// the planning process did not entirely run.
	Applyable bool

	// Complete is true for a plan that shouldn't need any follow-up plans to
	// converge.
	Complete bool

	// Mode is the original mode of the plan.
	Mode plans.Mode

	// Root is the root StackInstance for the configuration being planned.
	// The StackInstance object wraps the specific components for each stack
	// instance.
	Root *StackInstance

	// The raw representation of the raw state that was provided in the request
	// to create the plan. We use this primarily to perform mundane state
	// data structure maintenence operations, such as discarding keys that
	// are no longer needed or replacing data in old formats with the
	// equivalent new representations.
	PrevRunStateRaw map[string]*anypb.Any

	// RootInputValues are the input variable values provided to calculate
	// the plan. We must use the same values during the apply step to
	// sure that the actions taken can be consistent with what was planned.
	RootInputValues map[stackaddrs.InputVariable]cty.Value

	// ApplyTimeInputVariables are the names of the root input variable
	// values whose values must be re-supplied during the apply phase,
	// instead of being persisted in [Plan.RootInputValues].
	ApplyTimeInputVariables collections.Set[stackaddrs.InputVariable]

	// DeletedInputVariables tracks the set of input variables that are being
	// deleted by this plan. The apply operation will miss any values
	// that are not defined in the configuration, but should still emit
	// deletion events to remove them from the state.
	DeletedInputVariables collections.Set[stackaddrs.InputVariable]

	// DeletedOutputValues tracks the set of output values that are being
	// deleted by this plan. The apply operation will miss any output values
	// that are not defined in the configuration, but should still emit
	// deletion events to remove them from the state. Output values not being
	// deleted will be recomputed during the apply so are not needed.
	DeletedOutputValues collections.Set[stackaddrs.OutputValue]

	// DeletedComponents are a set of components that are in the state that
	// should just be removed without any apply operation. This is typically
	// because they are not referenced in the configuration and have no
	// associated resources.
	DeletedComponents collections.Set[stackaddrs.AbsComponentInstance]

	// FunctionResults is a shared table of results from calling
	// provider functions. This is stored and loaded from during the planning
	// stage to use during apply operations.
	FunctionResults []lang.FunctionResultHash

	// PlanTimestamp is the time at which the plan was created.
	PlanTimestamp time.Time
}

func (p *Plan) AllComponents() iter.Seq2[stackaddrs.AbsComponentInstance, *Component] {
	return func(yield func(stackaddrs.AbsComponentInstance, *Component) bool) {
		p.Root.iterate(yield)
	}
}

func (p *Plan) ComponentInstanceAddresses(addr stackaddrs.AbsComponent) iter.Seq[stackaddrs.ComponentInstance] {
	return func(yield func(stackaddrs.ComponentInstance) bool) {
		stack := p.Root.GetDescendentStack(addr.Stack)
		if stack != nil {
			components := stack.Components[addr.Item]
			for key := range components {
				proceed := yield(stackaddrs.ComponentInstance{
					Component: addr.Item,
					Key:       key,
				})
				if !proceed {
					return
				}
			}
		}
	}
}

// ComponentInstances returns a set of the component instances that belong to
// the given component.
func (p *Plan) ComponentInstances(addr stackaddrs.AbsComponent) iter.Seq2[stackaddrs.ComponentInstance, *Component] {
	return func(yield func(stackaddrs.ComponentInstance, *Component) bool) {
		stack := p.Root.GetDescendentStack(addr.Stack)
		if stack != nil {
			components := stack.Components[addr.Item]
			for key, component := range components {
				proceed := yield(stackaddrs.ComponentInstance{
					Component: addr.Item,
					Key:       key,
				}, component)
				if !proceed {
					return
				}
			}
		}
	}
}

func (p *Plan) StackInstances(addr stackaddrs.AbsStackCall) iter.Seq[stackaddrs.StackInstance] {
	return func(yield func(stackaddrs.StackInstance) bool) {
		stack := p.Root.GetDescendentStack(addr.Stack)
		if stack != nil {
			stacks := stack.Children[addr.Item.Name]
			for key := range stacks {
				proceed := yield(append(addr.Stack, stackaddrs.StackInstanceStep{
					Name: addr.Item.Name,
					Key:  key,
				}))
				if !proceed {
					return
				}
			}
		}
	}
}

func (p *Plan) GetOrCreate(addr stackaddrs.AbsComponentInstance, component *Component) *Component {
	targetStackInstance := p.Root.GetOrCreateDescendentStack(addr.Stack)
	return targetStackInstance.GetOrCreateComponent(addr.Item, component)
}

func (p *Plan) GetComponent(addr stackaddrs.AbsComponentInstance) *Component {
	targetStackInstance := p.Root.GetDescendentStack(addr.Stack)
	return targetStackInstance.GetComponent(addr.Item)
}

func (p *Plan) GetStack(addr stackaddrs.StackInstance) *StackInstance {
	return p.Root.GetDescendentStack(addr)
}

// RequiredProviderInstances returns a description of all of the provider
// instance slots that are required to satisfy the resource instances
// belonging to the given component instance.
//
// See also stackeval.ComponentConfig.RequiredProviderInstances for a similar
// function that operates on the configuration of a component instance rather
// than the plan of one.
func (p *Plan) RequiredProviderInstances(addr stackaddrs.AbsComponentInstance) addrs.Set[addrs.RootProviderConfig] {
	stack := p.Root.GetDescendentStack(addr.Stack)
	if stack == nil {
		return addrs.MakeSet[addrs.RootProviderConfig]()
	}

	components, ok := stack.Components[addr.Item.Component]
	if !ok {
		return addrs.MakeSet[addrs.RootProviderConfig]()
	}

	component, ok := components[addr.Item.Key]
	if !ok {
		return addrs.MakeSet[addrs.RootProviderConfig]()
	}
	return component.RequiredProviderInstances()
}

// StackInstance stores the components and embedded stacks for a single stack
// instance.
type StackInstance struct {
	Address    stackaddrs.StackInstance
	Children   map[string]map[addrs.InstanceKey]*StackInstance
	Components map[stackaddrs.Component]map[addrs.InstanceKey]*Component
}

func newStackInstance(address stackaddrs.StackInstance) *StackInstance {
	return &StackInstance{
		Address:    address,
		Components: make(map[stackaddrs.Component]map[addrs.InstanceKey]*Component),
		Children:   make(map[string]map[addrs.InstanceKey]*StackInstance),
	}
}

func (stack *StackInstance) GetComponent(addr stackaddrs.ComponentInstance) *Component {
	components, ok := stack.Components[addr.Component]
	if !ok {
		return nil
	}
	return components[addr.Key]
}

func (stack *StackInstance) GetOrCreateComponent(addr stackaddrs.ComponentInstance, component *Component) *Component {
	components, ok := stack.Components[addr.Component]
	if !ok {
		components = make(map[addrs.InstanceKey]*Component)
	}
	existing, ok := components[addr.Key]
	if ok {
		return existing
	}
	components[addr.Key] = component
	stack.Components[addr.Component] = components
	return component
}

func (stack *StackInstance) GetOrCreateDescendentStack(addr stackaddrs.StackInstance) *StackInstance {
	if len(addr) == 0 {
		return stack
	}
	next := stack.GetOrCreateChildStack(addr[0])
	return next.GetOrCreateDescendentStack(addr[1:])
}

func (stack *StackInstance) GetOrCreateChildStack(step stackaddrs.StackInstanceStep) *StackInstance {
	child := stack.GetChildStack(step)
	if child == nil {
		child = stack.CreateChildStack(step)
	}
	return child
}

func (stack *StackInstance) GetDescendentStack(addr stackaddrs.StackInstance) *StackInstance {
	if len(addr) == 0 {
		return stack
	}

	next := stack.GetChildStack(addr[0])
	if next == nil {
		return nil
	}
	return next.GetDescendentStack(addr[1:])
}

func (stack *StackInstance) GetChildStack(step stackaddrs.StackInstanceStep) *StackInstance {
	insts, ok := stack.Children[step.Name]
	if !ok {
		return nil
	}
	return insts[step.Key]
}

func (stack *StackInstance) CreateChildStack(step stackaddrs.StackInstanceStep) *StackInstance {
	stacks, ok := stack.Children[step.Name]
	if !ok {
		stacks = make(map[addrs.InstanceKey]*StackInstance)
	}
	stacks[step.Key] = newStackInstance(append(stack.Address, step))
	stack.Children[step.Name] = stacks
	return stacks[step.Key]
}

func (stack *StackInstance) GetOk(addr stackaddrs.AbsComponentInstance) (*Component, bool) {
	if len(addr.Stack) == 0 {
		component, ok := stack.Components[addr.Item.Component]
		if !ok {
			return nil, false
		}

		instance, ok := component[addr.Item.Key]
		return instance, ok
	}

	stacks, ok := stack.Children[addr.Stack[0].Name]
	if !ok {
		return nil, false
	}
	next, ok := stacks[addr.Stack[0].Key]
	if !ok {
		return nil, false
	}
	return next.GetOk(stackaddrs.AbsComponentInstance{
		Stack: addr.Stack[1:],
		Item:  addr.Item,
	})
}

func (stack *StackInstance) iterate(yield func(stackaddrs.AbsComponentInstance, *Component) bool) bool {
	for name, components := range stack.Components {
		for key, component := range components {
			proceed := yield(stackaddrs.AbsComponentInstance{
				Stack: stack.Address,
				Item: stackaddrs.ComponentInstance{
					Component: name,
					Key:       key,
				},
			}, component)
			if !proceed {
				return false
			}
		}
	}

	for _, stacks := range stack.Children {
		for _, inst := range stacks {
			proceed := inst.iterate(yield)
			if !proceed {
				return false
			}
		}
	}

	return true
}
