// Copyright IBM Corp. 2019, 2026
// SPDX-License-Identifier: MPL-2.0

package tfjson

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"

	"github.com/hashicorp/go-version"
)

// PlanFormatVersionConstraints defines the versions of the JSON plan format
// that are supported by this package.
var PlanFormatVersionConstraints = ">= 0.1, < 2.0"

// ResourceMode is a string representation of the resource type found
// in certain fields in the plan.
type ResourceMode string

const (
	// DataResourceMode is the resource mode for data sources.
	DataResourceMode ResourceMode = "data"

	// ManagedResourceMode is the resource mode for managed resources.
	ManagedResourceMode ResourceMode = "managed"
)

// Plan represents the entire contents of an output Terraform plan.
type Plan struct {
	// useJSONNumber opts into the behavior of calling
	// json.Decoder.UseNumber prior to decoding the plan, which turns
	// numbers into json.Numbers instead of float64s. Set it using
	// Plan.UseJSONNumber.
	useJSONNumber bool

	// The version of the plan format. This should always match the
	// PlanFormatVersion constant in this package, or else an unmarshal
	// will be unstable.
	FormatVersion string `json:"format_version,omitempty"`

	// The version of Terraform used to make the plan.
	TerraformVersion string `json:"terraform_version,omitempty"`

	// The variables set in the root module when creating the plan.
	Variables map[string]*PlanVariable `json:"variables,omitempty"`

	// The common state representation of resources within this plan.
	// This is a product of the existing state merged with the diff for
	// this plan.
	PlannedValues *StateValues `json:"planned_values,omitempty"`

	// The change operations for resources and data sources within this plan
	// resulting from resource drift.
	ResourceDrift []*ResourceChange `json:"resource_drift,omitempty"`

	// The change operations for resources and data sources within this
	// plan.
	ResourceChanges []*ResourceChange `json:"resource_changes,omitempty"`

	// DeferredChanges contains the change operations for resources that are deferred
	// for this plan.
	DeferredChanges []*DeferredResourceChange `json:"deferred_changes,omitempty"`

	// Complete indicates that all resources have successfully planned changes.
	// This will be false if there are DeferredChanges or if the -target flag is used.
	//
	// Complete was introduced in Terraform 1.8 and will be nil for all previous
	// Terraform versions.
	Complete *bool `json:"complete,omitempty"`

	// The change operations for outputs within this plan.
	OutputChanges map[string]*Change `json:"output_changes,omitempty"`

	// The Terraform state prior to the plan operation. This is the
	// same format as PlannedValues, without the current diff merged.
	PriorState *State `json:"prior_state,omitempty"`

	// The Terraform configuration used to make the plan.
	Config *Config `json:"configuration,omitempty"`

	// RelevantAttributes represents any resource instances and their
	// attributes which may have contributed to the planned changes
	RelevantAttributes []ResourceAttribute `json:"relevant_attributes,omitempty"`

	// Checks contains the results of any conditional checks executed, or
	// planned to be executed, during this plan.
	Checks []CheckResultStatic `json:"checks,omitempty"`

	// Timestamp contains the static timestamp that Terraform considers to be
	// the time this plan executed, in UTC.
	Timestamp string `json:"timestamp,omitempty"`

	ActionInvocations []*ActionInvocation `json:"action_invocations,omitempty"`
}

// ResourceAttribute describes a full path to a resource attribute
type ResourceAttribute struct {
	// Resource describes resource instance address (e.g. null_resource.foo)
	Resource string `json:"resource"`
	// Attribute describes the attribute path using a lossy representation
	// of cty.Path. (e.g. ["id"] or ["objects", 0, "val"]).
	Attribute []json.RawMessage `json:"attribute"`
}

// UseJSONNumber controls whether the Plan will be decoded using the
// json.Number behavior or the float64 behavior. When b is true, the Plan will
// represent numbers in PlanOutputs as json.Numbers. When b is false, the
// Plan will represent numbers in PlanOutputs as float64s.
func (p *Plan) UseJSONNumber(b bool) {
	p.useJSONNumber = b
}

// Validate checks to ensure that the plan is present, and the
// version matches the version supported by this library.
func (p *Plan) Validate() error {
	if p == nil {
		return errors.New("plan is nil")
	}

	if p.FormatVersion == "" {
		return errors.New("unexpected plan input, format version is missing")
	}

	constraint, err := version.NewConstraint(PlanFormatVersionConstraints)
	if err != nil {
		return fmt.Errorf("invalid version constraint: %w", err)
	}

	version, err := version.NewVersion(p.FormatVersion)
	if err != nil {
		return fmt.Errorf("invalid format version %q: %w", p.FormatVersion, err)
	}

	if !constraint.Check(version) {
		return fmt.Errorf("unsupported plan format version: %q does not satisfy %q",
			version, constraint)
	}

	return nil
}

// UnmarshalJSON implements json.Unmarshaler for Plan.
//
// As per established convention this method should only ever
// be invoked *indirectly* via [encoding/json] library.
func (p *Plan) UnmarshalJSON(b []byte) error {
	type rawPlan Plan
	var plan rawPlan

	dec := json.NewDecoder(bytes.NewReader(b))
	if p.useJSONNumber {
		dec.UseNumber()
	}
	err := dec.Decode(&plan)
	if err != nil {
		return err
	}

	*p = *(*Plan)(&plan)

	return p.Validate()
}

// ResourceChange is a description of an individual change action
// that Terraform plans to use to move from the prior state to a new
// state matching the configuration.
type ResourceChange struct {
	// The absolute resource address.
	Address string `json:"address,omitempty"`

	// The absolute address that this resource instance had
	// at the conclusion of a previous plan.
	PreviousAddress string `json:"previous_address,omitempty"`

	// The module portion of the above address. Omitted if the instance
	// is in the root module.
	ModuleAddress string `json:"module_address,omitempty"`

	// The resource mode.
	Mode ResourceMode `json:"mode,omitempty"`

	// The resource type, example: "aws_instance" for aws_instance.foo.
	Type string `json:"type,omitempty"`

	// The resource name, example: "foo" for aws_instance.foo.
	Name string `json:"name,omitempty"`

	// The instance key for any resources that have been created using
	// "count" or "for_each". If neither of these apply the key will be
	// empty.
	//
	// This value can be either an integer (int) or a string.
	Index interface{} `json:"index,omitempty"`

	// The name of the provider this resource belongs to. This allows
	// the provider to be interpreted unambiguously in the unusual
	// situation where a provider offers a resource type whose name
	// does not start with its own name, such as the "googlebeta"
	// provider offering "google_compute_instance".
	ProviderName string `json:"provider_name,omitempty"`

	// An identifier used during replacement operations, and can be
	// used to identify the exact resource being replaced in state.
	DeposedKey string `json:"deposed,omitempty"`

	// The data describing the change that will be made to this object.
	Change *Change `json:"change,omitempty"`

	// ActionReason is an optional keyword providing extra context for why the
	// actions in Change.Actions were chosen, for example that a resource must
	// be replaced because its existing object was tainted. It is empty when
	// Terraform did not report a reason. This detail is for display purposes
	// only and should not be used to make decisions.
	ActionReason ActionReason `json:"action_reason,omitempty"`
}

// ActionReason is a keyword representing the optional reason Terraform reports
// for the actions proposed in a ResourceChange. The set of possible values may
// grow in future Terraform versions, so consumers should treat unrecognized
// values as equivalent to ActionReasonNone.
//
// The canonical list of reason keywords is defined by Terraform in
// internal/command/jsonplan/plan.go (the ResourceInstance* constants):
// https://github.com/hashicorp/terraform/blob/main/internal/command/jsonplan/plan.go
type ActionReason string

const (
	// ActionReasonNone is the absence of any specific reason; Terraform omits
	// the action_reason property in this case.
	ActionReasonNone ActionReason = ""

	// ActionReasonReplaceBecauseCannotUpdate indicates a resource must be
	// replaced because the requested change is not possible without doing so.
	ActionReasonReplaceBecauseCannotUpdate ActionReason = "replace_because_cannot_update"

	// ActionReasonReplaceBecauseTainted indicates a resource must be replaced
	// because its existing object was marked as tainted.
	ActionReasonReplaceBecauseTainted ActionReason = "replace_because_tainted"

	// ActionReasonReplaceByRequest indicates a resource must be replaced because
	// the user explicitly requested it (e.g. "terraform plan -replace=...").
	ActionReasonReplaceByRequest ActionReason = "replace_by_request"

	// ActionReasonReplaceByTriggers indicates a resource must be replaced
	// because of a change in a value referenced by its replace_triggered_by.
	ActionReasonReplaceByTriggers ActionReason = "replace_by_triggers"

	// ActionReasonDeleteBecauseNoResourceConfig indicates a resource will be
	// destroyed because it has no matching configuration block.
	ActionReasonDeleteBecauseNoResourceConfig ActionReason = "delete_because_no_resource_config"

	// ActionReasonDeleteBecauseWrongRepetition indicates a resource instance
	// will be destroyed because its instance key does not match the resource's
	// count/for_each repetition mode.
	ActionReasonDeleteBecauseWrongRepetition ActionReason = "delete_because_wrong_repetition"

	// ActionReasonDeleteBecauseCountIndex indicates a resource instance will be
	// destroyed because its index is out of range for the count value.
	ActionReasonDeleteBecauseCountIndex ActionReason = "delete_because_count_index"

	// ActionReasonDeleteBecauseEachKey indicates a resource instance will be
	// destroyed because its key is not present in the for_each value.
	ActionReasonDeleteBecauseEachKey ActionReason = "delete_because_each_key"

	// ActionReasonDeleteBecauseNoModule indicates a resource instance will be
	// destroyed because its containing module instance is no longer declared.
	ActionReasonDeleteBecauseNoModule ActionReason = "delete_because_no_module"

	// ActionReasonDeleteBecauseNoMoveTarget indicates a resource was moved to an
	// address that has no configuration, so it will be destroyed.
	ActionReasonDeleteBecauseNoMoveTarget ActionReason = "delete_because_no_move_target"

	// ActionReasonReadBecauseConfigUnknown indicates a data resource will be
	// read during apply because its configuration contains unknown values.
	ActionReasonReadBecauseConfigUnknown ActionReason = "read_because_config_unknown"

	// ActionReasonReadBecauseDependencyPending indicates a data resource will be
	// read during apply because it depends on a resource or module with changes
	// pending.
	ActionReasonReadBecauseDependencyPending ActionReason = "read_because_dependency_pending"

	// ActionReasonReadBecauseCheckNested indicates a data resource nested within
	// a check block will be read during apply.
	ActionReasonReadBecauseCheckNested ActionReason = "read_because_check_nested"
)

// Change is the representation of a proposed change for an object.
type Change struct {
	// The action to be carried out by this change.
	Actions Actions `json:"actions,omitempty"`

	// Before and After are representations of the object value both
	// before and after the action. For create and delete actions,
	// either Before or After is unset (respectively). For no-op
	// actions, both values will be identical. After will be incomplete
	// if there are values within it that won't be known until after
	// apply.
	Before interface{} `json:"before,"`
	After  interface{} `json:"after,omitempty"`

	// A deep object of booleans that denotes any values that are
	// unknown in a resource. These values were previously referred to
	// as "computed" values.
	//
	// If the value cannot be found in this map, then its value should
	// be available within After, so long as the operation supports it.
	AfterUnknown interface{} `json:"after_unknown,omitempty"`

	// BeforeSensitive and AfterSensitive are object values with similar
	// structure to Before and After, but with all sensitive leaf values
	// replaced with true, and all non-sensitive leaf values omitted. These
	// objects should be combined with Before and After to prevent accidental
	// display of sensitive values in user interfaces.
	BeforeSensitive interface{} `json:"before_sensitive,omitempty"`
	AfterSensitive  interface{} `json:"after_sensitive,omitempty"`

	// Importing contains the import metadata about this operation. If importing
	// is present (ie. not null) then the change is an import operation in
	// addition to anything mentioned in the actions field. The actual contents
	// of the Importing struct is subject to change, so downstream consumers
	// should treat any values in here as strictly optional.
	Importing *Importing `json:"importing,omitempty"`

	// GeneratedConfig contains any HCL config generated for this resource
	// during planning as a string.
	//
	// If this is populated, then Importing should also be populated but this
	// might change in the future. However, not all Importing changes will
	// contain generated config.
	GeneratedConfig string `json:"generated_config,omitempty"`

	// ReplacePaths contains a set of paths that point to attributes/elements
	// that are causing the overall resource to be replaced rather than simply
	// updated.
	//
	// This field is always a slice of indexes, where an index in this context
	// is either an integer pointing to a child of a set/list, or a string
	// pointing to the child of a map, object, or block.
	ReplacePaths []interface{} `json:"replace_paths,omitempty"`

	// BeforeIdentity and AfterIdentity are representations of the resource
	// identity value both before and after the action.
	BeforeIdentity interface{} `json:"before_identity,omitempty"`
	AfterIdentity  interface{} `json:"after_identity,omitempty"`
}

// Importing is a nested object for the resource import metadata.
type Importing struct {
	// The original ID of this resource used to target it as part of planned
	// import operation.
	ID string `json:"id,omitempty"`

	// Unknown indicates the ID or identity was unknown at the time of
	// planning. This would have led to the overall change being deferred, as
	// such this should only be true when processing changes from the deferred
	// changes list.
	Unknown bool `json:"unknown,omitempty"`

	// The identity can be used instead of the ID to target the resource as part
	// of the planned import operation.
	Identity interface{} `json:"identity,omitempty"`
}

// PlanVariable is a top-level variable in the Terraform plan.
type PlanVariable struct {
	// The value for this variable at plan time.
	Value interface{} `json:"value,omitempty"`
}

// DeferredResourceChange is a description of a resource change that has been
// deferred for some reason.
type DeferredResourceChange struct {
	// Reason is the reason why this resource change was deferred.
	Reason string `json:"reason,omitempty"`

	// Change contains any information we have about the deferred change.
	ResourceChange *ResourceChange `json:"resource_change,omitempty"`
}

type ActionInvocation struct {
	// Address is the absolute action address
	Address string `json:"address,omitempty"`
	// Type is the type of the action
	Type string `json:"type,omitempty"`
	// Name is the name of the action
	Name string `json:"name,omitempty"`

	// ConfigValues is the JSON representation of the values in the config block of the action
	ConfigValues    interface{} `json:"config_values,omitempty"`
	ConfigSensitive interface{} `json:"config_sensitive,omitempty"`
	ConfigUnknown   interface{} `json:"config_unknown,omitempty"`

	// ProviderName allows the property "type" to be interpreted unambiguously
	// in the unusual situation where a provider offers a type whose
	// name does not start with its own name, such as the "googlebeta" provider
	// offering "google_compute_instance".
	ProviderName string `json:"provider_name,omitempty"`

	LifecycleActionTrigger *LifecycleActionTrigger `json:"lifecycle_action_trigger,omitempty"`
	InvokeActionTrigger    *InvokeActionTrigger    `json:"invoke_action_trigger,omitempty"`
}

type LifecycleActionTrigger struct {
	TriggeringResourceAddress string `json:"triggering_resource_address,omitempty"`
	ActionTriggerEvent        string `json:"action_trigger_event,omitempty"`
	ActionTriggerBlockIndex   int    `json:"action_trigger_block_index"`
	ActionsListIndex          int    `json:"actions_list_index"`
}

type InvokeActionTrigger struct{}
