// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package planning

import (
	"fmt"

	"github.com/opentofu/opentofu/internal/addrs"
	"github.com/opentofu/opentofu/internal/engine/internal/exec"
	"github.com/opentofu/opentofu/internal/engine/internal/execgraph"
	"github.com/opentofu/opentofu/internal/plans"
)

////////////////////////////////////////////////////////////////////////////////
// This file contains methods of [execGraphBuilder] that are related to the
// parts of an execution graph that deal with resource instances of all modes.
////////////////////////////////////////////////////////////////////////////////

// AddResourceInstanceObjectSubgraphs adds all of the execution graph items
// needed to apply the planned changes for the given resource instance objects,
// including the operations required for the provider instances that those
// resource instances belong to.
func (b *execGraphBuilder) AddResourceInstanceObjectSubgraphs(
	objs *resourceInstanceObjects,
	effectiveReplaceOrders addrs.Map[addrs.AbsResourceInstanceObject, resourceInstanceReplaceOrder],
) {
	// TODO: We don't currently have any unit tests for this function. If this
	// survives into a shipping version of the planning engine then we should
	// write unit tests, and until then we should aim to keep this function
	// self-contained so that it _could_ be unit tested in isolation from the
	// rest of the planning engine.

	// TODO: With the earlier incarnation of execgraph building we assumed that
	// cycles in the execution graph were basically impossible because in all
	// cases except provider close we were always adding dependency before
	// dependent. This new model instead adds all of the subggraphs first and
	// then adds the explicit dependencies between them afterwards, so this
	// _could_ produce a cyclic graph if the input isn't valid. Can we do
	// something in here to detect cycles during the graph-building process,
	// or do we instead need a post-hoc validate step which applies Tarjan's
	// Strongly Connected Components algorithm to the execution graph?

	// resultRefs tracks the execgraph result reference for each resource
	// instance object, populated gradually as we build it out.
	resultRefs := addrs.MakeMap[addrs.AbsResourceInstanceObject, execgraph.ResourceInstanceResultRef]()
	// deletionRefs is like resultRefs except that it tracks the result of
	// a deletion step for each object. There's only an entry in this table
	// for objects whose subgraphs involve a deletion.
	deletionRefs := addrs.MakeMap[addrs.AbsResourceInstanceObject, execgraph.ResourceInstanceResultRef]()

	// addConfigDeps and addDeleteDeps both track functions we can use to add
	// additional dependencies to operations in the execution subgraphs of
	// different resource instance objects.
	//
	// addConfigDeps callbacks are for operations that must complete before
	// evaluating the configuration for an object, and so this captures the
	// relevant dependencies of each object.
	//
	// addDeleteDeps callbacks are for operations that must complete before
	// applying a "delete" plan for the object, and so these represent the
	// "reverse dependencies" between deleting things so that they get destroyed
	// in "inside out" dependency order.
	//
	// Not all resource instance objects will have elements in both of these
	// maps. For example, an addDeleteDeps entry is present only if the
	// execution subgraph for an object includes a ManagedApply operation
	// for a "delete" plan.
	addConfigDeps := addrs.MakeMap[addrs.AbsResourceInstanceObject, func(execgraph.AnyResultRef)]()
	addDeleteDeps := addrs.MakeMap[addrs.AbsResourceInstanceObject, func(execgraph.AnyResultRef)]()

	// providerClientRefs, addProviderConfigDeps, and addProviderCloseDeps
	// capture the three values we need to be able to connect a resource
	// instance with its provider instance.
	providerClientRefs := addrs.MakeMap[addrs.AbsProviderInstanceCorrect, execgraph.ResultRef[*exec.ProviderClient]]()
	addProviderConfigDeps := addrs.MakeMap[addrs.AbsProviderInstanceCorrect, func(execgraph.AnyResultRef)]()
	addProviderCloseDeps := addrs.MakeMap[addrs.AbsProviderInstanceCorrect, func(execgraph.AnyResultRef)]()

	// We pre-sort the keys here because that causes our execution graph
	// operations to be in a deterministic order, for easier unit testing and
	// easier reading of debug output.
	objAddrs := sortedResourceInstanceObjectAddrKeys(objs.All())

	// First we'll insert separate subgraphs for each resource instance object
	// that has a planned action, without putting any explicit dependency
	// edges between them yet. This loop also ensures that we have the
	// operations needed for any provider instance at least one object
	// belongs to.
	//
	// We'll insert the explicit dependency edges between the subgraphs in a
	// separate loop afterwards, along with any needed prior state operations
	// for objects that aren't changing.
	for _, addr := range objAddrs {
		obj := objs.Get(addr)
		plannedChange := obj.PlannedChange
		if plannedChange == nil {
			// For this first loop we only care about objects that have planned
			// changes. We'll fill in the subset of objects that aren't changing
			// afterwards only if at least one object that _is_ changing depends
			// on them.
			continue
		}
		// FIXME: We're currenly keeping the provider instance address in a
		// direct field of resourceInstanceObject instead of as part of the
		// plannedChange because we want to use our "correct" provider instance
		// address type. The documented rules for this field are that we expect
		// it to be valid when and only when obj.PlannedChange is not nil.
		providerInstAddr := obj.ProviderInst

		providerClientRef, ok := providerClientRefs.GetOk(providerInstAddr)
		var addProviderCloseDep func(execgraph.AnyResultRef)
		if !ok {
			var addProviderConfigDep func(execgraph.AnyResultRef)
			providerClientRef, addProviderConfigDep, addProviderCloseDep = b.ProviderInstanceSubgraph(providerInstAddr)
			providerClientRefs.Put(providerInstAddr, providerClientRef)
			addProviderConfigDeps.Put(providerInstAddr, addProviderConfigDep)
			addProviderCloseDeps.Put(providerInstAddr, addProviderCloseDep)
		} else {
			addProviderCloseDep = addProviderCloseDeps.Get(providerInstAddr)
		}

		valueRef, deletionRef, addConfigDep, addDeleteDep := b.resourceInstanceChangeSubgraph(
			plannedChange,
			effectiveReplaceOrders.Get(addr),
			providerClientRef,
		)

		// We'll use these two add*Dep functions in the second loop below as
		// we fill in all of the explicit dependencies caused by expressions
		// in the configuration.
		if addConfigDep != nil {
			resultRefs.Put(addr, valueRef)
			addConfigDeps.Put(addr, addConfigDep)
			addProviderCloseDep(valueRef)
		}
		if addDeleteDep != nil {
			deletionRefs.Put(addr, deletionRef)
			addDeleteDeps.Put(addr, addDeleteDep)
			addProviderCloseDep(deletionRef)
		}

		if addr.IsCurrent() {
			b.SetResourceInstanceFinalStateResult(addr.InstanceAddr, valueRef)
		}
	}

	// Now we'll add explicit dependencies between the subgraphs we just created
	// for the resource instance object changes. Any object that has a planned
	// change should already have entries in addConfigDeps/addDeleteDeps where
	// appropriate, but we will need to add prior-state-reading stubs for
	// any object that isn't being changed but is a dependency for something
	// that is changing.
	for _, addr := range objAddrs {
		if addConfigDep, ok := addConfigDeps.GetOk(addr); ok {
			for dependency := range objs.Dependencies(addr) {
				addConfigDep(ensureResourceInstanceObjectResultRef(dependency, resultRefs, b))
			}
		}
		if addDeleteDep, ok := addDeleteDeps.GetOk(addr); ok {
			for dependent := range objs.Dependendents(addr) {
				if ref, ok := deletionRefs.GetOk(dependent); ok {
					addDeleteDep(ref)
				}
			}
		}
	}

	// We also need explicit dependency relationships whenever a provider
	// instance's configuration refers to information from a resource instance.
	for _, elem := range addProviderConfigDeps.Elems {
		providerInstAddr := elem.Key
		addConfigDep := elem.Value
		for dependency := range objs.ProviderInstanceDependencies(providerInstAddr) {
			addConfigDep(ensureResourceInstanceObjectResultRef(dependency, resultRefs, b))
		}
	}
}

func ensureResourceInstanceObjectResultRef(addr addrs.AbsResourceInstanceObject, knownResults addrs.Map[addrs.AbsResourceInstanceObject, execgraph.ResourceInstanceResultRef], b *execGraphBuilder) execgraph.ResourceInstanceResultRef {
	if existing, ok := knownResults.GetOk(addr); ok {
		return existing
	}
	// If we don't already have an existing result then this is an object
	// that doesn't have any planned changes, so and we'll just provide
	// a minimum subgraph for it that only involves reading its prior state.
	var resultRef execgraph.ResourceInstanceResultRef
	if addr.IsCurrent() {
		resultRef = b.lower.ResourceInstancePrior(b.lower.ConstantResourceInstAddr(addr.InstanceAddr))
		b.SetResourceInstanceFinalStateResult(addr.InstanceAddr, resultRef)
	} else {
		resultRef = b.lower.ManagedAlreadyDeposed(b.lower.ConstantResourceInstAddr(addr.InstanceAddr), b.lower.ConstantDeposedKey(addr.DeposedKey))
	}
	knownResults.Put(addr, resultRef)
	return resultRef
}

func (b *execGraphBuilder) resourceInstanceChangeSubgraph(
	change *plans.ResourceInstanceChange,
	effectiveReplaceOrder resourceInstanceReplaceOrder,
	providerClientRef execgraph.ResultRef[*exec.ProviderClient],
) (
	valueRef, deletionRef execgraph.ResourceInstanceResultRef, // reference to the final new value and, if addDeleteDep is not nil, the deletion result
	addConfigDep, addDeleteDep func(execgraph.AnyResultRef), // callbacks to register explicit dependencies, or nil when not relevant
) {
	resourceMode := change.Addr.Resource.Resource.Mode
	switch resourceMode {
	case addrs.ManagedResourceMode:
		return b.ManagedResourceInstanceSubgraph(change, effectiveReplaceOrder, providerClientRef)

	// TODO: DataResourceMode, and possibly also EphemeralResourceMode if
	// we decide to handle those as "changes" (but it's currently looking
	// like they would be better handled in some other special way, since
	// they don't "change" in the same sense that other modes do.)
	default:
		// We should not get here because the above should cover all modes that
		// the earlier planning pass could possibly plan changes for.
		panic(fmt.Sprintf("can't build resource instance change subgraph for unexpected resource mode %s", resourceMode))
	}
}

// SetResourceInstanceFinalStateResult records which result should be treated
// as the "final state" for the given resource instance, for purposes such as
// propagating the result value back into the evaluation system to allow
// downstream expressions to derive from it.
//
// Only one call is allowed per distinct [addrs.AbsResourceInstance] value. If
// two callers try to register for the same address then the second call will
// panic.
func (b *execGraphBuilder) SetResourceInstanceFinalStateResult(addr addrs.AbsResourceInstance, result execgraph.ResourceInstanceResultRef) {
	b.mu.Lock()
	b.lower.SetResourceInstanceFinalStateResult(addr, result)
	b.mu.Unlock()
}
