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

package structured

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"reflect"

	"github.com/hashicorp/terraform/internal/command/jsonformat/structured/attribute_path"
	"github.com/hashicorp/terraform/internal/command/jsonplan"
	"github.com/hashicorp/terraform/internal/command/jsonstate"
	viewsjson "github.com/hashicorp/terraform/internal/command/views/json"
	"github.com/hashicorp/terraform/internal/plans"
)

// Change contains the unmarshalled generic interface{} types that are output by
// the JSON functions in the various json packages (such as jsonplan and
// jsonprovider).
//
// A Change can be converted into a computed.Diff, ready for rendering, with the
// ComputeDiffForAttribute, ComputeDiffForOutput, and ComputeDiffForBlock
// functions.
//
// The Before and After fields are actually go-cty values, but we cannot convert
// them directly because of the HCP Terraform redacted endpoint. The redacted
// endpoint turns sensitive values into strings regardless of their types.
// Because of this, we cannot just do a direct conversion using the ctyjson
// package. We would have to iterate through the schema first, find the
// sensitive values and their mapped types, update the types inside the schema
// to strings, and then go back and do the overall conversion. This isn't
// including any of the more complicated parts around what happens if something
// was sensitive before and isn't sensitive after or vice versa. This would mean
// the type would need to change between the before and after value. It is in
// fact just easier to iterate through the values as generic JSON interfaces.
type Change struct {

	// BeforeExplicit matches AfterExplicit except references the Before value.
	BeforeExplicit bool

	// AfterExplicit refers to whether the After value is explicit or
	// implicit. It is explicit if it has been specified by the user, and
	// implicit if it has been set as a consequence of other changes.
	//
	// For example, explicitly setting a value to null in a list should result
	// in After being null and AfterExplicit being true. In comparison,
	// removing an element from a list should also result in After being null
	// and AfterExplicit being false. Without the explicit information our
	// functions would not be able to tell the difference between these two
	// cases.
	AfterExplicit bool

	// Before contains the value before the proposed change.
	//
	// The type of the value should be informed by the schema and cast
	// appropriately when needed.
	Before interface{}

	// After contains the value after the proposed change.
	//
	// The type of the value should be informed by the schema and cast
	// appropriately when needed.
	After interface{}

	// Unknown describes whether the After value is known or unknown at the time
	// of the plan. In practice, this means the after value should be rendered
	// simply as `(known after apply)`.
	//
	// The concrete value could be a boolean describing whether the entirety of
	// the After value is unknown, or it could be a list or a map depending on
	// the schema describing whether specific elements or attributes within the
	// value are unknown.
	Unknown interface{}

	// BeforeSensitive matches Unknown, but references whether the Before value
	// is sensitive.
	BeforeSensitive interface{}

	// AfterSensitive matches Unknown, but references whether the After value is
	// sensitive.
	AfterSensitive interface{}

	// ReplacePaths contains a set of paths that point to attributes/elements
	// that are causing the overall resource to be replaced rather than simply
	// updated.
	ReplacePaths attribute_path.Matcher

	// RelevantAttributes contains a set of paths that point attributes/elements
	// that we should display. Any element/attribute not matched by this Matcher
	// should be skipped.
	RelevantAttributes attribute_path.Matcher

	// NonLegacySchema must only be used when rendering the change to the CLI,
	// and is otherwise ignored. This flag is set when we can be sure that the
	// change originated from a resource which is not using the legacy SDK, so
	// we don't need to hide changes between empty and null strings.
	// NonLegacySchema is only switched to true by the renderer, because that is
	// where we have most of the schema information to detect the condition.
	NonLegacySchema bool
}

// FromJsonChange unmarshals the raw []byte values in the jsonplan.Change
// structs into generic interface{} types that can be reasoned about.
func FromJsonChange(change jsonplan.Change, relevantAttributes attribute_path.Matcher) Change {
	ret := Change{
		Before:             unmarshalGeneric(change.Before),
		After:              unmarshalGeneric(change.After),
		Unknown:            unmarshalGeneric(change.AfterUnknown),
		BeforeSensitive:    unmarshalGeneric(change.BeforeSensitive),
		AfterSensitive:     unmarshalGeneric(change.AfterSensitive),
		ReplacePaths:       attribute_path.Parse(change.ReplacePaths, false),
		RelevantAttributes: relevantAttributes,
	}

	// A forget-only action (i.e. ["forget"], not ["create", "forget"])
	// should be represented as a no-op, so it does not look like we are
	// proposing to delete the resource.
	if len(change.Actions) == 1 && change.Actions[0] == "forget" {
		ret = ret.AsNoOp()
	}

	return ret
}

// FromJsonResource unmarshals the raw values in the jsonstate.Resource structs
// into generic interface{} types that can be reasoned about.
func FromJsonResource(resource jsonstate.Resource) Change {
	return Change{
		// We model resource formatting as NoOps.
		Before: unwrapAttributeValues(resource.AttributeValues),
		After:  unwrapAttributeValues(resource.AttributeValues),

		// We have some sensitive values, but we don't have any unknown values.
		Unknown:         false,
		BeforeSensitive: unmarshalGeneric(resource.SensitiveValues),
		AfterSensitive:  unmarshalGeneric(resource.SensitiveValues),

		// We don't display replacement data for resources, and all attributes
		// are relevant.
		ReplacePaths:       attribute_path.Empty(false),
		RelevantAttributes: attribute_path.AlwaysMatcher(),
	}
}

// FromJsonOutput unmarshals the raw values in the jsonstate.Output structs into
// generic interface{} types that can be reasoned about.
func FromJsonOutput(output jsonstate.Output) Change {
	return Change{
		// We model resource formatting as NoOps.
		Before: unmarshalGeneric(output.Value),
		After:  unmarshalGeneric(output.Value),

		// We have some sensitive values, but we don't have any unknown values.
		Unknown:         false,
		BeforeSensitive: output.Sensitive,
		AfterSensitive:  output.Sensitive,

		// We don't display replacement data for resources, and all attributes
		// are relevant.
		ReplacePaths:       attribute_path.Empty(false),
		RelevantAttributes: attribute_path.AlwaysMatcher(),
	}
}

// FromJsonViewsOutput unmarshals the raw values in the viewsjson.Output structs into
// generic interface{} types that can be reasoned about.
func FromJsonViewsOutput(output viewsjson.Output) Change {
	return Change{
		// We model resource formatting as NoOps.
		Before: unmarshalGeneric(output.Value),
		After:  unmarshalGeneric(output.Value),

		// We have some sensitive values, but we don't have any unknown values.
		Unknown:         false,
		BeforeSensitive: output.Sensitive,
		AfterSensitive:  output.Sensitive,

		// We don't display replacement data for resources, and all attributes
		// are relevant.
		ReplacePaths:       attribute_path.Empty(false),
		RelevantAttributes: attribute_path.AlwaysMatcher(),
	}
}

func FromJsonActionInvocation(actionInvocation jsonplan.ActionInvocation) Change {
	return Change{
		Before:          unmarshalGeneric(actionInvocation.ConfigValues),
		After:           unmarshalGeneric(actionInvocation.ConfigValues),
		Unknown:         unmarshalGeneric(actionInvocation.ConfigUnknown),
		BeforeSensitive: unmarshalGeneric(actionInvocation.ConfigSensitive),
		AfterSensitive:  unmarshalGeneric(actionInvocation.ConfigSensitive),

		ReplacePaths:       attribute_path.Empty(false),
		RelevantAttributes: attribute_path.AlwaysMatcher(),
	}
}

// CalculateAction does a very simple analysis to make the best guess at the
// action this change describes. For complex types such as objects, maps, lists,
// or sets it is likely more efficient to work out the action directly instead
// of relying on this function.
func (change Change) CalculateAction() plans.Action {
	if (change.Before == nil && !change.BeforeExplicit) && (change.After != nil || change.AfterExplicit) {
		return plans.Create
	}
	if (change.After == nil && !change.AfterExplicit) && (change.Before != nil || change.BeforeExplicit) {
		return plans.Delete
	}

	if reflect.DeepEqual(change.Before, change.After) && change.AfterExplicit == change.BeforeExplicit && change.IsAfterSensitive() == change.IsBeforeSensitive() {
		return plans.NoOp
	}

	return plans.Update
}

// GetDefaultActionForIteration is used to guess what the change could be for
// complex attributes (collections and objects) and blocks.
//
// You can't really tell the difference between a NoOp and an Update just by
// looking at the attribute itself as you need to inspect the children.
//
// This function returns a Delete or a Create action if the before or after
// values were null, and returns a NoOp for all other cases. It should be used
// in conjunction with compareActions to calculate the actual action based on
// the actions of the children.
func (change Change) GetDefaultActionForIteration() plans.Action {
	if change.Before == nil && change.After == nil {
		return plans.NoOp
	}

	if change.Before == nil {
		return plans.Create
	}
	if change.After == nil {
		return plans.Delete
	}
	return plans.NoOp
}

// AsNoOp returns the current change as if it is a NoOp operation.
//
// Basically it replaces all the after values with the before values.
func (change Change) AsNoOp() Change {
	return Change{
		BeforeExplicit:     change.BeforeExplicit,
		AfterExplicit:      change.BeforeExplicit,
		Before:             change.Before,
		After:              change.Before,
		Unknown:            false,
		BeforeSensitive:    change.BeforeSensitive,
		AfterSensitive:     change.BeforeSensitive,
		ReplacePaths:       change.ReplacePaths,
		RelevantAttributes: change.RelevantAttributes,
	}
}

// AsDelete returns the current change as if it is a Delete operation.
//
// Basically it replaces all the after values with nil or false.
func (change Change) AsDelete() Change {
	return Change{
		BeforeExplicit:     change.BeforeExplicit,
		AfterExplicit:      false,
		Before:             change.Before,
		After:              nil,
		Unknown:            nil,
		BeforeSensitive:    change.BeforeSensitive,
		AfterSensitive:     nil,
		ReplacePaths:       change.ReplacePaths,
		RelevantAttributes: change.RelevantAttributes,
	}
}

// AsCreate returns the current change as if it is a Create operation.
//
// Basically it replaces all the before values with nil or false.
func (change Change) AsCreate() Change {
	return Change{
		BeforeExplicit:     false,
		AfterExplicit:      change.AfterExplicit,
		Before:             nil,
		After:              change.After,
		Unknown:            change.Unknown,
		BeforeSensitive:    nil,
		AfterSensitive:     change.AfterSensitive,
		ReplacePaths:       change.ReplacePaths,
		RelevantAttributes: change.RelevantAttributes,
	}
}

func unmarshalGeneric(raw json.RawMessage) interface{} {
	if raw == nil {
		return nil
	}

	out, err := ParseJson(bytes.NewReader(raw))
	if err != nil {
		panic("unrecognized json type: " + err.Error())
	}
	return out
}

func unwrapAttributeValues(values jsonstate.AttributeValues) map[string]interface{} {
	out := make(map[string]interface{})
	for key, value := range values {
		out[key] = unmarshalGeneric(value)
	}
	return out
}

func ParseJson(reader io.Reader) (interface{}, error) {
	decoder := json.NewDecoder(reader)
	decoder.UseNumber()

	var jv interface{}
	if err := decoder.Decode(&jv); err != nil {
		return nil, err
	}

	// The JSON decoder should have consumed the entire input stream, so
	// we should be at EOF now.
	if token, err := decoder.Token(); err != io.EOF {
		return nil, fmt.Errorf("unexpected token after valid JSON: %v", token)
	}

	return jv, nil
}
