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

	"github.com/opentofu/opentofu/internal/addrs"
	"github.com/opentofu/opentofu/internal/configs"
	"github.com/opentofu/opentofu/internal/states"
	"github.com/opentofu/opentofu/internal/tfdiags"
)

type MoveStatement struct {
	From, To  *addrs.MoveEndpointInModule
	DeclRange tfdiags.SourceRange

	// Implied is true for statements produced by ImpliedMoveStatements, and
	// false for statements produced by FindMoveStatements.
	//
	// An "implied" statement is one that has no explicit "moved" block in
	// the configuration and was instead generated automatically based on a
	// comparison between current configuration and previous run state.
	// For implied statements, the DeclRange field contains the source location
	// of something in the source code that implied the statement, in which
	// case it would probably be confusing to show that source range to the
	// user, e.g. in an error message, without clearly mentioning that it's
	// related to an implied move statement.
	Implied bool
}

// FindMoveStatements recurses through the modules of the given configuration
// and returns a flat set of all "moved" blocks defined within, in a
// deterministic but undefined order.
func FindMoveStatements(rootCfg *configs.Config) []MoveStatement {
	return findMoveStatements(rootCfg, nil)
}

func findMoveStatements(cfg *configs.Config, into []MoveStatement) []MoveStatement {
	modAddr := cfg.Path
	for _, mc := range cfg.Module.Moved {
		fromAddr, toAddr := addrs.UnifyMoveEndpoints(modAddr, mc.From, mc.To)
		if fromAddr == nil || toAddr == nil {
			// Invalid combination should've been caught during original
			// configuration decoding, in the configs package.
			panic(fmt.Sprintf("incompatible move endpoints in %s", mc.DeclRange))
		}

		into = append(into, MoveStatement{
			From:      fromAddr,
			To:        toAddr,
			DeclRange: tfdiags.SourceRangeFromHCL(mc.DeclRange),
			Implied:   false,
		})
	}

	for _, childCfg := range cfg.Children {
		into = findMoveStatements(childCfg, into)
	}

	return into
}

// ImpliedMoveStatements compares addresses in the given state with addresses
// in the given configuration and potentially returns additional MoveStatement
// objects representing moves we infer automatically, even though they aren't
// explicitly recorded in the configuration.
//
// We do this primarily for backward compatibility with behaviors of Terraform
// versions prior to introducing explicit "moved" blocks. Specifically, this
// function aims to achieve the same result as the "NodeCountBoundary"
// heuristic from Terraform v1.0 and earlier, where adding or removing the
// "count" meta-argument from an already-created resource can automatically
// preserve the zeroth or the NoKey instance, depending on the direction of
// the change. We do this only for resources that aren't mentioned already
// in at least one explicit move statement.
//
// As with the previous-version heuristics it replaces, this is a best effort
// and doesn't handle all situations. An explicit move statement is always
// preferred, but our goal here is to match exactly the same cases that the
// old heuristic would've matched, to retain compatibility for existing modules.
//
// We should think very hard before adding any _new_ implication rules for
// moved statements.
func ImpliedMoveStatements(cfg *configs.Config, prevRunState *states.State, explicitStmts []MoveStatement) []MoveStatement {
	modAddr := cfg.Path
	into := make([]MoveStatement, 0)

	// Create implied move statements for module calls. We're typically
	// looking for module where meta-arguments were changed to see if they
	// can be moved without an explicit moved block. If there's an existing
	// explicit move statement for the module, we don't create an implied move statement.
	for modCallName, modCallCfg := range cfg.Module.ModuleCalls {
		into = append(into, impliedMoveStatementsForModules(prevRunState, cfg, modCallName, modCallCfg, explicitStmts)...)
	}

	// There can be potentially many instances of the module, so we need
	// to consider each of them separately.
	for _, modState := range prevRunState.ModuleInstances(modAddr) {
		// What we're looking for here is either a no-key resource instance
		// where the configuration has count set or a zero-key resource
		// instance where the configuration _doesn't_ have count set.
		// If so, we'll generate a statement replacing no-key with zero-key or
		// vice-versa.
		into = append(into, impliedMoveStatementsForModuleResources(cfg, modState, explicitStmts)...)
	}

	for _, childCfg := range cfg.Children {
		into = append(into, ImpliedMoveStatements(childCfg, prevRunState, explicitStmts)...)
	}

	return into
}

// impliedMoveStatementsForModules creates implied move statements for module calls.
func impliedMoveStatementsForModules(
	prevRunState *states.State,
	parentCfg *configs.Config,
	modCallName string,
	modCallCfg *configs.ModuleCall,
	explicitStmts []MoveStatement,
) []MoveStatement {
	var into []MoveStatement
	var toKey addrs.InstanceKey
	approxSrcRange := tfdiags.SourceRangeFromHCL(modCallCfg.DeclRange)

	// Use the configuration to determine the instance key to
	// use for the implied move `To statement.
	switch {
	case modCallCfg.Count != nil:
		// If we have a count expression then we'll use _that_ as
		// a slightly-more-precise approximate source range.
		approxSrcRange = tfdiags.SourceRangeFromHCL(modCallCfg.Count.Range())
		toKey = addrs.IntKey(0)
	case modCallCfg.Count == nil && modCallCfg.ForEach == nil: // no repetition at all
		toKey = addrs.NoKey
	default:
		// Other combinations of meta-arguments are not supported.
		return into
	}

	// Get the module address
	modAddr := parentCfg.Path.Child(modCallName)

	// Iterate over all module instances, that can be generated by
	// meta-arguments like count or for_each on the saved state for the module.
	for _, modState := range prevRunState.ModuleInstances(modAddr) {
		callerAddr, callAddr := modState.Addr.CallInstance()
		absCallAddr := addrs.AbsModuleCall{
			Module: callerAddr,
			Call:   callAddr.Call,
		}

		// Only one instance of the module can be dealt, because moving from single
		// to repeated can't support multiple instances. The other instances are going
		// to be removed or created, depending on the direction of the change.
		fromKey := callAddr.Key
		if fromKey != addrs.NoKey && fromKey != addrs.IntKey(0) {
			continue
		}

		// We mustn't generate an implied statement if the user already
		// wrote an explicit statement referring to this module,
		// because they may wish to select an instance key other than
		// zero as the one to retain. If the instance key from the state
		// equals the instance key from the configuration, then we don't
		// need to generate an implied statement.
		if fromKey == toKey || haveMoveStatementForModule(modState.Addr, explicitStmts) {
			continue
		}

		fromAddr := absCallAddr.Instance(fromKey)
		toAddr := absCallAddr.Instance(toKey)

		into = append(into, MoveStatement{
			From:      addrs.ImpliedMoveStatementEndpoint(fromAddr, approxSrcRange),
			To:        addrs.ImpliedMoveStatementEndpoint(toAddr, approxSrcRange),
			DeclRange: approxSrcRange,
			Implied:   true,
		})
	}
	return into
}

// impliedMoveStatementsForModuleResources creates implied move statements for module resources.
func impliedMoveStatementsForModuleResources(cfg *configs.Config, modState *states.Module, explicitStmts []MoveStatement) []MoveStatement {
	var into []MoveStatement

	for _, rState := range modState.Resources {
		rAddr := rState.Addr
		rCfg := cfg.Module.ResourceByAddr(rAddr.Resource)
		if rCfg == nil {
			// If there's no configuration at all then there can't be any
			// automatic move fixup to do.
			continue
		}
		approxSrcRange := tfdiags.SourceRangeFromHCL(rCfg.DeclRange)

		// NOTE: We're intentionally not checking to see whether the
		// "to" addresses in our implied statements already have
		// instances recorded in state, because ApplyMoves should
		// deal with such conflicts in a deterministic way for both
		// explicit and implicit moves, and we'd rather have that
		// handled all in one place.

		var fromKey, toKey addrs.InstanceKey

		switch {
		case rCfg.Count != nil:
			// If we have a count expression then we'll use _that_ as
			// a slightly-more-precise approximate source range.
			approxSrcRange = tfdiags.SourceRangeFromHCL(rCfg.Count.Range())

			if riState := rState.Instances[addrs.NoKey]; riState != nil {
				fromKey = addrs.NoKey
				toKey = addrs.IntKey(0)
			}
		case rCfg.Count == nil && rCfg.ForEach == nil: // no repetition at all
			if riState := rState.Instances[addrs.IntKey(0)]; riState != nil {
				fromKey = addrs.IntKey(0)
				toKey = addrs.NoKey
			}
		}

		if fromKey != toKey {
			// We mustn't generate an implied statement if the user already
			// wrote an explicit statement referring to this resource,
			// because they may wish to select an instance key other than
			// zero as the one to retain.
			if !haveMoveStatementForResource(rAddr, explicitStmts) {
				into = append(into, MoveStatement{
					From:      addrs.ImpliedMoveStatementEndpoint(rAddr.Instance(fromKey), approxSrcRange),
					To:        addrs.ImpliedMoveStatementEndpoint(rAddr.Instance(toKey), approxSrcRange),
					DeclRange: approxSrcRange,
					Implied:   true,
				})
			}
		}
	}
	return into
}

func (s *MoveStatement) ObjectKind() addrs.MoveEndpointKind {
	// addrs.UnifyMoveEndpoints guarantees that both of our addresses have
	// the same kind, so we can just arbitrary use From and assume To will
	// match it.
	return s.From.ObjectKind()
}

// Name is used internally for displaying the statement graph
func (s *MoveStatement) Name() string {
	return fmt.Sprintf("%s->%s", s.From, s.To)
}

func haveMoveStatementForResource(addr addrs.AbsResource, stmts []MoveStatement) bool {
	// This is not a particularly optimal way to answer this question,
	// particularly since our caller calls this function in a loop already,
	// but we expect the total number of explicit statements to be small
	// in any reasonable OpenTofu configuration and so a more complicated
	// approach wouldn't be justified here.

	for _, stmt := range stmts {
		if stmt.From.SelectsResource(addr) {
			return true
		}
		if stmt.To.SelectsResource(addr) {
			return true
		}
	}
	return false
}

func haveMoveStatementForModule(addr addrs.ModuleInstance, stmts []MoveStatement) bool {
	// This is not a particularly optimal way to answer this question,
	// particularly since our caller calls this function in a loop already,
	// but we expect the total number of explicit statements to be small
	// in any reasonable OpenTofu configuration and so a more complicated
	// approach wouldn't be justified here.

	for _, stmt := range stmts {
		if stmt.From.SelectsModule(addr) {
			return true
		}
		if stmt.To.SelectsModule(addr) {
			return true
		}
	}
	return false
}
