// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1

package stackplan

import (
	"fmt"
	"time"

	"github.com/hashicorp/go-version"
	"github.com/zclconf/go-cty/cty"
	"github.com/zclconf/go-cty/cty/msgpack"
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/types/known/anypb"

	"github.com/hashicorp/terraform/internal/addrs"
	"github.com/hashicorp/terraform/internal/collections"
	"github.com/hashicorp/terraform/internal/lang"
	"github.com/hashicorp/terraform/internal/lang/marks"
	"github.com/hashicorp/terraform/internal/plans"
	"github.com/hashicorp/terraform/internal/plans/planfile"
	"github.com/hashicorp/terraform/internal/plans/planproto"
	"github.com/hashicorp/terraform/internal/providers"
	"github.com/hashicorp/terraform/internal/rpcapi/terraform1/stacks"
	"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
	"github.com/hashicorp/terraform/internal/stacks/stackutils"
	"github.com/hashicorp/terraform/internal/stacks/tfstackdata1"
	"github.com/hashicorp/terraform/internal/states"
)

// PlannedChange represents a single isolated planned changed, emitted as
// part of a stream of planned changes during the PlanStackChanges RPC API
// operation.
//
// Each PlannedChange becomes a single event in the RPC API, which itself
// has zero or more opaque raw plan messages that the caller must collect and
// provide verbatim during planning and zero or one "description" messages
// that are to give the caller realtime updates about the planning process.
//
// The aggregated sequence of "raw" messages can be provided later to
// [LoadFromProto] to obtain a [Plan] object containing the information
// Terraform Core would need to apply the plan.
type PlannedChange interface {
	// PlannedChangeProto returns the protocol buffers representation of
	// the change, ready to be sent verbatim to an RPC API client.
	PlannedChangeProto() (*stacks.PlannedChange, error)
}

// PlannedChangeRootInputValue announces the existence of a root stack input
// variable and captures its plan-time value so we can make sure to use
// the same value during the apply phase.
type PlannedChangeRootInputValue struct {
	Addr stackaddrs.InputVariable

	// Action is the change being applied to this input variable.
	Action plans.Action

	// Before and After provide the values for before and after this plan.
	// Both could be cty.NilValue if the before or after was ephemeral at the
	// time it was set. Before will be cty.NullVal if Action is plans.Create.
	Before cty.Value
	After  cty.Value

	// RequiredOnApply is true if a non-null value for this variable
	// must be supplied during the apply phase.
	//
	// If this field is false then the variable must either be left unset
	// or must be set to the same value during the apply phase, both of
	// which are equivalent.
	//
	// This is set for an input variable that was declared as ephemeral
	// and was set to a non-null value during the planning phase. The
	// "null-ness" of an ephemeral value is not allowed to change between
	// plan and apply, but a value set during planning can have a different
	// value during apply.
	RequiredOnApply bool

	// DeleteOnApply is true if this variable should be removed from the state
	// on apply even if it was not actively removed from the configuration in
	// a delete action. This is typically the case during a destroy only plan
	// in which we want to update the state to remove everything.
	DeleteOnApply bool
}

var _ PlannedChange = (*PlannedChangeRootInputValue)(nil)

// PlannedChangeProto implements PlannedChange.
func (pc *PlannedChangeRootInputValue) PlannedChangeProto() (*stacks.PlannedChange, error) {
	protoChangeTypes, err := stacks.ChangeTypesForPlanAction(pc.Action)
	if err != nil {
		return nil, err
	}

	var raws []*anypb.Any
	if pc.Action == plans.Delete || pc.DeleteOnApply {
		var raw anypb.Any
		if err := anypb.MarshalFrom(&raw, &tfstackdata1.DeletedRootInputVariable{
			Name: pc.Addr.Name,
		}, proto.MarshalOptions{}); err != nil {
			return nil, fmt.Errorf("failed to encode raw state for %s: %w", pc.Addr, err)
		}
		raws = append(raws, &raw)
	}

	before, err := stacks.ToDynamicValue(pc.Before, cty.DynamicPseudoType)
	if err != nil {
		return nil, fmt.Errorf("failed to encode before planned input variable %s: %w", pc.Addr, err)
	}
	after, err := stacks.ToDynamicValue(pc.After, cty.DynamicPseudoType)
	if err != nil {
		return nil, fmt.Errorf("failed to encode after planned input variable %s: %w", pc.Addr, err)
	}

	if pc.Action != plans.Delete {
		var raw anypb.Any
		if err := anypb.MarshalFrom(&raw, &tfstackdata1.PlanRootInputValue{
			Name:            pc.Addr.Name,
			Value:           tfstackdata1.Terraform1ToStackDataDynamicValue(after),
			RequiredOnApply: pc.RequiredOnApply,
		}, proto.MarshalOptions{}); err != nil {
			return nil, err
		}
		raws = append(raws, &raw)
	}

	return &stacks.PlannedChange{
		Raw: raws,
		Descriptions: []*stacks.PlannedChange_ChangeDescription{
			{
				Description: &stacks.PlannedChange_ChangeDescription_InputVariablePlanned{
					InputVariablePlanned: &stacks.PlannedChange_InputVariable{
						Name:    pc.Addr.Name,
						Actions: protoChangeTypes,
						Values: &stacks.DynamicValueChange{
							Old: before,
							New: after,
						},
						RequiredDuringApply: pc.RequiredOnApply,
					},
				},
			},
		},
	}, nil
}

// PlannedChangeComponentInstanceRemoved is just a reminder for the apply
// operation to delete this component from the state because it's not in
// the configuration and is empty.
type PlannedChangeComponentInstanceRemoved struct {
	Addr stackaddrs.AbsComponentInstance
}

var _ PlannedChange = (*PlannedChangeComponentInstanceRemoved)(nil)

func (pc *PlannedChangeComponentInstanceRemoved) PlannedChangeProto() (*stacks.PlannedChange, error) {
	var raw anypb.Any
	if err := anypb.MarshalFrom(&raw, &tfstackdata1.DeletedComponent{
		ComponentInstanceAddr: pc.Addr.String(),
	}, proto.MarshalOptions{}); err != nil {
		return nil, err
	}

	return &stacks.PlannedChange{
		Raw: []*anypb.Any{&raw},
	}, nil
}

// PlannedChangeComponentInstance announces the existence of a component
// instance and describes (using a plan action) whether it is being added
// or removed.
type PlannedChangeComponentInstance struct {
	Addr stackaddrs.AbsComponentInstance

	// PlanApplyable is true if the modules runtime ruled that this particular
	// component's plan is applyable.
	//
	// See the documentation for [plans.Plan.Applyable] for details on what
	// exactly this represents.
	PlanApplyable bool

	// PlanApplyable is true if the modules runtime ruled that this particular
	// component's plan is complete.
	//
	// See the documentation for [plans.Plan.Complete] for details on what
	// exactly this represents.
	PlanComplete bool

	// Action describes any difference in the existence of this component
	// instance compared to the prior state.
	//
	// Currently it can only be "Create", "Delete", or "NoOp". This action
	// relates to the existence of the component instance itself and does
	// not consider the resource instances inside, whose change actions
	// are tracked in their own [PlannedChange] objects.
	Action plans.Action

	// Mode describes the mode that the component instance is being planned
	// in.
	Mode plans.Mode

	// RequiredComponents is a set of the addresses of all of the components
	// that provide infrastructure that this one's infrastructure will
	// depend on. Any component named here must exist for the entire lifespan
	// of this component instance.
	RequiredComponents collections.Set[stackaddrs.AbsComponent]

	// PlannedInputValues records our best approximation of the component's
	// topmost input values during the planning phase. This could contain
	// unknown values if one component is configured from results of another.
	// This therefore won't be used directly as the input values during apply,
	// but the final set of input values during apply should be consistent
	// with what's captured here.
	PlannedInputValues map[string]plans.DynamicValue

	PlannedInputValueMarks map[string][]cty.PathValueMarks

	PlannedOutputValues map[string]cty.Value

	PlannedCheckResults *states.CheckResults

	PlannedProviderFunctionResults []lang.FunctionResultHash

	// PlanTimestamp is the timestamp that would be returned from the
	// "plantimestamp" function in modules inside this component. We
	// must preserve this in the raw plan data to ensure that we can
	// return the same timestamp again during the apply phase.
	PlanTimestamp time.Time
}

var _ PlannedChange = (*PlannedChangeComponentInstance)(nil)

// PlannedChangeProto implements PlannedChange.
func (pc *PlannedChangeComponentInstance) PlannedChangeProto() (*stacks.PlannedChange, error) {
	var plannedInputValues map[string]*tfstackdata1.DynamicValue
	if n := len(pc.PlannedInputValues); n != 0 {
		plannedInputValues = make(map[string]*tfstackdata1.DynamicValue, n)
		for k, v := range pc.PlannedInputValues {
			var sensitivePaths []*planproto.Path
			if pvm, ok := pc.PlannedInputValueMarks[k]; ok {
				for _, p := range pvm {
					path, err := planproto.NewPath(p.Path)
					if err != nil {
						return nil, err
					}
					sensitivePaths = append(sensitivePaths, path)
				}
			}
			plannedInputValues[k] = &tfstackdata1.DynamicValue{
				Value: &planproto.DynamicValue{
					Msgpack: v,
				},
				SensitivePaths: sensitivePaths,
			}
		}
	}

	var planTimestampStr string
	var zeroTime time.Time
	if pc.PlanTimestamp != zeroTime {
		planTimestampStr = pc.PlanTimestamp.Format(time.RFC3339)
	}

	componentAddrsRaw := make([]string, 0, pc.RequiredComponents.Len())
	for componentAddr := range pc.RequiredComponents.All() {
		componentAddrsRaw = append(componentAddrsRaw, componentAddr.String())
	}

	outputDescs := make(map[string]*stacks.DynamicValue, len(pc.PlannedOutputValues))
	plannedOutputValues := make(map[string]*tfstackdata1.DynamicValue, len(outputDescs))
	for k, v := range pc.PlannedOutputValues {
		dv, err := stacks.ToDynamicValue(v, cty.DynamicPseudoType)
		if err != nil {
			return nil, fmt.Errorf("encoding output value %q: %w", k, err)
		}
		outputDescs[k] = dv
		plannedOutputValues[k] = tfstackdata1.Terraform1ToStackDataDynamicValue(dv)
	}

	plannedCheckResults, err := planfile.CheckResultsToPlanProto(pc.PlannedCheckResults)
	if err != nil {
		return nil, fmt.Errorf("failed to encode check results: %s", err)
	}

	var plannedFunctionResults []*planproto.FunctionCallHash
	for _, result := range pc.PlannedProviderFunctionResults {
		plannedFunctionResults = append(plannedFunctionResults, &planproto.FunctionCallHash{
			Key:    result.Key,
			Result: result.Result,
		})
	}

	mode, err := planproto.NewMode(pc.Mode)
	if err != nil {
		return nil, fmt.Errorf("failed to encode mode: %s", err)
	}

	var raw anypb.Any
	err = anypb.MarshalFrom(&raw, &tfstackdata1.PlanComponentInstance{
		ComponentInstanceAddr:   pc.Addr.String(),
		PlanTimestamp:           planTimestampStr,
		PlannedInputValues:      plannedInputValues,
		PlannedAction:           planproto.NewAction(pc.Action),
		Mode:                    mode,
		PlanApplyable:           pc.PlanApplyable,
		PlanComplete:            pc.PlanComplete,
		DependsOnComponentAddrs: componentAddrsRaw,
		PlannedOutputValues:     plannedOutputValues,
		PlannedCheckResults:     plannedCheckResults,
		FunctionResults:         plannedFunctionResults,
	}, proto.MarshalOptions{})
	if err != nil {
		return nil, err
	}

	protoChangeTypes, err := stacks.ChangeTypesForPlanAction(pc.Action)
	if err != nil {
		return nil, err
	}

	return &stacks.PlannedChange{
		Raw: []*anypb.Any{&raw},
		Descriptions: []*stacks.PlannedChange_ChangeDescription{
			{
				Description: &stacks.PlannedChange_ChangeDescription_ComponentInstancePlanned{
					ComponentInstancePlanned: &stacks.PlannedChange_ComponentInstance{
						Addr: &stacks.ComponentInstanceInStackAddr{
							ComponentAddr:         stackaddrs.ConfigComponentForAbsInstance(pc.Addr).String(),
							ComponentInstanceAddr: pc.Addr.String(),
						},
						Actions:      protoChangeTypes,
						PlanComplete: pc.PlanComplete,
						OutputValues: outputDescs,
						// We don't include "applyable" in here since for a
						// stack operation it's the overall stack plan applyable
						// flag that matters, and the per-component flags
						// are just an implementation detail.
					},
				},
			},
		},
	}, nil
}

// PlannedChangeResourceInstancePlanned announces an action that Terraform
// is proposing to take if this plan is applied.
type PlannedChangeResourceInstancePlanned struct {
	ResourceInstanceObjectAddr stackaddrs.AbsResourceInstanceObject

	// ChangeSrc describes the planned change, if any. This can be nil if
	// we're only intending to update the state to match PriorStateSrc.
	ChangeSrc *plans.ResourceInstanceChangeSrc

	// PriorStateSrc describes the "prior state" that the planned change, if
	// any, was generated against.
	//
	// This can be nil if the object didn't previously exist. If both
	// PriorStateSrc and ChangeSrc are nil then that suggests that the
	// object existed in the previous run's state but was found to no
	// longer exist while refreshing during plan.
	PriorStateSrc *states.ResourceInstanceObjectSrc

	// ProviderConfigAddr is the address of the provider configuration
	// that planned this change, resolved in terms of the configuration for
	// the component this resource instance object belongs to.
	ProviderConfigAddr addrs.AbsProviderConfig

	// Schema MUST be the same schema that was used to encode the dynamic
	// values inside ChangeSrc and PriorStateSrc.
	//
	// Can be empty if and only if ChangeSrc and PriorStateSrc are both nil
	// themselves.
	Schema providers.Schema
}

var _ PlannedChange = (*PlannedChangeResourceInstancePlanned)(nil)

func (pc *PlannedChangeResourceInstancePlanned) PlanResourceInstanceChangePlannedProto() (*tfstackdata1.PlanResourceInstanceChangePlanned, error) {
	rioAddr := pc.ResourceInstanceObjectAddr

	if pc.ChangeSrc == nil && pc.PriorStateSrc == nil {
		// This is just a stubby placeholder to remind us to drop the
		// apparently-deleted-outside-of-Terraform object from the state
		// if this plan later gets applied.

		return &tfstackdata1.PlanResourceInstanceChangePlanned{
			ComponentInstanceAddr: rioAddr.Component.String(),
			ResourceInstanceAddr:  rioAddr.Item.ResourceInstance.String(),
			DeposedKey:            rioAddr.Item.DeposedKey.String(),
			ProviderConfigAddr:    pc.ProviderConfigAddr.String(),
		}, nil
	}

	// We include the prior state as part of the raw plan because that
	// contains the result of upgrading the state to the provider's latest
	// schema version and incorporating any changes detected in the refresh
	// step, which we'll rely on during the apply step to make sure that
	// the final plan is consistent, etc.
	priorStateProto := tfstackdata1.ResourceInstanceObjectStateToTFStackData1(pc.PriorStateSrc, pc.ProviderConfigAddr)

	changeProto, err := planfile.ResourceChangeToProto(pc.ChangeSrc)
	if err != nil {
		return nil, fmt.Errorf("converting resource instance change to proto: %w", err)
	}

	return &tfstackdata1.PlanResourceInstanceChangePlanned{
		ComponentInstanceAddr: rioAddr.Component.String(),
		ResourceInstanceAddr:  rioAddr.Item.ResourceInstance.String(),
		DeposedKey:            rioAddr.Item.DeposedKey.String(),
		ProviderConfigAddr:    pc.ProviderConfigAddr.String(),
		Change:                changeProto,
		PriorState:            priorStateProto,
	}, nil
}

func (pc *PlannedChangeResourceInstancePlanned) ChangeDescription() (*stacks.PlannedChange_ChangeDescription, error) {
	rioAddr := pc.ResourceInstanceObjectAddr
	// We only emit an external description if there's a change to describe.
	// Otherwise, we just emit a raw to remind us to update the state for
	// this object during the apply step, to match the prior state.
	if pc.ChangeSrc == nil {
		return nil, nil
	}

	protoChangeTypes, err := stacks.ChangeTypesForPlanAction(pc.ChangeSrc.Action)
	if err != nil {
		return nil, err
	}
	replacePaths, err := encodePathSet(pc.ChangeSrc.RequiredReplace)
	if err != nil {
		return nil, err
	}

	var moved *stacks.PlannedChange_ResourceInstance_Moved
	var imported *stacks.PlannedChange_ResourceInstance_Imported

	if pc.ChangeSrc.Moved() {
		moved = &stacks.PlannedChange_ResourceInstance_Moved{
			PrevAddr: stacks.NewResourceInstanceInStackAddr(stackaddrs.AbsResourceInstance{
				Component: rioAddr.Component,
				Item:      pc.ChangeSrc.PrevRunAddr,
			}),
		}
	}

	if pc.ChangeSrc.Importing != nil {
		imported = &stacks.PlannedChange_ResourceInstance_Imported{
			ImportId: pc.ChangeSrc.Importing.ID,
			Unknown:  pc.ChangeSrc.Importing.Unknown,
		}
	}

	var index *stacks.PlannedChange_ResourceInstance_Index
	if pc.ChangeSrc.Addr.Resource.Key != nil {
		key := pc.ChangeSrc.Addr.Resource.Key
		if key == addrs.WildcardKey {
			index = &stacks.PlannedChange_ResourceInstance_Index{
				Unknown: true,
			}
		} else {
			value, err := DynamicValueToTerraform1(key.Value(), cty.DynamicPseudoType)
			if err != nil {
				return nil, err
			}
			index = &stacks.PlannedChange_ResourceInstance_Index{
				Value: value,
			}
		}
	}

	return &stacks.PlannedChange_ChangeDescription{
		Description: &stacks.PlannedChange_ChangeDescription_ResourceInstancePlanned{
			ResourceInstancePlanned: &stacks.PlannedChange_ResourceInstance{
				Addr:         stacks.NewResourceInstanceObjectInStackAddr(rioAddr),
				ResourceName: pc.ChangeSrc.Addr.Resource.Resource.Name,
				Index:        index,
				ModuleAddr:   pc.ChangeSrc.Addr.Module.String(),
				ResourceMode: stackutils.ResourceModeForProto(pc.ChangeSrc.Addr.Resource.Resource.Mode),
				ResourceType: pc.ChangeSrc.Addr.Resource.Resource.Type,
				ProviderAddr: pc.ChangeSrc.ProviderAddr.Provider.String(),
				ActionReason: pc.ChangeSrc.ActionReason.String(),

				Actions: protoChangeTypes,
				Values: &stacks.DynamicValueChange{
					Old: stacks.NewDynamicValue(
						pc.ChangeSrc.Before,
						pc.ChangeSrc.BeforeSensitivePaths,
					),
					New: stacks.NewDynamicValue(
						pc.ChangeSrc.After,
						pc.ChangeSrc.AfterSensitivePaths,
					),
				},
				ReplacePaths: replacePaths,
				Moved:        moved,
				Imported:     imported,
			},
		},
	}, nil
}

func DynamicValueToTerraform1(val cty.Value, ty cty.Type) (*stacks.DynamicValue, error) {
	unmarkedVal, markPaths := val.UnmarkDeepWithPaths()
	sensitivePaths, withOtherMarks := marks.PathsWithMark(markPaths, marks.Sensitive)
	_, withOtherMarks = marks.PathsWithMark(withOtherMarks, marks.Sensitive)
	if len(withOtherMarks) != 0 {
		return nil, withOtherMarks[0].Path.NewErrorf(
			"can't serialize value marked with %#v (this is a bug in Terraform)",
			withOtherMarks[0].Marks,
		)
	}

	rawVal, err := msgpack.Marshal(unmarkedVal, ty)
	if err != nil {
		return nil, err
	}
	ret := &stacks.DynamicValue{
		Msgpack: rawVal,
	}

	if len(markPaths) == 0 {
		return ret, nil
	}

	ret.Sensitive = make([]*stacks.AttributePath, 0, len(markPaths))
	for _, path := range sensitivePaths {
		ret.Sensitive = append(ret.Sensitive, stacks.NewAttributePath(path))
	}
	return ret, nil
}

// PlannedChangeProto implements PlannedChange.
func (pc *PlannedChangeResourceInstancePlanned) PlannedChangeProto() (*stacks.PlannedChange, error) {
	pric, err := pc.PlanResourceInstanceChangePlannedProto()
	if err != nil {
		return nil, err
	}
	var raw anypb.Any
	err = anypb.MarshalFrom(&raw, pric, proto.MarshalOptions{})
	if err != nil {
		return nil, err
	}

	if pc.ChangeSrc == nil && pc.PriorStateSrc == nil {
		// We only emit a "raw" in this case, because this is a relatively
		// uninteresting edge-case. The PlanResourceInstanceChangePlannedProto
		// function should have returned a placeholder value for this use case.

		return &stacks.PlannedChange{
			Raw: []*anypb.Any{&raw},
		}, nil
	}

	var descs []*stacks.PlannedChange_ChangeDescription
	desc, err := pc.ChangeDescription()
	if err != nil {
		return nil, err
	}
	if desc != nil {
		descs = append(descs, desc)
	}

	return &stacks.PlannedChange{
		Raw:          []*anypb.Any{&raw},
		Descriptions: descs,
	}, nil
}

// PlannedChangeDeferredResourceInstancePlanned announces that an action that Terraform
// is proposing to take if this plan is applied is being deferred.
type PlannedChangeDeferredResourceInstancePlanned struct {
	// ResourceInstancePlanned is the planned change that is being deferred.
	ResourceInstancePlanned PlannedChangeResourceInstancePlanned

	// DeferredReason is the reason why the change is being deferred.
	DeferredReason providers.DeferredReason
}

var _ PlannedChange = (*PlannedChangeDeferredResourceInstancePlanned)(nil)

// PlannedChangeProto implements PlannedChange.
func (dpc *PlannedChangeDeferredResourceInstancePlanned) PlannedChangeProto() (*stacks.PlannedChange, error) {
	change, err := dpc.ResourceInstancePlanned.PlanResourceInstanceChangePlannedProto()
	if err != nil {
		return nil, err
	}

	// We'll ignore the error here. We certainly should not have got this far
	// if we have a deferred reason that the Terraform Core runtime doesn't
	// recognise. There will be diagnostics elsewhere to reflect this, as we
	// can just use INVALID to capture this. This also makes us forwards and
	// backwards compatible, as we'll return INVALID for any new deferred
	// reasons that are added in the future without erroring.
	deferredReason, _ := planfile.DeferredReasonToProto(dpc.DeferredReason)

	var raw anypb.Any
	err = anypb.MarshalFrom(&raw, &tfstackdata1.PlanDeferredResourceInstanceChange{
		Change: change,
		Deferred: &planproto.Deferred{
			Reason: deferredReason,
		},
	}, proto.MarshalOptions{})
	if err != nil {
		return nil, err
	}
	ricd, err := dpc.ResourceInstancePlanned.ChangeDescription()
	if err != nil {
		return nil, err
	}

	var descs []*stacks.PlannedChange_ChangeDescription
	descs = append(descs, &stacks.PlannedChange_ChangeDescription{
		Description: &stacks.PlannedChange_ChangeDescription_ResourceInstanceDeferred{
			ResourceInstanceDeferred: &stacks.PlannedChange_ResourceInstanceDeferred{
				ResourceInstance: ricd.GetResourceInstancePlanned(),
				Deferred:         EncodeDeferred(dpc.DeferredReason),
			},
		},
	})

	return &stacks.PlannedChange{
		Raw:          []*anypb.Any{&raw},
		Descriptions: descs,
	}, nil
}

func EncodeDeferred(reason providers.DeferredReason) *stacks.Deferred {
	deferred := new(stacks.Deferred)
	switch reason {
	case providers.DeferredReasonInstanceCountUnknown:
		deferred.Reason = stacks.Deferred_INSTANCE_COUNT_UNKNOWN
	case providers.DeferredReasonResourceConfigUnknown:
		deferred.Reason = stacks.Deferred_RESOURCE_CONFIG_UNKNOWN
	case providers.DeferredReasonProviderConfigUnknown:
		deferred.Reason = stacks.Deferred_PROVIDER_CONFIG_UNKNOWN
	case providers.DeferredReasonAbsentPrereq:
		deferred.Reason = stacks.Deferred_ABSENT_PREREQ
	case providers.DeferredReasonDeferredPrereq:
		deferred.Reason = stacks.Deferred_DEFERRED_PREREQ
	default:
		deferred.Reason = stacks.Deferred_INVALID
	}
	return deferred
}

func encodePathSet(pathSet cty.PathSet) ([]*stacks.AttributePath, error) {
	if pathSet.Empty() {
		return nil, nil
	}

	pathList := pathSet.List()
	paths := make([]*stacks.AttributePath, 0, len(pathList))

	for _, path := range pathList {
		paths = append(paths, stacks.NewAttributePath(path))
	}
	return paths, nil
}

// PlannedChangeOutputValue announces the change action for one output value
// declared in the top-level stack configuration.
//
// This change type only includes an external description, and does not
// contribute anything to the raw plan sequence.
type PlannedChangeOutputValue struct {
	Addr          stackaddrs.OutputValue // Covers only root stack output values
	Action        plans.Action
	Before, After cty.Value
}

var _ PlannedChange = (*PlannedChangeOutputValue)(nil)

// PlannedChangeProto implements PlannedChange.
func (pc *PlannedChangeOutputValue) PlannedChangeProto() (*stacks.PlannedChange, error) {
	protoChangeTypes, err := stacks.ChangeTypesForPlanAction(pc.Action)
	if err != nil {
		return nil, err
	}

	before, err := stacks.ToDynamicValue(pc.Before, cty.DynamicPseudoType)
	if err != nil {
		return nil, fmt.Errorf("failed to encode planned output value %s: %w", pc.Addr, err)
	}

	after, err := stacks.ToDynamicValue(pc.After, cty.DynamicPseudoType)
	if err != nil {
		return nil, fmt.Errorf("failed to encode planned output value %s: %w", pc.Addr, err)
	}

	var raw []*anypb.Any
	if pc.Action == plans.Delete {
		var r anypb.Any
		if err := anypb.MarshalFrom(&r, &tfstackdata1.DeletedRootOutputValue{
			Name: pc.Addr.Name,
		}, proto.MarshalOptions{}); err != nil {
			return nil, fmt.Errorf("failed to encode raw state for %s: %w", pc.Addr, err)
		}

		raw = []*anypb.Any{&r}
	}

	return &stacks.PlannedChange{
		Raw: raw,
		Descriptions: []*stacks.PlannedChange_ChangeDescription{
			{
				Description: &stacks.PlannedChange_ChangeDescription_OutputValuePlanned{
					OutputValuePlanned: &stacks.PlannedChange_OutputValue{
						Name:    pc.Addr.Name,
						Actions: protoChangeTypes,
						Values: &stacks.DynamicValueChange{
							Old: before,
							New: after,
						},
					},
				},
			},
		},
	}, nil
}

// PlannedChangeHeader is a special change type we typically emit before any
// others to capture overall metadata about a plan. [LoadFromProto] fails if
// asked to decode a plan sequence that doesn't include at least one raw
// message generated from this change type.
//
// PlannedChangeHeader has only a raw message and does not contribute to
// the external-facing plan description.
type PlannedChangeHeader struct {
	TerraformVersion *version.Version
}

var _ PlannedChange = (*PlannedChangeHeader)(nil)

// PlannedChangeProto implements PlannedChange.
func (pc *PlannedChangeHeader) PlannedChangeProto() (*stacks.PlannedChange, error) {
	var raw anypb.Any
	err := anypb.MarshalFrom(&raw, &tfstackdata1.PlanHeader{
		TerraformVersion: pc.TerraformVersion.String(),
	}, proto.MarshalOptions{})
	if err != nil {
		return nil, err
	}

	return &stacks.PlannedChange{
		Raw: []*anypb.Any{&raw},
	}, nil
}

// PlannedChangePriorStateElement is a special change type we emit to capture
// each element of the prior state.
//
// PlannedChangePriorStateElement has only a raw message and does not
// contribute to the external-facing plan description, since it's really just
// an implementation detail that allows us to deal with various state cleanup
// concerns during the apply phase; this isn't really a "planned change" in
// the typical sense.
type PlannedChangePriorStateElement struct {
	Key string
	Raw *anypb.Any
}

var _ PlannedChange = (*PlannedChangePriorStateElement)(nil)

// PlannedChangeProto implements PlannedChange.
func (pc *PlannedChangePriorStateElement) PlannedChangeProto() (*stacks.PlannedChange, error) {
	var raw anypb.Any
	err := anypb.MarshalFrom(&raw, &tfstackdata1.PlanPriorStateElem{
		Key: pc.Key,
		Raw: pc.Raw,
	}, proto.MarshalOptions{})
	if err != nil {
		return nil, err
	}

	return &stacks.PlannedChange{
		Raw: []*anypb.Any{&raw},
	}, nil
}

// PlannedChangePlannedTimestamp is a special change type we emit to record the timestamp
// of when the plan was generated. This is being used in the plantimestamp function.
type PlannedChangePlannedTimestamp struct {
	PlannedTimestamp time.Time
}

var _ PlannedChange = (*PlannedChangePlannedTimestamp)(nil)

// PlannedChangeProto implements PlannedChange.
func (pc *PlannedChangePlannedTimestamp) PlannedChangeProto() (*stacks.PlannedChange, error) {
	var raw anypb.Any
	err := anypb.MarshalFrom(&raw, &tfstackdata1.PlanTimestamp{
		PlanTimestamp: pc.PlannedTimestamp.Format(time.RFC3339),
	}, proto.MarshalOptions{})
	if err != nil {
		return nil, err
	}

	return &stacks.PlannedChange{
		Raw: []*anypb.Any{&raw},
	}, nil
}

// PlannedChangeApplyable is a special change type we typically append at the
// end of the raw plan stream to represent that the planning process ran to
// completion without encountering any errors, and therefore the plan could
// potentially be applied.
type PlannedChangeApplyable struct {
	Applyable bool
}

var _ PlannedChange = (*PlannedChangeApplyable)(nil)

// PlannedChangeProto implements PlannedChange.
func (pc *PlannedChangeApplyable) PlannedChangeProto() (*stacks.PlannedChange, error) {
	var raw anypb.Any
	err := anypb.MarshalFrom(&raw, &tfstackdata1.PlanApplyable{
		Applyable: pc.Applyable,
	}, proto.MarshalOptions{})
	if err != nil {
		return nil, err
	}

	return &stacks.PlannedChange{
		Raw: []*anypb.Any{&raw},
		Descriptions: []*stacks.PlannedChange_ChangeDescription{
			{
				Description: &stacks.PlannedChange_ChangeDescription_PlanApplyable{
					PlanApplyable: pc.Applyable,
				},
			},
		},
	}, nil
}

type PlannedChangeProviderFunctionResults struct {
	Results []lang.FunctionResultHash
}

var _ PlannedChange = (*PlannedChangeProviderFunctionResults)(nil)

func (pc *PlannedChangeProviderFunctionResults) PlannedChangeProto() (*stacks.PlannedChange, error) {
	var results tfstackdata1.FunctionResults
	for _, result := range pc.Results {
		results.FunctionResults = append(results.FunctionResults, &planproto.FunctionCallHash{
			Key:    result.Key,
			Result: result.Result,
		})
	}

	var raw anypb.Any
	err := anypb.MarshalFrom(&raw, &results, proto.MarshalOptions{})
	if err != nil {
		return nil, err
	}

	return &stacks.PlannedChange{
		Raw: []*anypb.Any{&raw},
	}, nil
}

// PlannedChangeActionInvocationInstancePlanned represents a planned action
// invocation within a component instance.
type PlannedChangeActionInvocationInstancePlanned struct {
	ActionInvocationAddr stackaddrs.AbsActionInvocationInstance

	// Invocation describes the planned invocation.
	Invocation *plans.ActionInvocationInstanceSrc

	// ProviderConfigAddr is the address of the provider configuration
	// that planned this change, resolved in terms of the configuration for
	// the component this action invocation belongs to.
	ProviderConfigAddr addrs.AbsProviderConfig

	// Schema MUST be the same schema that was used to encode the dynamic
	// values inside Invocation.
	//
	// Can be empty if and only if Invocation is nil.
	Schema providers.ActionSchema
}

var _ PlannedChange = (*PlannedChangeActionInvocationInstancePlanned)(nil)

// PlanActionInvocationProto converts the planned action invocation to the
// internal protobuf representation for persistence.
func (pc *PlannedChangeActionInvocationInstancePlanned) PlanActionInvocationProto() (*tfstackdata1.PlanActionInvocationPlanned, error) {
	addr := pc.ActionInvocationAddr

	if pc.Invocation == nil {
		// Return a minimal placeholder if there's no actual invocation
		return &tfstackdata1.PlanActionInvocationPlanned{
			ComponentInstanceAddr: addr.Component.String(),
			ActionInvocationAddr:  addr.Item.String(),
			ProviderConfigAddr:    pc.ProviderConfigAddr.Provider.String(),
		}, nil
	}

	invocationProto, err := planfile.ActionInvocationToProto(pc.Invocation)
	if err != nil {
		return nil, fmt.Errorf("converting action invocation to proto: %w", err)
	}

	return &tfstackdata1.PlanActionInvocationPlanned{
		ComponentInstanceAddr: addr.Component.String(),
		ActionInvocationAddr:  addr.Item.String(),
		ProviderConfigAddr:    pc.ProviderConfigAddr.Provider.String(),
		Invocation:            invocationProto,
	}, nil
}

// ChangeDescription implements PlannedChange by producing an external
// description of the action invocation for the RPC API.
func (pc *PlannedChangeActionInvocationInstancePlanned) ChangeDescription() (*stacks.PlannedChange_ChangeDescription, error) {
	addr := pc.ActionInvocationAddr

	// We only emit an external description if there's an invocation to describe.
	if pc.Invocation == nil {
		return nil, nil
	}

	invoke := stacks.PlannedChange_ActionInvocationInstance{
		Addr:         stacks.NewActionInvocationInStackAddr(addr),
		ProviderAddr: pc.Invocation.ProviderAddr.Provider.String(),
		ActionType:   pc.Invocation.Addr.Action.Action.Type,

		ConfigValue: stacks.NewDynamicValue(
			pc.Invocation.ConfigValue,
			pc.Invocation.SensitiveConfigPaths,
		),
	}

	// Convert the action trigger information
	switch at := pc.Invocation.ActionTrigger.(type) {
	case *plans.ResourceActionTrigger:
		triggerEvent, err := stacks.ActionTriggerEventForPlannedChange(at.ActionTriggerEvent)
		if err != nil {
			return nil, err
		}

		invoke.ActionTrigger = &stacks.PlannedChange_ActionInvocationInstance_ResourceActionTrigger{
			ResourceActionTrigger: &stacks.PlannedChange_ResourceActionTrigger{
				TriggerEvent: triggerEvent,
				TriggeringResourceAddress: stacks.NewResourceInstanceInStackAddr(stackaddrs.AbsResourceInstance{
					Component: addr.Component,
					Item:      at.TriggeringResourceAddr,
				}),
				ActionTriggerBlockIndex: int64(at.ActionTriggerBlockIndex),
				ActionsListIndex:        int64(at.ActionsListIndex),
			},
		}
	case *plans.InvokeActionTrigger:
		// TODO Implement this when implementing Stacks support for Direct Action Invocation
		invoke.ActionTrigger = &stacks.PlannedChange_ActionInvocationInstance_InvokeActionTrigger{
			InvokeActionTrigger: &stacks.PlannedChange_InvokeActionTrigger{},
		}
	default:
		// This should be exhaustive
		return nil, fmt.Errorf("unsupported action trigger type: %T", at)
	}

	return &stacks.PlannedChange_ChangeDescription{
		Description: &stacks.PlannedChange_ChangeDescription_ActionInvocationPlanned{
			ActionInvocationPlanned: &invoke,
		},
	}, nil
}

// PlannedChangeProto implements PlannedChange.
func (pc *PlannedChangeActionInvocationInstancePlanned) PlannedChangeProto() (*stacks.PlannedChange, error) {
	paip, err := pc.PlanActionInvocationProto()
	if err != nil {
		return nil, err
	}
	var raw anypb.Any
	err = anypb.MarshalFrom(&raw, paip, proto.MarshalOptions{})
	if err != nil {
		return nil, err
	}

	if pc.Invocation == nil {
		// We only emit a "raw" in this case, because this is a relatively
		// uninteresting edge-case. The PlanActionInvocationProto
		// function should have returned a placeholder value for this use case.

		return &stacks.PlannedChange{
			Raw: []*anypb.Any{&raw},
		}, nil
	}

	var descs []*stacks.PlannedChange_ChangeDescription
	desc, err := pc.ChangeDescription()
	if err != nil {
		return nil, err
	}
	if desc != nil {
		descs = append(descs, desc)
	}

	return &stacks.PlannedChange{
		Raw:          []*anypb.Any{&raw},
		Descriptions: descs,
	}, nil
}

// PlannedChangeDeferredActionInvocation represents an action invocation
// that was deferred due to incomplete information.
type PlannedChangeDeferredActionInvocation struct {
	// ActionInvocationPlanned is the planned action invocation that is being deferred.
	ActionInvocationPlanned PlannedChangeActionInvocationInstancePlanned

	// DeferredReason is the reason why the action invocation is being deferred.
	DeferredReason providers.DeferredReason
}

var _ PlannedChange = (*PlannedChangeDeferredActionInvocation)(nil)

// PlannedChangeProto implements PlannedChange.
func (dpc *PlannedChangeDeferredActionInvocation) PlannedChangeProto() (*stacks.PlannedChange, error) {
	action, err := dpc.ActionInvocationPlanned.PlanActionInvocationProto()
	if err != nil {
		return nil, err
	}

	// Convert the deferred reason to proto format
	deferredReason, _ := planfile.DeferredReasonToProto(dpc.DeferredReason)

	var raw anypb.Any
	err = anypb.MarshalFrom(&raw, &tfstackdata1.PlanDeferredActionInvocation{
		Invocation: action,
		Deferred: &planproto.Deferred{
			Reason: deferredReason,
		},
	}, proto.MarshalOptions{})
	if err != nil {
		return nil, err
	}

	// Build the change description
	aicd, err := dpc.ActionInvocationPlanned.ChangeDescription()
	if err != nil {
		return nil, err
	}

	var descs []*stacks.PlannedChange_ChangeDescription
	if aicd != nil {
		descs = append(descs, &stacks.PlannedChange_ChangeDescription{
			Description: &stacks.PlannedChange_ChangeDescription_ActionInvocationDeferred{
				ActionInvocationDeferred: &stacks.PlannedChange_ActionInvocationDeferred{
					ActionInvocation: aicd.GetActionInvocationPlanned(),
					Deferred:         EncodeDeferred(dpc.DeferredReason),
				},
			},
		})
	}

	return &stacks.PlannedChange{
		Raw:          []*anypb.Any{&raw},
		Descriptions: descs,
	}, nil
}
