// 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/plans"
)

// findEffectiveReplaceOrders analyzes the given graph of resource instance
// objects to decide the final effective "replace order" for each resource
// instance object.
//
// Specifically, any object whose initial replace order is [replaceAnyOrder]
// will have its effective order set to either [replaceDestroyThenCreate] or
// [replaceCreateThenDestroy], depending on whether they are in dependency
// chains with objects those initial replace order was
// [replaceCreateThenDestroy]. All objects in a chain of dependencies are
// required to have the same replace order.
//
// The second return value is a set of addresses of objects which depend on
// themselves either directly or indirectly, which should be impossible if the
// graph was constructed correctly. If that set contains any elements then the
// map of effective replace orders is likely to be incomplete.
//
// This function currently assumes that all of the provided objects have their
// initial replace order set to either [replaceAnyOrder] or
// [replaceCreateThenDestroy]. If any of the given objects have the initial
// order [replaceDestroyThenCreate] then this function will panic; that
// replace order is used only as the _effective_ replace order for any object
// that isn't chained with an object whose initial order is
// [replaceCreateThenDestroy]. This models the current constraints of the
// surface language where "create_before_destroy = true" is treated as
// [replaceCreateThenDestroy] and everything else is treated as
// [replaceAnyOrder].
func findEffectiveReplaceOrders(objs *resourceInstanceObjects) (addrs.Map[addrs.AbsResourceInstanceObject, resourceInstanceReplaceOrder], addrs.Set[addrs.AbsResourceInstanceObject]) {
	orders := addrs.MakeMap[addrs.AbsResourceInstanceObject, resourceInstanceReplaceOrder]()
	selfDeps := addrs.MakeSet[addrs.AbsResourceInstanceObject]()

	// This initial implementation is pretty simplistic: we just visit every
	// object in an arbitrary order and then visit each of its dependencies
	// and dependents in an arbitrary order until either we find one that
	// requires "create then destroy" or until we run out of objects to check.
	//
	// Maybe later we'll devise a cleverer algorithm for this which doesn't
	// involve revisiting the same objects quite as much. For now our only
	// minor optimization is to stop as soon as we find the first neighbor
	// with [replaceCreateThenDestroy].

Objects:
	for currentInst, currentObj := range objs.All() {
		if currentObj.ReplaceOrder == replaceCreateThenDestroy {
			// Easy case: this one is definitely create-then-destroy.
			orders.Put(currentInst, replaceCreateThenDestroy)
			continue
		}

		for otherInst := range objs.DependenciesAndDependents(currentInst) {
			if otherInst.Equal(currentInst) {
				// We've found a self-dependency problem, so we'll record
				// it but continue anyway because the rest of this algorithm
				// can tolerate that sitution for now.
				selfDeps.Add(currentInst)
				continue
			}

			if currentObj.ReplaceOrder != replaceAnyOrder && currentObj.ReplaceOrder != replaceCreateThenDestroy {
				panic(fmt.Sprintf("%s has invalid initial replace order %s", currentInst, currentObj.ReplaceOrder))
			}

			// If we've already recorded a decision for this one then we'll
			// prefer to use that decision. At this point in the process that
			// decision can only be [replaceCreateThenDestroy], because we
			// don't populate any others until after these loops are complete.
			if previous, ok := orders.GetOk(otherInst); ok {
				orders.Put(currentInst, previous)
				continue Objects
			}

			otherObj := objs.Get(otherInst)
			if otherObj == nil {
				// Can potentially happen if the resource instance graph is
				// incomplete, such as if the input config was invalid. In
				// that case we're just making a best effort to finalize a
				// partial plan, so we'll ignore the invalid item.
				continue
			}
			if otherObj.ReplaceOrder == replaceCreateThenDestroy {
				orders.Put(currentInst, replaceCreateThenDestroy)
			}
		}
	}

	// Now we'll make a followup pass and just set everything we didn't already
	// decide to replaceDestroyThenCreate, which is the default.
	for currentInst := range objs.All() {
		if !orders.Has(currentInst) {
			orders.Put(currentInst, replaceDestroyThenCreate)
		}
	}

	return orders, selfDeps
}

// resourceInstanceReplaceOrder represents the constraint, if any, for what
// order the create and destroy steps of a "replace" action must happen in.
type resourceInstanceReplaceOrder int

//go:generate go tool golang.org/x/tools/cmd/stringer -type=resourceInstanceReplaceOrder -trimprefix=replace

const (
	// replaceAnyOrder means that it's okay to use either order, in which
	// case the associated resource instance will just follow the prevailing
	// order chosen by its upstream and downstream dependencies.
	//
	// It isn't possible for conflicting replace orders to coexist in the
	// same chain of dependent resource instances because that would mean there
	// is no valid order to perform the steps in, and so we rely on the
	// assumption that most resource instances begin without any constraint
	// and then just follow whatever order is required to satisfy the needs
	// of their neighbors.
	replaceAnyOrder resourceInstanceReplaceOrder = iota

	// replaceCreateThenDestroy represents that a replacement object must be
	// created before destroying the previous object.
	replaceCreateThenDestroy

	// replaceDestroyThenCreate represents that the previous object must be
	// destroyed before creating its replacement, such as if both objects
	// would try to occupy the same unique object name and so cannot coexist
	// at the same time.
	//
	// This is the default resolution if all dependencies in a chain start
	// off as [replaceAnyOrder].
	replaceDestroyThenCreate
)

// ChangeAction returns the [plans.Action] corresponding to the receiver,
// or panics if the receiver is [replaceAnyOrder] because that value represents
// that we haven't yet decided which action to use.
//
// This should typically be used only on values taken from the result of a
// call to [findEffectiveReplaceOrders], where all resource instance objects
// are expected to have a definitive effective replace order.
func (o resourceInstanceReplaceOrder) ChangeAction() plans.Action {
	switch o {
	case replaceCreateThenDestroy:
		return plans.CreateThenDelete
	case replaceDestroyThenCreate:
		return plans.DeleteThenCreate
	default:
		panic(fmt.Errorf("no change action for undecided replace order"))
	}
}
