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

package plans

import (
	"github.com/zclconf/go-cty/cty"

	"github.com/opentofu/opentofu/internal/addrs"
	"github.com/opentofu/opentofu/internal/providers"
	"github.com/opentofu/opentofu/internal/states"
)

// Changes describes various actions that OpenTofu will attempt to take if
// the corresponding plan is applied.
//
// A Changes object can be rendered into a visual diff (by the caller, using
// code in another package) for display to the user.
type Changes struct {
	// Resources tracks planned changes to resource instance objects.
	Resources []*ResourceInstanceChangeSrc

	// Outputs tracks planned changes output values.
	//
	// Note that although an in-memory plan contains planned changes for
	// outputs throughout the configuration, a plan serialized
	// to disk retains only the root outputs because they are
	// externally-visible, while other outputs are implementation details and
	// can be easily re-calculated during the apply phase. Therefore only root
	// module outputs will survive a round-trip through a plan file.
	Outputs []*OutputChangeSrc
}

// NewChanges returns a valid Changes object that describes no changes.
func NewChanges() *Changes {
	return &Changes{}
}

// BuildChanges is a helper -- primarily intended for tests -- to build a state
// using imperative code against the StateSync type while still acting as
// an expression of type *State to assign into a containing struct.
func BuildChanges(cb func(sync *ChangesSync)) *Changes {
	c := NewChanges()
	cb(c.SyncWrapper())
	return c
}

func (c *Changes) Empty() bool {
	for _, res := range c.Resources {
		if res.Action != NoOp || res.Moved() {
			return false
		}

		if res.Importing != nil {
			return false
		}
	}

	for _, out := range c.Outputs {
		if out.Addr.Module.IsRoot() && out.Action != NoOp {
			return false
		}
	}

	return true
}

// ResourceInstance returns the planned change for the current object of the
// resource instance of the given address, if any. Returns nil if no change is
// planned.
func (c *Changes) ResourceInstance(addr addrs.AbsResourceInstance) *ResourceInstanceChangeSrc {
	for _, rc := range c.Resources {
		if rc.Addr.Equal(addr) && rc.DeposedKey == states.NotDeposed {
			return rc
		}
	}

	return nil
}

// InstancesForAbsResource returns the planned change for the current objects
// of the resource instances of the given address, if any. Returns nil if no
// changes are planned.
func (c *Changes) InstancesForAbsResource(addr addrs.AbsResource) []*ResourceInstanceChangeSrc {
	var changes []*ResourceInstanceChangeSrc
	for _, rc := range c.Resources {
		resAddr := rc.Addr.ContainingResource()
		if resAddr.Equal(addr) && rc.DeposedKey == states.NotDeposed {
			changes = append(changes, rc)
		}
	}

	return changes
}

// InstancesForConfigResource returns the planned change for the current objects
// of the resource instances of the given address, if any. Returns nil if no
// changes are planned.
func (c *Changes) InstancesForConfigResource(addr addrs.ConfigResource) []*ResourceInstanceChangeSrc {
	var changes []*ResourceInstanceChangeSrc
	for _, rc := range c.Resources {
		resAddr := rc.Addr.ContainingResource().Config()
		if resAddr.Equal(addr) && rc.DeposedKey == states.NotDeposed {
			changes = append(changes, rc)
		}
	}

	return changes
}

// ResourceInstanceDeposed returns the plan change of a deposed object of
// the resource instance of the given address, if any. Returns nil if no change
// is planned.
func (c *Changes) ResourceInstanceDeposed(addr addrs.AbsResourceInstance, key states.DeposedKey) *ResourceInstanceChangeSrc {
	for _, rc := range c.Resources {
		if rc.Addr.Equal(addr) && rc.DeposedKey == key {
			return rc
		}
	}

	return nil
}

// OutputValue returns the planned change for the output value with the
//
//	given address, if any. Returns nil if no change is planned.
func (c *Changes) OutputValue(addr addrs.AbsOutputValue) *OutputChangeSrc {
	for _, oc := range c.Outputs {
		if oc.Addr.Equal(addr) {
			return oc
		}
	}

	return nil
}

// RootOutputValues returns planned changes for all outputs of the root module.
func (c *Changes) RootOutputValues() []*OutputChangeSrc {
	var res []*OutputChangeSrc

	for _, oc := range c.Outputs {
		// we can't evaluate root module outputs
		if !oc.Addr.Module.Equal(addrs.RootModuleInstance) {
			continue
		}

		res = append(res, oc)

	}

	return res
}

// OutputValues returns planned changes for all outputs for all module
// instances that reside in the parent path.  Returns nil if no changes are
// planned.
func (c *Changes) OutputValues(parent addrs.ModuleInstance, module addrs.ModuleCall) []*OutputChangeSrc {
	var res []*OutputChangeSrc

	for _, oc := range c.Outputs {
		// we can't evaluate root module outputs
		if oc.Addr.Module.Equal(addrs.RootModuleInstance) {
			continue
		}

		changeMod, changeCall := oc.Addr.Module.Call()
		// this does not reside on our parent instance path
		if !changeMod.Equal(parent) {
			continue
		}

		// this is not the module you're looking for
		if changeCall.Name != module.Name {
			continue
		}

		res = append(res, oc)

	}

	return res
}

// SyncWrapper returns a wrapper object around the receiver that can be used
// to make certain changes to the receiver in a concurrency-safe way, as long
// as all callers share the same wrapper object.
func (c *Changes) SyncWrapper() *ChangesSync {
	return &ChangesSync{
		changes: c,
	}
}

// ResourceInstanceChange describes a change to a particular resource instance
// object.
// Note for developers: If you are adding new properties for this, ensure that the
// Simplify() method is updated to copy those properties when it creates new simplified changes.
// otherwise there could be data loss
type ResourceInstanceChange struct {
	// Addr is the absolute address of the resource instance that the change
	// will apply to.
	Addr addrs.AbsResourceInstance

	// PrevRunAddr is the absolute address that this resource instance had at
	// the conclusion of a previous run.
	//
	// This will typically be the same as Addr, but can be different if the
	// previous resource instance was subject to a "moved" block that we
	// handled in the process of creating this plan.
	//
	// For the initial creation of a resource instance there isn't really any
	// meaningful "previous run address", but PrevRunAddr will still be set
	// equal to Addr in that case in order to simplify logic elsewhere which
	// aims to detect and react to the movement of instances between addresses.
	PrevRunAddr addrs.AbsResourceInstance

	// DeposedKey is the identifier for a deposed object associated with the
	// given instance, or states.NotDeposed if this change applies to the
	// current object.
	//
	// A Replace change for a resource with create_before_destroy set will
	// create a new DeposedKey temporarily during replacement. In that case,
	// DeposedKey in the plan is always states.NotDeposed, representing that
	// the current object is being replaced with the deposed.
	DeposedKey states.DeposedKey

	// Provider is the address of the provider configuration that was used
	// to plan this change, and thus the configuration that must also be
	// used to apply it.
	ProviderAddr addrs.AbsProviderConfig

	// Change is an embedded description of the change.
	Change

	// ActionReason is an optional extra indication of why we chose the
	// action recorded in Change.Action for this particular resource instance.
	//
	// This is an approximate mechanism only for the purpose of explaining the
	// plan to end-users in the UI and is not to be used for any
	// decision-making during the apply step; if apply behavior needs to vary
	// depending on the "action reason" then the information for that decision
	// must be recorded more precisely elsewhere for that purpose.
	//
	// Sometimes there might be more than one reason for choosing a particular
	// action. In that case, it's up to the codepath making that decision to
	// decide which value would provide the most relevant explanation to the
	// end-user and return that. It's not a goal of this field to represent
	// fine details about the planning process.
	ActionReason ResourceInstanceChangeActionReason

	// RequiredReplace is a set of paths that caused the change action to be
	// Replace rather than Update. Always nil if the change action is not
	// Replace.
	//
	// This is retained only for UI-plan-rendering purposes and so it does not
	// currently survive a round-trip through a saved plan file.
	RequiredReplace cty.PathSet

	// Private allows a provider to stash any extra data that is opaque to
	// OpenTofu that relates to this change. OpenTofu will save this
	// byte-for-byte and return it to the provider in the apply call.
	Private []byte
}

// Encode produces a variant of the receiver that has its change values
// serialized so it can be written to a plan file. Pass the implied type of the
// corresponding resource type schema for correct operation.
func (rc *ResourceInstanceChange) Encode(schema *providers.Schema) (*ResourceInstanceChangeSrc, error) {
	cs, err := rc.Change.Encode(schema)
	if err != nil {
		return nil, err
	}
	prevRunAddr := rc.PrevRunAddr
	if prevRunAddr.Resource.Resource.Type == "" {
		// Suggests an old caller that hasn't been properly updated to
		// populate this yet.
		prevRunAddr = rc.Addr
	}
	return &ResourceInstanceChangeSrc{
		Addr:            rc.Addr,
		PrevRunAddr:     prevRunAddr,
		DeposedKey:      rc.DeposedKey,
		ProviderAddr:    rc.ProviderAddr,
		ChangeSrc:       *cs,
		ActionReason:    rc.ActionReason,
		RequiredReplace: rc.RequiredReplace,
		Private:         rc.Private,
	}, err
}

func (rc *ResourceInstanceChange) Moved() bool {
	return !rc.Addr.Equal(rc.PrevRunAddr)
}

// Simplify will, where possible, produce a change with a simpler action than
// the receiver given a flag indicating whether the caller is dealing with
// a normal apply or a destroy. This flag deals with the fact that OpenTofu
// Core uses a specialized graph node type for destroying; only that
// specialized node should set "destroying" to true.
//
// The following table shows the simplification behavior:
//
//	Action    Destroying?   New Action
//	--------+-------------+-----------
//	Create    true          NoOp
//	Delete    false         NoOp
//	Replace   true          Delete
//	Replace   false         Create
//
// For any combination not in the above table, the Simplify just returns the
// receiver as-is.
func (rc *ResourceInstanceChange) Simplify(destroying bool) *ResourceInstanceChange {
	if destroying {
		switch rc.Action {
		case Delete:
			// We'll fall out and just return rc verbatim, then.
		case CreateThenDelete, DeleteThenCreate:
			return &ResourceInstanceChange{
				Addr:         rc.Addr,
				DeposedKey:   rc.DeposedKey,
				Private:      rc.Private,
				ProviderAddr: rc.ProviderAddr,
				Change: Change{
					Action:          Delete,
					Before:          rc.Before,
					After:           cty.NullVal(rc.Before.Type()),
					Importing:       rc.Importing,
					GeneratedConfig: rc.GeneratedConfig,
					BeforeIdentity:  rc.BeforeIdentity,
					AfterIdentity:   cty.NullVal(rc.AfterIdentity.Type()),
				},
			}
		default:
			return &ResourceInstanceChange{
				Addr:         rc.Addr,
				DeposedKey:   rc.DeposedKey,
				Private:      rc.Private,
				ProviderAddr: rc.ProviderAddr,
				Change: Change{
					Action:          NoOp,
					Before:          rc.Before,
					After:           rc.Before,
					Importing:       rc.Importing,
					GeneratedConfig: rc.GeneratedConfig,
					BeforeIdentity:  rc.BeforeIdentity,
					AfterIdentity:   rc.AfterIdentity,
				},
			}
		}
	} else {
		switch rc.Action {
		case Delete:
			return &ResourceInstanceChange{
				Addr:         rc.Addr,
				DeposedKey:   rc.DeposedKey,
				Private:      rc.Private,
				ProviderAddr: rc.ProviderAddr,
				Change: Change{
					Action:          NoOp,
					Before:          rc.Before,
					After:           rc.Before,
					Importing:       rc.Importing,
					GeneratedConfig: rc.GeneratedConfig,
					BeforeIdentity:  rc.BeforeIdentity,
					AfterIdentity:   rc.AfterIdentity,
				},
			}
		case CreateThenDelete, DeleteThenCreate, ForgetThenCreate:
			return &ResourceInstanceChange{
				Addr:         rc.Addr,
				DeposedKey:   rc.DeposedKey,
				Private:      rc.Private,
				ProviderAddr: rc.ProviderAddr,
				Change: Change{
					Action:          Create,
					Before:          cty.NullVal(rc.After.Type()),
					After:           rc.After,
					Importing:       rc.Importing,
					GeneratedConfig: rc.GeneratedConfig,
					BeforeIdentity:  cty.NullVal(rc.BeforeIdentity.Type()),
					AfterIdentity:   rc.AfterIdentity,
				},
			}
		}
	}

	// If we fall out here then our change is already simple enough.
	return rc
}

// ResourceInstanceChangeActionReason allows for some extra user-facing
// reasoning for why a particular change action was chosen for a particular
// resource instance.
//
// This only represents sufficient detail to give a suitable explanation to
// an end-user, and mustn't be used for any real decision-making during the
// apply step.
type ResourceInstanceChangeActionReason rune

//go:generate go tool golang.org/x/tools/cmd/stringer -type=ResourceInstanceChangeActionReason changes.go

const (
	// In most cases there's no special reason for choosing a particular
	// action, which is represented by ResourceInstanceChangeNoReason.
	ResourceInstanceChangeNoReason ResourceInstanceChangeActionReason = 0

	// ResourceInstanceReplaceBecauseTainted indicates that the resource
	// instance must be replaced because its existing current object is
	// marked as "tainted".
	ResourceInstanceReplaceBecauseTainted ResourceInstanceChangeActionReason = 'T'

	// ResourceInstanceReplaceByRequest indicates that the resource instance
	// is planned to be replaced because a caller specifically asked for it
	// to be using ReplaceAddrs. (On the command line, the -replace=...
	// planning option.)
	ResourceInstanceReplaceByRequest ResourceInstanceChangeActionReason = 'R'

	// ResourceInstanceReplaceByTriggers indicates that the resource instance
	// is planned to be replaced because of a corresponding change in a
	// replace_triggered_by reference.
	ResourceInstanceReplaceByTriggers ResourceInstanceChangeActionReason = 'D'

	// ResourceInstanceReplaceBecauseCannotUpdate indicates that the resource
	// instance is planned to be replaced because the provider has indicated
	// that a requested change cannot be applied as an update.
	//
	// In this case, the RequiredReplace field will typically be populated on
	// the ResourceInstanceChange object to give information about specifically
	// which arguments changed in a non-updatable way.
	ResourceInstanceReplaceBecauseCannotUpdate ResourceInstanceChangeActionReason = 'F'

	// ResourceInstanceDeleteBecauseNoResourceConfig indicates that the
	// resource instance is planned to be deleted because there's no
	// corresponding resource configuration block in the configuration.
	ResourceInstanceDeleteBecauseNoResourceConfig ResourceInstanceChangeActionReason = 'N'

	// ResourceInstanceDeleteBecauseWrongRepetition indicates that the
	// resource instance is planned to be deleted because the instance key
	// type isn't consistent with the repetition mode selected in the
	// resource configuration.
	ResourceInstanceDeleteBecauseWrongRepetition ResourceInstanceChangeActionReason = 'W'

	// ResourceInstanceDeleteBecauseCountIndex indicates that the resource
	// instance is planned to be deleted because its integer instance key
	// is out of range for the current configured resource "count" value.
	ResourceInstanceDeleteBecauseCountIndex ResourceInstanceChangeActionReason = 'C'

	// ResourceInstanceDeleteBecauseEachKey indicates that the resource
	// instance is planned to be deleted because its string instance key
	// isn't one of the keys included in the current configured resource
	// "for_each" value.
	ResourceInstanceDeleteBecauseEachKey ResourceInstanceChangeActionReason = 'E'

	// ResourceInstanceDeleteBecauseEnabledFalse indicates that the resource
	// instance is planned to be deleted because its enabled argument is false.
	ResourceInstanceDeleteBecauseEnabledFalse ResourceInstanceChangeActionReason = 'L'

	// ResourceInstanceDeleteBecauseNoModule indicates that the resource
	// instance is planned to be deleted because it belongs to a module
	// instance that's no longer declared in the configuration.
	//
	// This is less specific than the reasons we return for the various ways
	// a resource instance itself can be no longer declared, including both
	// the total removal of a module block and changes to its count/for_each
	// arguments. This difference in detail is out of pragmatism, because
	// potentially multiple nested modules could all contribute conflicting
	// specific reasons for a particular instance to no longer be declared.
	ResourceInstanceDeleteBecauseNoModule ResourceInstanceChangeActionReason = 'M'

	// ResourceInstanceDeleteBecauseNoMoveTarget indicates that the resource
	// address appears as the target ("to") in a moved block, but no
	// configuration exists for that resource. According to our move rules,
	// this combination evaluates to a deletion of the "new" resource.
	ResourceInstanceDeleteBecauseNoMoveTarget ResourceInstanceChangeActionReason = 'A'

	// ResourceInstanceReadBecauseConfigUnknown indicates that the resource
	// must be read during apply (rather than during planning) because its
	// configuration contains unknown values. This reason applies only to
	// data resources.
	ResourceInstanceReadBecauseConfigUnknown ResourceInstanceChangeActionReason = '?'

	// ResourceInstanceReadBecauseDependencyPending indicates that the resource
	// must be read during apply (rather than during planning) because it
	// depends on a managed resource instance which has its own changes
	// pending.
	ResourceInstanceReadBecauseDependencyPending ResourceInstanceChangeActionReason = '!'

	// ResourceInstanceReadBecauseCheckNested indicates that the resource must
	// be read during apply (as well as during planning) because it is inside
	// a check block and when the check assertions execute we want them to use
	// the most up-to-date data.
	ResourceInstanceReadBecauseCheckNested ResourceInstanceChangeActionReason = '#'

	// ResourceInstanceForgotBecauseLifecycleDestroyInState indicates that the resource
	// instance is being forgotten because the resource has a lifecycle configuration with "destroy" set to false in the state.
	// This is used to avoid confusion for users not having the "lifecycle.destroy" attribute set in the current configuration (or the resource has been removed entirely).
	// Used to differentiate between the different origins of the `forgot` action.
	// However, we still have the attribute in the state to avoid deleting resources that should have been retained.
	ResourceInstanceForgotBecauseLifecycleDestroyInState ResourceInstanceChangeActionReason = 'X'

	// ResourceInstanceForgotBecauseLifecycleDestroyInConfig indicates that the resource
	// instance is being forgotten because the resource has a lifecycle configuration with "destroy" set to false.
	// Used to differentiate between the different origins of the `forgot` action.
	ResourceInstanceForgotBecauseLifecycleDestroyInConfig ResourceInstanceChangeActionReason = 'Y'
)

// OutputChange describes a change to an output value.
type OutputChange struct {
	// Addr is the absolute address of the output value that the change
	// will apply to.
	Addr addrs.AbsOutputValue

	// Change is an embedded description of the change.
	//
	// For output value changes, the type constraint for the DynamicValue
	// instances is always cty.DynamicPseudoType.
	Change

	// Sensitive, if true, indicates that either the old or new value in the
	// change is sensitive and so a rendered version of the plan in the UI
	// should elide the actual values while still indicating the action of the
	// change.
	Sensitive bool
}

// Encode produces a variant of the receiver that has its change values
// serialized so it can be written to a plan file.
func (oc *OutputChange) Encode() (*OutputChangeSrc, error) {
	cs, err := oc.Change.Encode(nil) // we don't have schemas here, so just pass through nil
	if err != nil {
		return nil, err
	}
	return &OutputChangeSrc{
		Addr:      oc.Addr,
		ChangeSrc: *cs,
		Sensitive: oc.Sensitive,
	}, err
}

// Importing is the part of a ChangeSrc that describes the embedded import
// action.
//
// The fields in here are subject to change, so downstream consumers should be
// prepared for backwards compatibility in case the contents changes.
type Importing struct {
	// ID is the original ID of the imported resource.
	ID string

	// Identity is an alterate identity value for the imported resource.
	// Mutually exclusive with ID.
	Identity cty.Value
}

// Change describes a single change with a given action.
type Change struct {
	// Action defines what kind of change is being made.
	Action Action

	// Interpretation of Before and After depend on Action:
	//
	//     NoOp     Before and After are the same, unchanged value
	//     Create   Before is nil, and After is the expected value after create.
	//     Read     Before is any prior value (nil if no prior), and After is the
	//              value that was or will be read.
	//     Update   Before is the value prior to update, and After is the expected
	//              value after update.
	//     Replace  As with Update.
	//     Delete   Before is the value prior to delete, and After is always nil.
	//
	// Unknown values may appear anywhere within the Before and After values,
	// either as the values themselves or as nested elements within known
	// collections/structures.
	Before, After cty.Value

	// Importing is present if the resource is being imported as part of this
	// change.
	//
	// Use the simple presence of this field to detect if a ChangeSrc is to be
	// imported, the contents of this structure may be modified going forward.
	Importing *Importing

	// GeneratedConfig contains any HCL config generated for this resource
	// during planning, as a string. If GeneratedConfig is populated, Importing
	// should be true. However, not all Importing changes contain generated
	// config.
	GeneratedConfig string

	// BeforeIdentity is the identity value from the known state of the resource instance
	// before the plan is executed.
	// Only relevant for managed resources, not outputs.
	BeforeIdentity cty.Value

	// AfterIdentity is the identity value returned by the provider during
	// planning. This is used to pass identity data through to the apply phase.
	// Only relevant for managed resources, not outputs.
	AfterIdentity cty.Value
}

// Encode produces a variant of the receiver that has its change values
// serialized so it can be written to a plan file. Pass the type constraint
// that the values are expected to conform to; to properly decode the values
// later an identical type constraint must be provided at that time.
//
// Where a Change is embedded in some other struct, it's generally better
// to call the corresponding Encode method of that struct rather than working
// directly with its embedded Change.
func (c *Change) Encode(schema *providers.Schema) (*ChangeSrc, error) {
	var ty cty.Type

	if schema == nil {
		// we've been passed a nil set of schema, we should use a dynamic type here
		ty = cty.DynamicPseudoType
	} else {
		ty = schema.Block.ImpliedType()
	}

	// Storing unmarked values so that we can encode unmarked values
	// and save the PathValueMarks for re-marking the values later
	var beforeVM, afterVM []cty.PathValueMarks
	unmarkedBefore := c.Before
	unmarkedAfter := c.After

	if c.Before.ContainsMarked() {
		unmarkedBefore, beforeVM = c.Before.UnmarkDeepWithPaths()
	}
	beforeDV, err := NewDynamicValue(unmarkedBefore, ty)
	if err != nil {
		return nil, err
	}

	if c.After.ContainsMarked() {
		unmarkedAfter, afterVM = c.After.UnmarkDeepWithPaths()
	}
	afterDV, err := NewDynamicValue(unmarkedAfter, ty)
	if err != nil {
		return nil, err
	}

	var importing *ImportingSrc
	if c.Importing != nil {
		var idVal DynamicValue

		// Only encode identity if it's present and schema has IdentitySchema
		if c.Importing.Identity != cty.NilVal && !c.Importing.Identity.IsNull() && schema != nil && schema.IdentitySchema != nil {
			identityType := schema.IdentitySchema.ImpliedType()
			id, dvErr := NewDynamicValue(c.Importing.Identity, identityType)
			if dvErr != nil {
				return nil, dvErr
			}
			idVal = id
		}

		importing = &ImportingSrc{
			ID:       c.Importing.ID,
			Identity: idVal,
		}
	}

	var beforeIdentityDV DynamicValue
	if c.BeforeIdentity != cty.NilVal && !c.BeforeIdentity.IsNull() {
		identityTy := c.BeforeIdentity.Type()
		if schema != nil && schema.IdentitySchema != nil {
			identityTy = schema.IdentitySchema.ImpliedType()
		}
		beforeIdentityDV, err = NewDynamicValue(c.BeforeIdentity, identityTy)
		if err != nil {
			return nil, err
		}
	}

	var afterIdentityDV DynamicValue
	if c.AfterIdentity != cty.NilVal && !c.AfterIdentity.IsNull() {
		identityTy := c.AfterIdentity.Type()
		if schema != nil && schema.IdentitySchema != nil {
			identityTy = schema.IdentitySchema.ImpliedType()
		}
		afterIdentityDV, err = NewDynamicValue(c.AfterIdentity, identityTy)
		if err != nil {
			return nil, err
		}
	}

	return &ChangeSrc{
		Action:          c.Action,
		Before:          beforeDV,
		After:           afterDV,
		BeforeValMarks:  beforeVM,
		AfterValMarks:   afterVM,
		Importing:       importing,
		GeneratedConfig: c.GeneratedConfig,
		BeforeIdentity:  beforeIdentityDV,
		AfterIdentity:   afterIdentityDV,
	}, nil
}
