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

package refactoring

import (
	"fmt"
	"log"

	"github.com/opentofu/opentofu/internal/addrs"
	"github.com/opentofu/opentofu/internal/dag"
	"github.com/opentofu/opentofu/internal/logging"
	"github.com/opentofu/opentofu/internal/states"
)

// ApplyMoves modifies in-place the given state object so that any existing
// objects that are matched by a "from" argument of one of the move statements
// will be moved to instead appear at the "to" argument of that statement.
//
// The result is a map from the unique key of each absolute address that was
// either the source or destination of a move to a MoveResult describing
// what happened at that address.
//
// ApplyMoves does not have any error situations itself, and will instead just
// ignore any unresolvable move statements. Validation of a set of moves is
// a separate concern applied to the configuration, because validity of
// moves is always dependent only on the configuration, not on the state.
//
// ApplyMoves expects exclusive access to the given state while it's running.
// Don't read or write any part of the state structure until ApplyMoves returns.
func ApplyMoves(stmts []MoveStatement, state *states.State) MoveResults {
	ret := makeMoveResults()

	if len(stmts) == 0 {
		return ret
	}

	// The methodology here is to construct a small graph of all of the move
	// statements where the edges represent where a particular statement
	// is either chained from or nested inside the effect of another statement.
	// That then means we can traverse the graph in topological sort order
	// to gradually move objects through potentially multiple moves each.

	g := buildMoveStatementGraph(stmts)

	// If the graph is not valid the we will not take any action at all. The
	// separate validation step should detect this and return an error.
	if diags := validateMoveStatementGraph(g); diags.HasErrors() {
		log.Printf("[ERROR] ApplyMoves: %s", diags.ErrWithWarnings())
		return ret
	}

	// The graph must be reduced in order for ReverseDepthFirstWalk to work
	// correctly, since it is built from following edges and can skip over
	// dependencies if there is a direct edge to a transitive dependency.
	g.TransitiveReduction()

	// The starting nodes are the ones that don't depend on any other nodes.
	startNodes := make(dag.Set, len(stmts))
	for _, v := range g.Vertices() {
		if len(g.DownEdges(v)) == 0 {
			startNodes.Add(v)
		}
	}

	if startNodes.Len() == 0 {
		log.Println("[TRACE] refactoring.ApplyMoves: No 'moved' statements to consider in this configuration")
		return ret
	}

	log.Printf("[TRACE] refactoring.ApplyMoves: Processing 'moved' statements in the configuration\n%s", logging.Indent(g.String()))

	recordOldAddr := func(oldAddr, newAddr addrs.AbsResourceInstance) {
		if prevMove, exists := ret.Changes.GetOk(oldAddr); exists {
			// If the old address was _already_ the result of a move then
			// we'll replace that entry so that our results summarize a chain
			// of moves into a single entry.
			ret.Changes.Remove(oldAddr)
			oldAddr = prevMove.From
		}
		ret.Changes.Put(newAddr, MoveSuccess{
			From: oldAddr,
			To:   newAddr,
		})
	}
	recordBlockage := func(newAddr, wantedAddr addrs.AbsMoveable) {
		ret.Blocked.Put(newAddr, MoveBlocked{
			Wanted: wantedAddr,
			Actual: newAddr,
		})
	}

	for _, v := range g.ReverseTopologicalOrder() {
		stmt := v.(*MoveStatement)

		for _, ms := range state.Modules {
			modAddr := ms.Addr

			// We don't yet know that the current module is relevant, and
			// we determine that differently for each the object kind.
			switch kind := stmt.ObjectKind(); kind {
			case addrs.MoveEndpointModule:
				// For a module endpoint we just try the module address
				// directly, and execute the moves if it matches.
				if newAddr, matches := modAddr.MoveDestination(stmt.From, stmt.To); matches {
					log.Printf("[TRACE] refactoring.ApplyMoves: %s has moved to %s", modAddr, newAddr)

					// If we already have a module at the new address then
					// we'll skip this move and let the existing object take
					// priority.
					if ms := state.Module(newAddr); ms != nil {
						log.Printf("[WARN] Skipped moving %s to %s, because there's already another module instance at the destination", modAddr, newAddr)
						recordBlockage(modAddr, newAddr)
						continue
					}

					// We need to visit all of the resource instances in the
					// module and record them individually as results.
					for _, rs := range ms.Resources {
						relAddr := rs.Addr.Resource
						for key := range rs.Instances {
							oldInst := relAddr.Instance(key).Absolute(modAddr)
							newInst := relAddr.Instance(key).Absolute(newAddr)
							recordOldAddr(oldInst, newInst)
						}
					}

					state.MoveModuleInstance(modAddr, newAddr)
					continue
				}
			case addrs.MoveEndpointResource:
				// For a resource endpoint we require an exact containing
				// module match, because by definition a matching resource
				// cannot be nested any deeper than that.
				if !stmt.From.SelectsModule(modAddr) {
					continue
				}

				// We then need to search each of the resources and resource
				// instances in the module.
				for _, rs := range ms.Resources {
					rAddr := rs.Addr
					if newAddr, matches := rAddr.MoveDestination(stmt.From, stmt.To); matches {
						log.Printf("[TRACE] refactoring.ApplyMoves: resource %s has moved to %s", rAddr, newAddr)

						// If we already have a resource at the new address then
						// we'll skip this move and let the existing object take
						// priority.
						if rs := state.Resource(newAddr); rs != nil {
							log.Printf("[WARN] Skipped moving %s to %s, because there's already another resource at the destination", rAddr, newAddr)
							recordBlockage(rAddr, newAddr)
							continue
						}

						for key := range rs.Instances {
							oldInst := rAddr.Instance(key)
							newInst := newAddr.Instance(key)
							recordOldAddr(oldInst, newInst)
						}
						state.MoveAbsResource(rAddr, newAddr)
						continue
					}
					for key := range rs.Instances {
						iAddr := rAddr.Instance(key)
						if newAddr, matches := iAddr.MoveDestination(stmt.From, stmt.To); matches {
							log.Printf("[TRACE] refactoring.ApplyMoves: resource instance %s has moved to %s", iAddr, newAddr)

							// If we already have a resource instance at the new
							// address then we'll skip this move and let the existing
							// object take priority.
							if is := state.ResourceInstance(newAddr); is != nil {
								log.Printf("[WARN] Skipped moving %s to %s, because there's already another resource instance at the destination", iAddr, newAddr)
								recordBlockage(iAddr, newAddr)
								continue
							}

							recordOldAddr(iAddr, newAddr)

							state.MoveAbsResourceInstance(iAddr, newAddr)
							continue
						}
					}
				}
			default:
				panic(fmt.Sprintf("unhandled move object kind %s", kind))
			}
		}
	}

	return ret
}

// buildMoveStatementGraph constructs a dependency graph of the given move
// statements, where the nodes are all pointers to statements in the given
// slice and the edges represent either chaining or nesting relationships.
//
// buildMoveStatementGraph doesn't do any validation of the graph, so it
// may contain cycles and other sorts of invalidity.
func buildMoveStatementGraph(stmts []MoveStatement) *dag.AcyclicGraph {
	g := &dag.AcyclicGraph{}
	for i := range stmts {
		// The graph nodes are pointers to the actual statements directly.
		g.Add(&stmts[i])
	}

	// Now we'll add the edges representing chaining and nesting relationships.
	// We assume that a reasonable configuration will have at most tens of
	// move statements and thus this N*M algorithm is acceptable.
	for dependerI := range stmts {
		depender := &stmts[dependerI]
		for dependeeI := range stmts {
			if dependerI == dependeeI {
				// skip comparing the statement to itself
				continue
			}
			dependee := &stmts[dependeeI]

			if statementDependsOn(depender, dependee) {
				g.Connect(dag.BasicEdge(depender, dependee))
			}
		}
	}

	return g
}

// statementDependsOn returns true if statement a depends on statement b;
// i.e. statement b must be executed before statement a.
func statementDependsOn(a, b *MoveStatement) bool {
	// chain-able moves are simple, as on the destination of one move could be
	// equal to the source of another.
	if a.From.CanChainFrom(b.To) {
		return true
	}

	// Statement nesting in more complex, as we have 8 possible combinations to
	// assess. Here we list all combinations, along with the statement which
	// must be executed first when one address is nested within another.
	// A.From  IsNestedWithin  B.From => A
	// A.From  IsNestedWithin  B.To   => B
	// A.To    IsNestedWithin  B.From => A
	// A.To    IsNestedWithin  B.To   => B
	// B.From  IsNestedWithin  A.From => B
	// B.From  IsNestedWithin  A.To   => A
	// B.To    IsNestedWithin  A.From => B
	// B.To    IsNestedWithin  A.To   => A
	//
	// Since we are only interested in checking if A depends on B, we only need
	// to check the 4 possibilities above which result in B being executed
	// first. If we're there's no dependency at all we can return immediately.
	if !a.From.NestedWithin(b.To) && !a.To.NestedWithin(b.To) &&
		!b.From.NestedWithin(a.From) && !b.To.NestedWithin(a.From) {
		return false
	}

	// If a nested move has a dependency, we need to rule out the possibility
	// that this is a move inside a module only changing indexes. If an
	// ancestor module is only changing the index of a nested module, any
	// nested move statements are going to match both the From and To address
	// when the base name is not changing, causing a cycle in the order of
	// operations.

	// if A is not declared in an ancestor module, then we can't be nested
	// within a module index change.
	if len(a.To.Module()) >= len(b.To.Module()) {
		return true
	}
	// We only want the nested move statement to depend on the outer module
	// move, so we only test this in the reverse direction.
	if a.From.IsModuleReIndex(a.To) {
		return false
	}

	return true
}

// MoveResults describes the outcome of an ApplyMoves call.
type MoveResults struct {
	// Changes is a map from the unique keys of the final new resource
	// instance addresses to an object describing what changed.
	//
	// This includes one entry for each resource instance address that was
	// the destination of a move statement. It doesn't include resource
	// instances that were not affected by moves at all, but it does include
	// resource instance addresses that were "blocked" (also recorded in
	// BlockedAddrs) if and only if they were able to move at least
	// partially along a chain before being blocked.
	//
	// In the return value from ApplyMoves, all of the keys are guaranteed to
	// be unique keys derived from addrs.AbsResourceInstance values.
	Changes addrs.Map[addrs.AbsResourceInstance, MoveSuccess]

	// Blocked is a map from the unique keys of the final new
	// resource instances addresses to information about where they "wanted"
	// to move, but were blocked by a pre-existing object at the same address.
	//
	// "Blocking" can arise in unusual situations where multiple points along
	// a move chain were already bound to objects, and thus only one of them
	// can actually adopt the final position in the chain. It can also
	// occur in other similar situations, such as if a configuration contains
	// a move of an entire module and a move of an individual resource into
	// that module, such that the individual resource would collide with a
	// resource in the whole module that was moved.
	//
	// In the return value from ApplyMoves, all of the keys are guaranteed to
	// be unique keys derived from values of addrs.AbsMoveable types.
	Blocked addrs.Map[addrs.AbsMoveable, MoveBlocked]
}

func makeMoveResults() MoveResults {
	return MoveResults{
		Changes: addrs.MakeMap[addrs.AbsResourceInstance, MoveSuccess](),
		Blocked: addrs.MakeMap[addrs.AbsMoveable, MoveBlocked](),
	}
}

type MoveSuccess struct {
	From addrs.AbsResourceInstance
	To   addrs.AbsResourceInstance
}

type MoveBlocked struct {
	Wanted addrs.AbsMoveable
	Actual addrs.AbsMoveable
}

// AddrMoved returns true if and only if the given resource instance moved to
// a new address in the ApplyMoves call that the receiver is describing.
//
// If AddrMoved returns true, you can pass the same address to method OldAddr
// to find its original address prior to moving.
func (rs MoveResults) AddrMoved(newAddr addrs.AbsResourceInstance) bool {
	return rs.Changes.Has(newAddr)
}

// OldAddr returns the old address of the given resource instance address, or
// just returns back the same address if the given instance wasn't affected by
// any move statements.
func (rs MoveResults) OldAddr(newAddr addrs.AbsResourceInstance) addrs.AbsResourceInstance {
	change, ok := rs.Changes.GetOk(newAddr)
	if !ok {
		return newAddr
	}
	return change.From
}
