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

package deferring

import (
	"fmt"
	"sync"

	"github.com/hashicorp/hcl/v2"
	"github.com/zclconf/go-cty/cty"

	"github.com/hashicorp/terraform/internal/addrs"
	"github.com/hashicorp/terraform/internal/lang/marks"
	"github.com/hashicorp/terraform/internal/plans"
	"github.com/hashicorp/terraform/internal/providers"
	"github.com/hashicorp/terraform/internal/tfdiags"
)

// Deferred keeps track of deferrals that have already happened, to help
// guide decisions about whether downstream operations might also need to be
// deferred.
type Deferred struct {

	// deferralAllowed marks whether deferred actions are supported by the
	// current runtime. At time of writing, the modules runtime does not support
	// deferral, but the stacks runtime does.
	deferralAllowed bool

	// externalDependencyDeferred marks the special situation where the
	// subsystem that's calling the modules runtime knows that some external
	// dependency of the configuration has deferred changes itself, and thus
	// all planned actions in this configuration must be deferred even if
	// the modules runtime can't find its own reason to do that.
	//
	// This is used by the stacks runtime when component B depends on
	// component A and component A's plan had deferred changes, so therefore
	// everything that component B might plan must also be deferred even
	// though the planning process for B cannot see into the plan for A.
	externalDependencyDeferred bool

	// Must hold this lock when accessing all fields after this one.
	mu sync.Mutex

	// dataSourceInstancesDeferred tracks the data source instances that have
	// been deferred despite their full addresses being known. This can happen
	// either because an upstream change was already deferred, or because
	// during planning the owning provider indicated that it doesn't yet have
	// enough information to produce a plan.
	//
	// These are grouped by the static resource configuration address because
	// there can potentially be various different deferrals for the same
	// configuration block at different amounts of instance expansion under
	// different prefixes, and so some queries require us to search across
	// all of those options to decide if each instance is relevant.
	dataSourceInstancesDeferred addrs.Map[addrs.ConfigResource, addrs.Map[addrs.AbsResourceInstance, *plans.DeferredResourceInstanceChange]]

	// resourceInstancesDeferred tracks the resource instances that have
	// been deferred despite their full addresses being known. This can happen
	// either because an upstream change was already deferred, or because
	// during planning the owning provider indicated that it doesn't yet have
	// enough information to produce a plan.
	//
	// These are grouped by the static resource configuration address because
	// there can potentially be various different deferrals for the same
	// configuration block at different amounts of instance expansion under
	// different prefixes, and so some queries require us to search across
	// all of those options to decide if each instance is relevant.
	resourceInstancesDeferred addrs.Map[addrs.ConfigResource, addrs.Map[addrs.AbsResourceInstance, *plans.DeferredResourceInstanceChange]]

	// ephemeralResourceInstancesDeferred tracks the ephemeral resource instances
	// that have been deferred despite their full addresses being known. This can happen
	// either because an upstream change was already deferred, or because
	// during planning the owning provider indicated that it doesn't yet have
	// enough information to produce a plan.
	//
	// These are grouped by the static resource configuration address because
	// there can potentially be various different deferrals for the same
	// configuration block at different amounts of instance expansion under
	// different prefixes, and so some queries require us to search across
	// all of those options to decide if each instance is relevant.
	ephemeralResourceInstancesDeferred addrs.Map[addrs.ConfigResource, addrs.Map[addrs.AbsResourceInstance, *plans.DeferredResourceInstanceChange]]

	// actionInvocationDeferred tracks the action invocations that have been
	// deferred despite their full addresses being known. This can happen
	// either because an upstream change was already deferred, or because
	// the action invocation is not yet ready to be executed.
	actionInvocationDeferred []*plans.DeferredActionInvocation

	// actionExpansionDeferred tracks the action expansions that have been
	// deferred. This can happen because the action expansion is not yet ready to be executed.
	actionExpansionDeferred addrs.Map[addrs.ConfigAction, addrs.Map[addrs.AbsActionInstance, providers.DeferredReason]]

	// partialExpandedResourcesDeferred tracks placeholders that cover an
	// unbounded set of potential resource instances in situations where we
	// don't yet even have enough information to predict which instances of
	// a resource will exist.
	//
	// These are grouped by the static resource configuration address because
	// there can potentially be various different deferrals for the same
	// configuration block at different amounts of instance expansion under
	// different prefixes, and so some queries require us to search across
	// all of those options to find the one that matches most closely.
	partialExpandedResourcesDeferred addrs.Map[addrs.ConfigResource, addrs.Map[addrs.PartialExpandedResource, *plans.DeferredResourceInstanceChange]]

	// partialExpandedDataSourcesDeferred tracks placeholders that cover an
	// unbounded set of potential data sources in situations where we don't yet
	// even have enough information to predict which instances of a data source
	// will exist.
	//
	// Data sources are never written into the plan, even when deferred, so we
	// are tracking these for purely internal reasons. If a resource depends on
	// a deferred data source, then that resource should be deferred as well.
	partialExpandedDataSourcesDeferred addrs.Map[addrs.ConfigResource, addrs.Map[addrs.PartialExpandedResource, *plans.DeferredResourceInstanceChange]]

	// partialExpandedEphemeralResourceDeferred tracks placeholders that cover an
	// unbounded set of potential data sources in situations where we don't yet
	// even have enough information to predict which instances of a data source
	// will exist.
	//
	// Data sources are never written into the plan, even when deferred, so we
	// are tracking these for purely internal reasons. If a resource depends on
	// a deferred data source, then that resource should be deferred as well.
	partialExpandedEphemeralResourceDeferred addrs.Map[addrs.ConfigResource, addrs.Map[addrs.PartialExpandedResource, *plans.DeferredResourceInstanceChange]]

	// partialExpandedActionsDeferred tracks placeholders that cover an
	// unbounded set of potential action instances in situations where we don't
	// yet have enough information to predict which concrete action instances
	// will exist (for example, because the action uses expressions that depend
	// on values not known until a prior deferred change is applied).
	//
	// These are grouped by the static action configuration address because
	// there can be several distinct partial-expansion situations for the same
	// configuration block reached through different module instance prefixes
	// or differing dynamic collection lengths. Some queries therefore must
	// search across all of those to determine whether a particular action
	// instance should be considered deferred.
	partialExpandedActionsDeferred addrs.Map[addrs.ConfigAction, addrs.Map[addrs.PartialExpandedAction, providers.DeferredReason]]

	// partialExpandedModulesDeferred tracks all of the partial-expanded module
	// prefixes we were notified about.
	//
	// We don't need to track anything for these other than that we saw them
	// reported, because the relevant data is tracked in [instances.Expander]
	// and [namedvals.State], but we do need to remember the addresses just
	// so that we can inform the caller that there was something deferred
	// even if there weren't any resources beneath the partial-expanded prefix.
	//
	// (If we didn't catch that then we'd mislead the caller into thinking
	// we fully-evaluated everything, which would be incorrect if any of the
	// root module output values are derived from the results of the
	// partial-expanded calls.)
	partialExpandedModulesDeferred addrs.Set[addrs.PartialExpandedModule]
}

// NewDeferred constructs a new empty [Deferred] object. The enabled argument
// controls whether the receiver will actually track any deferrals. If false,
// all methods will return false and no deferrals will be recorded.
func NewDeferred(enabled bool) *Deferred {
	return &Deferred{
		deferralAllowed:                          enabled,
		resourceInstancesDeferred:                addrs.MakeMap[addrs.ConfigResource, addrs.Map[addrs.AbsResourceInstance, *plans.DeferredResourceInstanceChange]](),
		ephemeralResourceInstancesDeferred:       addrs.MakeMap[addrs.ConfigResource, addrs.Map[addrs.AbsResourceInstance, *plans.DeferredResourceInstanceChange]](),
		dataSourceInstancesDeferred:              addrs.MakeMap[addrs.ConfigResource, addrs.Map[addrs.AbsResourceInstance, *plans.DeferredResourceInstanceChange]](),
		actionInvocationDeferred:                 []*plans.DeferredActionInvocation{},
		actionExpansionDeferred:                  addrs.MakeMap[addrs.ConfigAction, addrs.Map[addrs.AbsActionInstance, providers.DeferredReason]](),
		partialExpandedResourcesDeferred:         addrs.MakeMap[addrs.ConfigResource, addrs.Map[addrs.PartialExpandedResource, *plans.DeferredResourceInstanceChange]](),
		partialExpandedDataSourcesDeferred:       addrs.MakeMap[addrs.ConfigResource, addrs.Map[addrs.PartialExpandedResource, *plans.DeferredResourceInstanceChange]](),
		partialExpandedEphemeralResourceDeferred: addrs.MakeMap[addrs.ConfigResource, addrs.Map[addrs.PartialExpandedResource, *plans.DeferredResourceInstanceChange]](),
		partialExpandedActionsDeferred:           addrs.MakeMap[addrs.ConfigAction, addrs.Map[addrs.PartialExpandedAction, providers.DeferredReason]](),
		partialExpandedModulesDeferred:           addrs.MakeSet[addrs.PartialExpandedModule](),
	}
}

// GetDeferredChanges returns a slice of all the deferred changes that have
// been reported to the receiver.
func (d *Deferred) GetDeferredChanges() []*plans.DeferredResourceInstanceChange {
	var changes []*plans.DeferredResourceInstanceChange

	if !d.deferralAllowed {
		return changes
	}

	for _, configMapElem := range d.resourceInstancesDeferred.Elems {
		for _, changeElem := range configMapElem.Value.Elems {
			changes = append(changes, changeElem.Value)
		}
	}
	for _, configMapElem := range d.dataSourceInstancesDeferred.Elems {
		for _, changeElem := range configMapElem.Value.Elems {
			changes = append(changes, changeElem.Value)
		}
	}
	for _, configMapElem := range d.partialExpandedResourcesDeferred.Elems {
		for _, changeElem := range configMapElem.Value.Elems {
			changes = append(changes, changeElem.Value)
		}
	}
	for _, configMapElem := range d.partialExpandedDataSourcesDeferred.Elems {
		for _, changeElem := range configMapElem.Value.Elems {
			changes = append(changes, changeElem.Value)
		}
	}
	return changes
}

// GetDeferredActionInvocations returns a list of all deferred action invocations.
func (d *Deferred) GetDeferredActionInvocations() []*plans.DeferredActionInvocation {
	return d.actionInvocationDeferred
}

// SetExternalDependencyDeferred modifies a freshly-constructed [Deferred]
// so that it will consider all resource instances as needing their actions
// deferred, even if there's no other reason to do that.
//
// This must be called zero or one times before any other use of the receiver.
// Changing this setting after a [Deferred] has already been used, or
// concurrently with any other method call, will cause inconsistent and
// undefined behavior.
func (d *Deferred) SetExternalDependencyDeferred() {
	d.externalDependencyDeferred = true
}

// DeferralAllowed checks whether deferred actions are supported by the current
// runtime.
func (d *Deferred) DeferralAllowed() bool {
	// Gracefully recover from being called on nil, for tests that use
	// MockEvalContext without a real Deferred pointer set up.
	if d == nil {
		return false
	}
	return d.deferralAllowed
}

// HaveAnyDeferrals returns true if at least one deferral has been registered
// with the receiver.
//
// This method is intended as a summary result to propagate to the modules
// runtime caller so it can know if it should treat any downstream objects
// as having their own changes deferred without having to duplicate the
// modules runtime's rules for what counts as a deferral.
func (d *Deferred) HaveAnyDeferrals() bool {
	return d.deferralAllowed &&
		(d.externalDependencyDeferred ||
			d.resourceInstancesDeferred.Len() != 0 ||
			d.dataSourceInstancesDeferred.Len() != 0 ||
			d.ephemeralResourceInstancesDeferred.Len() != 0 ||
			len(d.actionInvocationDeferred) != 0 ||
			d.partialExpandedResourcesDeferred.Len() != 0 ||
			d.partialExpandedDataSourcesDeferred.Len() != 0 ||
			d.partialExpandedEphemeralResourceDeferred.Len() != 0 ||
			d.partialExpandedActionsDeferred.Len() != 0 ||
			len(d.partialExpandedModulesDeferred) != 0)
}

// GetDeferredResourceInstanceValue returns the deferred value for the given
// resource instance, if any.
func (d *Deferred) GetDeferredResourceInstanceValue(addr addrs.AbsResourceInstance) (cty.Value, bool) {
	if !d.deferralAllowed {
		return cty.NilVal, false
	}

	d.mu.Lock()
	defer d.mu.Unlock()

	configAddr := addr.ConfigResource()
	var instancesMap addrs.Map[addrs.ConfigResource, addrs.Map[addrs.AbsResourceInstance, *plans.DeferredResourceInstanceChange]]

	switch addr.Resource.Resource.Mode {
	case addrs.ManagedResourceMode:
		instancesMap = d.resourceInstancesDeferred
	case addrs.DataResourceMode:
		instancesMap = d.dataSourceInstancesDeferred
	case addrs.EphemeralResourceMode:
		instancesMap = d.ephemeralResourceInstancesDeferred
	default:
		panic(fmt.Sprintf("unexpected resource mode %q for %s", addr.Resource.Resource.Mode, addr))
	}

	change, ok := instancesMap.Get(configAddr).GetOk(addr)
	if !ok {
		return cty.NilVal, false
	}

	return change.Change.After, true
}

// GetDeferredResourceInstances returns a map of all the deferred instances of
// the given resource.
func (d *Deferred) GetDeferredResourceInstances(addr addrs.AbsResource) map[addrs.InstanceKey]cty.Value {
	if !d.deferralAllowed {
		return nil
	}

	d.mu.Lock()
	defer d.mu.Unlock()

	configAddr := addr.Config()
	var instancesMap addrs.Map[addrs.ConfigResource, addrs.Map[addrs.AbsResourceInstance, *plans.DeferredResourceInstanceChange]]

	switch addr.Resource.Mode {
	case addrs.ManagedResourceMode:
		instancesMap = d.resourceInstancesDeferred
	case addrs.DataResourceMode:
		instancesMap = d.dataSourceInstancesDeferred
	case addrs.EphemeralResourceMode:
		instancesMap = d.ephemeralResourceInstancesDeferred

	default:
		panic(fmt.Sprintf("unexpected resource mode %q for %s", addr.Resource.Mode, addr))
	}

	instances, ok := instancesMap.GetOk(configAddr)
	if !ok {
		return nil
	}

	result := make(map[addrs.InstanceKey]cty.Value)
	for _, elem := range instances.Elems {
		instanceAddr := elem.Key
		change := elem.Value

		if addr.Resource.Mode == addrs.EphemeralResourceMode {
			// Deferred ephemeral resources always have an unknown value.
			result[instanceAddr.Resource.Key] = cty.UnknownVal(cty.DynamicPseudoType).Mark(marks.Ephemeral)
			continue
		}
		// instances contains all the resources identified by the config address
		// regardless of the instances of the module they might be in. We need
		// to filter out the instances that are not part of the module we are
		// interested in.
		if addr.Equal(instanceAddr.ContainingResource()) {
			result[instanceAddr.Resource.Key] = change.Change.After
		}
	}
	return result
}

func (d *Deferred) GetDeferredPartialExpandedResource(addr addrs.PartialExpandedResource) *plans.DeferredResourceInstanceChange {
	d.mu.Lock()
	defer d.mu.Unlock()

	item, ok := d.partialExpandedResourcesDeferred.GetOk(addr.ConfigResource())
	if !ok {
		return nil
	}

	return item.Get(addr)
}

// ShouldDeferResourceInstanceChanges returns true if the receiver knows some
// reason why the resource instance with the given address should have its
// planned action deferred for a future plan/apply round.
//
// This method is specifically for resource instances whose full address is
// known and thus it would be possible in principle to plan changes, but we
// still need to respect dependency ordering and so any planned changes must
// be deferred if any upstream planned action was already deferred for
// some reason.
//
// Callers who get the answer true should announce an approximation of the
// action they would have planned to [Deferred.ReportResourceInstanceDeferred],
// but should skip writing that change into the live plan so that downstream
// evaluation will be based on the prior state (similar to in a refresh-only
// plan) rather than the result of the deferred action.
//
// It's invalid to call this method for an address that was already reported
// as deferred using [Deferred.ReportResourceInstanceDeferred], and so this
// method will panic in that case.
func (d *Deferred) ShouldDeferResourceInstanceChanges(addr addrs.AbsResourceInstance, deps []addrs.ConfigResource) bool {
	if !d.deferralAllowed {
		return false
	}
	configAddr := addr.ConfigResource()

	// Since d.DependenciesDeferred will also acquire the lock we don't use
	// the normal defer d.mu.Unlock() but handle it manually.
	d.mu.Lock()
	if d.resourceInstancesDeferred.Get(configAddr).Has(addr) || d.dataSourceInstancesDeferred.Get(configAddr).Has(addr) || d.ephemeralResourceInstancesDeferred.Get(configAddr).Has(addr) {
		d.mu.Unlock()
		// Asking for whether a resource instance should be deferred when
		// it was already reported as deferred suggests a programming error
		// in the caller, because the test for whether a change should be
		// deferred should always come before reporting that it has been.
		panic(fmt.Sprintf("checking whether %s should be deferred when it was already deferred", addr))
	}
	d.mu.Unlock()

	return d.DependenciesDeferred(deps)
}

// DependenciesDeferred returns true if any of the given configuration
// resources have had their planned actions deferred, either because they
// themselves were deferred or because they depend on something that was
// deferred.
//
// As
func (d *Deferred) DependenciesDeferred(deps []addrs.ConfigResource) bool {
	if !d.deferralAllowed {
		return false
	}

	d.mu.Lock()
	defer d.mu.Unlock()

	if d.externalDependencyDeferred {
		return true
	}

	// If neither of our resource-deferral-tracking collections have anything
	// in them then we definitely don't need to defer. This special case is
	// here primarily to minimize the amount of code from here that will run
	// when the deferred-actions-related experiments are inactive, so we can
	// minimize the risk of impacting non-participants.
	// (Maybe we'll remove this check once this stuff is non-experimental.)
	if d.resourceInstancesDeferred.Len() == 0 &&
		d.dataSourceInstancesDeferred.Len() == 0 &&
		d.ephemeralResourceInstancesDeferred.Len() == 0 &&
		d.partialExpandedResourcesDeferred.Len() == 0 &&
		d.partialExpandedDataSourcesDeferred.Len() == 0 &&
		d.partialExpandedEphemeralResourceDeferred.Len() == 0 {
		return false
	}

	// For this initial implementation we're taking the shortcut of assuming
	// that all of the configDeps are required. It would be better to do a
	// more precise analysis that takes into account how data could possibly
	// flow between instances of resources across different module paths,
	// but that may have some subtlety due to dynamic data flow, so we'll
	// need to do some more theory work to figure out what kind of analysis
	// we'd need to do to get this to be more precise.
	//
	// This conservative approach is a starting point so we can focus on
	// developing the workflow around deferred changes before making its
	// analyses more precise. This will defer more changes than strictly
	// necessary, but that's better than not deferring changes that should
	// have been deferred.
	//
	// (FWIW, it does seem like we _should_ be able to eliminate some
	// dynamic instances from consideration by relying on constraints such as
	// how a multi-instance module call can't have an object in one instance
	// depending on an object for another instance, but we'll need to make sure
	// any additional logic here is well-reasoned to avoid violating dependency
	// invariants.)
	for _, configDep := range deps {
		if d.resourceInstancesDeferred.Has(configDep) || d.dataSourceInstancesDeferred.Has(configDep) || d.ephemeralResourceInstancesDeferred.Has(configDep) {
			// For now we don't consider exactly which instances of that
			// configuration block were deferred; there being at least
			// one is enough.
			return true
		}
		if d.partialExpandedResourcesDeferred.Has(configDep) {
			return true
		}
		if d.partialExpandedDataSourcesDeferred.Has(configDep) {
			return true
		}
		if d.partialExpandedEphemeralResourceDeferred.Has(configDep) {
			return true
		}

		// We don't check d.partialExpandedModulesDeferred here because
		// we expect that the graph nodes representing any resource under
		// a partial-expanded module prefix to call
		// d.ReportResourceExpansionDeferred once they find out that they
		// are under a partial-expanded prefix, and so
		// partialExpandedModulesDeferred is effectively just a less-detailed
		// summary of the information in partialExpandedResourcesDeferred.
		// (instances.Expander is the one responsible for letting the resource
		// node discover that it needs to do that; package deferred does
		// not participate directly in that concern.)
	}
	return false
}

// ReportResourceExpansionDeferred reports that we cannot even predict which
// instances of a resource will be declared and thus we must defer all planning
// for that resource.
func (d *Deferred) ReportResourceExpansionDeferred(addr addrs.PartialExpandedResource, change *plans.ResourceInstanceChange) {
	if change == nil {
		// This indicates a bug in Terraform, we shouldn't ever be setting a
		// null change. Note, if we don't make this check here, then we'll
		// just crash later anyway. This way the stack trace points to the
		// source of the problem.
		panic("change must not be nil")
	}

	d.mu.Lock()
	defer d.mu.Unlock()

	if addr.Resource().Mode != addrs.ManagedResourceMode {
		// Use ReportDataSourceExpansionDeferred for data sources and ReportEphemeralResourceExpansionDeferred for ephemeral resources.
		panic(fmt.Sprintf("unexpected resource mode %q for %s", addr.Resource().Mode, addr))
	}

	configAddr := addr.ConfigResource()
	if !d.partialExpandedResourcesDeferred.Has(configAddr) {
		d.partialExpandedResourcesDeferred.Put(configAddr, addrs.MakeMap[addrs.PartialExpandedResource, *plans.DeferredResourceInstanceChange]())
	}

	configMap := d.partialExpandedResourcesDeferred.Get(configAddr)
	if configMap.Has(addr) {
		// This indicates a bug in the caller, since our graph walk should
		// ensure that we visit and evaluate each distinct partial-expanded
		// prefix only once.
		panic(fmt.Sprintf("duplicate deferral report for %s", addr))
	}
	configMap.Put(addr, &plans.DeferredResourceInstanceChange{
		DeferredReason: providers.DeferredReasonInstanceCountUnknown,
		Change:         change,
	})
}

// ReportDataSourceExpansionDeferred reports that we cannot even predict which
// instances of a data source will be declared and thus we must defer all
// planning for that data source.
func (d *Deferred) ReportDataSourceExpansionDeferred(addr addrs.PartialExpandedResource, change *plans.ResourceInstanceChange) {
	if change == nil {
		// This indicates a bug in Terraform, we shouldn't ever be setting a
		// null change. Note, if we don't make this check here, then we'll
		// just crash later anyway. This way the stack trace points to the
		// source of the problem.
		panic("change must not be nil")
	}

	d.mu.Lock()
	defer d.mu.Unlock()

	if addr.Resource().Mode != addrs.DataResourceMode {
		// Use ReportResourceExpansionDeferred for resources and ReportEphemeralResourceExpansionDeferred for ephemeral resources.
		panic(fmt.Sprintf("unexpected resource mode %q for %s", addr.Resource().Mode, addr))
	}

	configAddr := addr.ConfigResource()
	if !d.partialExpandedDataSourcesDeferred.Has(configAddr) {
		d.partialExpandedDataSourcesDeferred.Put(configAddr, addrs.MakeMap[addrs.PartialExpandedResource, *plans.DeferredResourceInstanceChange]())
	}

	configMap := d.partialExpandedDataSourcesDeferred.Get(configAddr)
	if configMap.Has(addr) {
		// This indicates a bug in the caller, since our graph walk should
		// ensure that we visit and evaluate each distinct partial-expanded
		// prefix only once.
		panic(fmt.Sprintf("duplicate deferral report for %s", addr))
	}
	configMap.Put(addr, &plans.DeferredResourceInstanceChange{
		DeferredReason: providers.DeferredReasonInstanceCountUnknown,
		Change:         change,
	})
}

func (d *Deferred) ReportEphemeralResourceExpansionDeferred(addr addrs.PartialExpandedResource) {
	d.mu.Lock()
	defer d.mu.Unlock()

	if addr.Resource().Mode != addrs.EphemeralResourceMode {
		// Use ReportResourceExpansionDeferred for resources and ReportDataSourceExpansionDeferred for data sources.
		panic(fmt.Sprintf("unexpected resource mode %q for %s", addr.Resource().Mode, addr))
	}

	configAddr := addr.ConfigResource()
	if !d.partialExpandedEphemeralResourceDeferred.Has(configAddr) {
		d.partialExpandedEphemeralResourceDeferred.Put(configAddr, addrs.MakeMap[addrs.PartialExpandedResource, *plans.DeferredResourceInstanceChange]())
	}

	configMap := d.partialExpandedEphemeralResourceDeferred.Get(configAddr)
	if configMap.Has(addr) {
		// This indicates a bug in the caller, since our graph walk should
		// ensure that we visit and evaluate each distinct partial-expanded
		// prefix only once.
		panic(fmt.Sprintf("duplicate deferral report for %s", addr))
	}
	configMap.Put(addr, &plans.DeferredResourceInstanceChange{
		DeferredReason: providers.DeferredReasonInstanceCountUnknown,
		Change:         nil, // since we don't serialize this we can get away with no change, we store the addr, that should be enough
	})
}

func (d *Deferred) ReportActionExpansionDeferred(addr addrs.PartialExpandedAction) {
	d.mu.Lock()
	defer d.mu.Unlock()

	configAddr := addr.ConfigAction()
	if !d.partialExpandedActionsDeferred.Has(configAddr) {
		d.partialExpandedActionsDeferred.Put(configAddr, addrs.MakeMap[addrs.PartialExpandedAction, providers.DeferredReason]())
	}

	configMap := d.partialExpandedActionsDeferred.Get(configAddr)
	if configMap.Has(addr) {
		// This indicates a bug in the caller, since our graph walk should
		// ensure that we visit and evaluate each distinct partial-expanded
		// prefix only once.
		panic(fmt.Sprintf("duplicate deferral report for %s", addr))
	}
	configMap.Put(addr, providers.DeferredReasonInstanceCountUnknown)
}

// ReportResourceInstanceDeferred records that a fully-expanded resource
// instance has had its planned action deferred to a future round for a reason
// other than its address being only partially-decided.
func (d *Deferred) ReportResourceInstanceDeferred(addr addrs.AbsResourceInstance, reason providers.DeferredReason, change *plans.ResourceInstanceChange) {
	if change == nil {
		// This indicates a bug in Terraform, we shouldn't ever be setting a
		// null change. Note, if we don't make this check here, then we'll
		// just crash later anyway. This way the stack trace points to the
		// source of the problem.
		panic("change must not be nil")
	}

	d.mu.Lock()
	defer d.mu.Unlock()

	configAddr := addr.ConfigResource()
	if !d.resourceInstancesDeferred.Has(configAddr) {
		d.resourceInstancesDeferred.Put(configAddr, addrs.MakeMap[addrs.AbsResourceInstance, *plans.DeferredResourceInstanceChange]())
	}

	configMap := d.resourceInstancesDeferred.Get(configAddr)
	if configMap.Has(addr) {
		// This indicates a bug in the caller, since our graph walk should
		// ensure that we visit and evaluate each resource instance only once.
		panic(fmt.Sprintf("duplicate deferral report for %s", addr))
	}
	configMap.Put(addr, &plans.DeferredResourceInstanceChange{
		DeferredReason: reason,
		Change:         change,
	})
}

func (d *Deferred) ReportDataSourceInstanceDeferred(addr addrs.AbsResourceInstance, reason providers.DeferredReason, change *plans.ResourceInstanceChange) {
	if change == nil {
		// This indicates a bug in Terraform, we shouldn't ever be setting a
		// null change. Note, if we don't make this check here, then we'll
		// just crash later anyway. This way the stack trace points to the
		// source of the problem.
		panic("change must not be nil")
	}

	d.mu.Lock()
	defer d.mu.Unlock()

	configAddr := addr.ConfigResource()
	if !d.dataSourceInstancesDeferred.Has(configAddr) {
		d.dataSourceInstancesDeferred.Put(configAddr, addrs.MakeMap[addrs.AbsResourceInstance, *plans.DeferredResourceInstanceChange]())
	}

	configMap := d.dataSourceInstancesDeferred.Get(configAddr)
	if configMap.Has(addr) {
		// This indicates a bug in the caller, since our graph walk should
		// ensure that we visit and evaluate each resource instance only once.
		panic(fmt.Sprintf("duplicate deferral report for %s", addr))
	}
	configMap.Put(addr, &plans.DeferredResourceInstanceChange{
		DeferredReason: reason,
		Change:         change,
	})
}

func (d *Deferred) ReportEphemeralResourceInstanceDeferred(addr addrs.AbsResourceInstance, reason providers.DeferredReason) {
	d.mu.Lock()
	defer d.mu.Unlock()

	configAddr := addr.ConfigResource()
	if !d.ephemeralResourceInstancesDeferred.Has(configAddr) {
		d.ephemeralResourceInstancesDeferred.Put(configAddr, addrs.MakeMap[addrs.AbsResourceInstance, *plans.DeferredResourceInstanceChange]())
	}

	configMap := d.ephemeralResourceInstancesDeferred.Get(configAddr)
	if configMap.Has(addr) {
		// This indicates a bug in the caller, since our graph walk should
		// ensure that we visit and evaluate each resource instance only once.
		panic(fmt.Sprintf("duplicate deferral report for %s", addr))
	}
	configMap.Put(addr, &plans.DeferredResourceInstanceChange{
		DeferredReason: reason,
		Change:         nil, // Since we don't serialize this we can get away with not storing a change
	})
}

// ReportModuleExpansionDeferred reports that we cannot even predict which
// instances of a module call will be declared and thus we must defer all
// planning for everything inside that module.
//
// Use the most precise partial-expanded module address possible.
func (d *Deferred) ReportModuleExpansionDeferred(addr addrs.PartialExpandedModule) {
	if d.partialExpandedModulesDeferred.Has(addr) {
		// This indicates a bug in the caller, since our graph walk should
		// ensure that we visit and evaluate each distinct partial-expanded
		// prefix only once.
		panic(fmt.Sprintf("duplicate deferral report for %s", addr))
	}
	d.partialExpandedModulesDeferred.Add(addr)
}

func (d *Deferred) ReportActionInvocationDeferred(ai plans.ActionInvocationInstance, reason providers.DeferredReason) {
	d.mu.Lock()
	defer d.mu.Unlock()

	// Check if the action invocation is already deferred
	for _, deferred := range d.actionInvocationDeferred {
		if deferred.ActionInvocationInstance.Equals(&ai) {
			// This indicates a bug in the caller, since our graph walk should
			// ensure that we visit and evaluate each distinct action invocation
			// only once.
			panic(fmt.Sprintf("duplicate deferral report for action %s invoked by %s", ai.Addr.String(), ai.ActionTrigger.TriggerEvent().String()))
		}
	}

	d.actionInvocationDeferred = append(d.actionInvocationDeferred, &plans.DeferredActionInvocation{
		ActionInvocationInstance: &ai,
		DeferredReason:           reason,
	})
}

func (d *Deferred) ReportActionDeferred(addr addrs.AbsActionInstance, reason providers.DeferredReason) {
	d.mu.Lock()
	defer d.mu.Unlock()

	configAddr := addr.ConfigAction()
	if !d.actionExpansionDeferred.Has(configAddr) {
		d.actionExpansionDeferred.Put(configAddr, addrs.MakeMap[addrs.AbsActionInstance, providers.DeferredReason]())
	}

	configMap := d.actionExpansionDeferred.Get(configAddr)
	if configMap.Has(addr) {
		// This indicates a bug in the caller, since our graph walk should
		// ensure that we visit and evaluate each resource instance only once.
		panic(fmt.Sprintf("duplicate deferral report for %s", addr))
	}
	configMap.Put(addr, reason)
}

// ShouldDeferActionInvocation returns true if there is a reason to defer the
// action invocation instance. We want to defer an action invocation only if
// the triggering resource was deferred. In addition, we will check if the
// underlying action was deferred via a reference, and consider it an error if
// the triggering resource wasn't also deferred.
//
// The reason behind the slightly different behaviour here, is that if an
// action invocation is deferred, then that implies the triggering action
// should also be deferred.
//
// We don't yet have the capability to retroactively defer a resource, so for
// now actions initiating deferrals themselves is considered an error.
func (d *Deferred) ShouldDeferActionInvocation(ai plans.ActionInvocationInstance, triggerRange *hcl.Range) (bool, tfdiags.Diagnostics) {
	d.mu.Lock()
	defer d.mu.Unlock()

	var diags tfdiags.Diagnostics

	// We only want to defer actions that are lifecycle triggered
	at, ok := ai.ActionTrigger.(*plans.ResourceActionTrigger)
	if !ok {
		return false, diags
	}

	// If the resource was deferred, we also need to defer any action potentially triggering from this
	if configResourceMap, ok := d.resourceInstancesDeferred.GetOk(at.TriggeringResourceAddr.ConfigResource()); ok {
		if configResourceMap.Has(at.TriggeringResourceAddr) {
			return true, diags
		}
	}

	if c, ok := d.actionExpansionDeferred.GetOk(ai.Addr.ConfigAction()); ok {
		if c.Has(ai.Addr) {
			// Then in this case, the resource wasn't deferred but the action
			// was and so we will consider this to be an error.
			diags = diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Invalid action deferral",
				Detail:   fmt.Sprintf("The action %s was marked as deferred, but was triggered by a non-deferred resource %s. To work around this, use the -target argument to first apply only the resources that the action block depends on.", ai.Addr, at.TriggeringResourceAddr),
				Subject:  triggerRange,
			})
		}
	}

	return false, diags
}

// ShouldDeferAction returns true if the action should be deferred. This is the case if a
// dependency of the action is deferred.
func (d *Deferred) ShouldDeferAction(deps []addrs.ConfigResource) bool {
	return d.DependenciesDeferred(deps)
}

// UnexpectedProviderDeferralDiagnostic is a diagnostic that indicates that a
// provider was deferred although deferrals were not allowed.
func UnexpectedProviderDeferralDiagnostic(addrs fmt.Stringer) tfdiags.Diagnostic {
	return tfdiags.Sourceless(tfdiags.Error, "Provider deferred changes when Terraform did not allow deferrals", fmt.Sprintf("The provider signaled a deferred action for %q, but in this context deferrals are disabled. This is a bug in the provider, please file an issue with the provider developers.", addrs.String()))
}
