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

package states

import (
	"fmt"
	"sort"

	"github.com/zclconf/go-cty/cty"
	ctyjson "github.com/zclconf/go-cty/cty/json"

	"github.com/hashicorp/terraform/internal/addrs"
	"github.com/hashicorp/terraform/internal/lang/format"
	"github.com/hashicorp/terraform/internal/lang/marks"
	"github.com/hashicorp/terraform/internal/providers"
)

// ResourceInstanceObject is the local representation of a specific remote
// object associated with a resource instance. In practice not all remote
// objects are actually remote in the sense of being accessed over the network,
// but this is the most common case.
//
// It is not valid to mutate a ResourceInstanceObject once it has been created.
// Instead, create a new object and replace the existing one.
type ResourceInstanceObject struct {
	// Value is the object-typed value representing the remote object within
	// Terraform.
	Value cty.Value

	// Identity is the object-typed value representing the identity of the remote
	// object within Terraform.
	Identity cty.Value

	// Private is an opaque value set by the provider when this object was
	// last created or updated. Terraform Core does not use this value in
	// any way and it is not exposed anywhere in the user interface, so
	// a provider can use it for retaining any necessary private state.
	Private []byte

	// Status represents the "readiness" of the object as of the last time
	// it was updated.
	Status ObjectStatus

	// Dependencies is a set of absolute address to other resources this
	// instance dependeded on when it was applied. This is used to construct
	// the dependency relationships for an object whose configuration is no
	// longer available, such as if it has been removed from configuration
	// altogether, or is now deposed.
	Dependencies []addrs.ConfigResource

	// CreateBeforeDestroy reflects the status of the lifecycle
	// create_before_destroy option when this instance was last updated.
	// Because create_before_destroy also effects the overall ordering of the
	// destroy operations, we need to record the status to ensure a resource
	// removed from the config will still be destroyed in the same manner.
	CreateBeforeDestroy bool
}

// NewResourceInstanceObjectFromIR converts the receiving
// ImportedResource into a ResourceInstanceObject that has status ObjectReady.
//
// The returned object does not know its own resource type, so the caller must
// retain the ResourceType value from the source object if this information is
// needed.
//
// The returned object also has no dependency addresses, but the caller may
// freely modify the direct fields of the returned object without affecting
// the receiver.
func NewResourceInstanceObjectFromIR(ir providers.ImportedResource) *ResourceInstanceObject {
	return &ResourceInstanceObject{
		Status:   ObjectReady,
		Value:    ir.State,
		Private:  ir.Private,
		Identity: ir.Identity,
	}
}

// ObjectStatus represents the status of a RemoteObject.
type ObjectStatus rune

//go:generate go tool golang.org/x/tools/cmd/stringer -type ObjectStatus

const (
	// ObjectReady is an object status for an object that is ready to use.
	ObjectReady ObjectStatus = 'R'

	// ObjectTainted is an object status representing an object that is in
	// an unrecoverable bad state due to a partial failure during a create,
	// update, or delete operation. Since it cannot be moved into the
	// ObjectRead state, a tainted object must be replaced.
	ObjectTainted ObjectStatus = 'T'

	// ObjectPlanned is a special object status used only for the transient
	// placeholder objects we place into state during the refresh and plan
	// walks to stand in for objects that will be created during apply.
	//
	// Any object of this status must have a corresponding change recorded
	// in the current plan, whose value must then be used in preference to
	// the value stored in state when evaluating expressions. A planned
	// object stored in state will be incomplete if any of its attributes are
	// not yet known, and the plan must be consulted in order to "see" those
	// unknown values, because the state is not able to represent them.
	ObjectPlanned ObjectStatus = 'P'
)

// Encode marshals values within the receiver to produce a
// ResourceInstanceObjectSrc ready to be written to a state file.
//
// The schema must contain the resource type body, and the given value must
// conform its implied type. The schema must also contain the version number
// of the schema, which will be recorded in the source object so it can be
// used to detect when schema migration is required on read.
// The schema may also contain an resource identity schema and version number,
// which will be used to encode the resource identity.
//
// The returned object may share internal references with the receiver and
// so the caller must not mutate the receiver any further once once this
// method is called.
func (o *ResourceInstanceObject) Encode(schema providers.Schema) (*ResourceInstanceObjectSrc, error) {
	// If it contains marks, remove these marks before traversing the
	// structure with UnknownAsNull, and save the PathValueMarks
	// so we can save them in state.
	val, sensitivePaths, err := unmarkValueForStorage(o.Value)
	if err != nil {
		return nil, err
	}

	// Our state serialization can't represent unknown values, so we convert
	// them to nulls here. This is lossy, but nobody should be writing unknown
	// values here and expecting to get them out again later.
	//
	// We get unknown values here while we're building out a "planned state"
	// during the plan phase, but the value stored in the plan takes precedence
	// for expression evaluation. The apply step should never produce unknown
	// values, but if it does it's the responsibility of the caller to detect
	// and raise an error about that.
	val = cty.UnknownAsNull(val)

	src, err := ctyjson.Marshal(val, schema.Body.ImpliedType())
	if err != nil {
		return nil, err
	}

	var idJSON []byte
	// If the Identity is known and not null we can marshal it.
	if !o.Identity.IsNull() && o.Identity.IsWhollyKnown() && schema.Identity != nil {
		idJSON, err = ctyjson.Marshal(o.Identity, schema.Identity.ImpliedType())
		if err != nil {
			return nil, err
		}
	}

	// Dependencies are collected and merged in an unordered format (using map
	// keys as a set), then later changed to a slice (in random ordering) to be
	// stored in state as an array. To avoid pointless thrashing of state in
	// refresh-only runs, we can either override comparison of dependency lists
	// (more desirable, but tricky for Reasons) or just sort when encoding.
	// Encoding of instances can happen concurrently, so we must copy the
	// dependencies to avoid mutating what may be a shared array of values.
	dependencies := make([]addrs.ConfigResource, len(o.Dependencies))
	copy(dependencies, o.Dependencies)

	sort.Slice(dependencies, func(i, j int) bool { return dependencies[i].String() < dependencies[j].String() })

	return &ResourceInstanceObjectSrc{
		SchemaVersion:         uint64(schema.Version),
		AttrsJSON:             src,
		AttrSensitivePaths:    sensitivePaths,
		Private:               o.Private,
		Status:                o.Status,
		Dependencies:          dependencies,
		CreateBeforeDestroy:   o.CreateBeforeDestroy,
		IdentityJSON:          idJSON,
		IdentitySchemaVersion: uint64(schema.IdentityVersion),
		// The cached value must have all its marks since it bypasses decoding.
		decodeValueCache:    o.Value,
		decodeIdentityCache: o.Identity,
	}, nil
}

// AsTainted returns a deep copy of the receiver with the status updated to
// ObjectTainted.
func (o *ResourceInstanceObject) AsTainted() *ResourceInstanceObject {
	if o == nil {
		// A nil object can't be tainted, but we'll allow this anyway to
		// avoid a crash, since we presumably intend to eventually record
		// the object has having been deleted anyway.
		return nil
	}
	ret := o.DeepCopy()
	ret.Status = ObjectTainted
	return ret
}

// unmarkValueForStorage takes a value that possibly contains marked values
// and returns an equal value without markings along with the separated mark
// metadata that should be stored alongside the value in another field.
//
// This function only accepts the marks that are valid to store, and so will
// return an error if other marks are present. Marks that this package doesn't
// know how to store must be dealt with somehow by a caller -- presumably by
// replacing each marked value with some sort of storage placeholder -- before
// writing a value into the state.
func unmarkValueForStorage(v cty.Value) (unmarkedV cty.Value, sensitivePaths []cty.Path, err error) {
	val, pvms := v.UnmarkDeepWithPaths()
	sensitivePaths, withOtherMarks := marks.PathsWithMark(pvms, marks.Sensitive)
	_, withOtherMarks = marks.PathsWithMark(withOtherMarks, marks.Deprecation)
	if len(withOtherMarks) != 0 {
		return cty.NilVal, nil, fmt.Errorf(
			"%s: cannot serialize value marked as %#v for inclusion in a state snapshot (this is a bug in Terraform)",
			format.CtyPath(withOtherMarks[0].Path), withOtherMarks[0].Marks,
		)
	}

	// sort the sensitive paths for consistency in comparison and serialization
	sort.Slice(sensitivePaths, func(i, j int) bool {
		// use our human-readable format of paths for comparison
		return format.CtyPath(sensitivePaths[i]) < format.CtyPath(sensitivePaths[j])
	})

	return val, sensitivePaths, nil
}
