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

package arguments

import (
	"flag"
	"fmt"
	"time"

	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/hclsyntax"

	"github.com/hashicorp/terraform/internal/addrs"
	"github.com/hashicorp/terraform/internal/plans"
	"github.com/hashicorp/terraform/internal/tfdiags"
)

// DefaultParallelism is the limit Terraform places on total parallel
// operations as it walks the dependency graph.
const DefaultParallelism = 10

// State describes arguments which are used to define how Terraform interacts
// with state.
type State struct {
	// Lock controls whether or not the state manager is used to lock state
	// during operations.
	Lock bool

	// LockTimeout allows setting a time limit on acquiring the state lock.
	// The default is 0, meaning no limit.
	LockTimeout time.Duration

	// StatePath specifies a non-default location for the state file. The
	// default value is blank, which is interpeted as "terraform.tfstate".
	StatePath string

	// StateOutPath specifies a different path to write the final state file.
	// The default value is blank, which results in state being written back to
	// StatePath.
	StateOutPath string

	// BackupPath specifies the path where a backup copy of the state file will
	// be stored before the new state is written. The default value is blank,
	// which is interpreted as StateOutPath +
	// ".backup".
	BackupPath string
}

// Operation describes arguments which are used to configure how a Terraform
// operation such as a plan or apply executes.
type Operation struct {
	// PlanMode selects one of the mutually-exclusive planning modes that
	// decides the overall goal of a plan operation. This field is relevant
	// only for an operation that produces a plan.
	PlanMode plans.Mode

	// Parallelism is the limit Terraform places on total parallel operations
	// as it walks the dependency graph.
	Parallelism int

	// Refresh controls whether or not the operation should refresh existing
	// state before proceeding. Default is true.
	Refresh bool

	// Targets allow limiting an operation to a set of resource addresses and
	// their dependencies.
	Targets []addrs.Targetable

	// ActionTargets means we should just invoke the actions specified here, and
	// not run a complete plan. Targets and ActionTargets are mutually exclusive
	// and this should only be set for plan and apply operations.
	ActionTargets []addrs.Targetable

	// ForceReplace addresses cause Terraform to force a particular set of
	// resource instances to generate "replace" actions in any plan where they
	// would normally have generated "no-op" or "update" actions.
	//
	// This is currently limited to specific instances because typical uses
	// of replace are associated with only specific remote objects that the
	// user has somehow learned to be malfunctioning, in which case it
	// would be unusual and potentially dangerous to replace everything under
	// a module all at once. We could potentially loosen this later if we
	// learn a use-case for broader matching.
	ForceReplace []addrs.AbsResourceInstance

	// DeferralAllowed enables experimental support for automatically performing
	// a partial plan if some objects are not yet plannable (due to unknown
	// values in count/for_each, or due to other missing dependencies that can't
	// be resolved in a single plan/apply cycle).
	//
	// IMPORTANT: This feature should only be available when Terraform is built
	// with experimental features enabled. Since extendedFlagSet can't currently
	// test whether experimental features are enabled, the check needs to happen
	// when _reading_ these Operation arguments and transferring values to the
	// backendrun.Operation struct.
	DeferralAllowed bool

	// These private fields are used only temporarily during decoding. Use
	// method Parse to populate the exported fields from these, validating
	// the raw values in the process.
	targetsRaw       []string
	actionTargetsRaw []string
	forceReplaceRaw  []string
	destroyRaw       bool
	refreshOnlyRaw   bool
}

// Parse must be called on Operation after initial flag parse. This processes
// the raw target flags into addrs.Targetable values, returning diagnostics if
// invalid.
func (o *Operation) Parse() tfdiags.Diagnostics {
	var diags tfdiags.Diagnostics

	o.Targets = nil

	for _, tr := range o.targetsRaw {
		traversal, syntaxDiags := hclsyntax.ParseTraversalAbs([]byte(tr), "", hcl.Pos{Line: 1, Column: 1})
		if syntaxDiags.HasErrors() {
			diags = diags.Append(tfdiags.Sourceless(
				tfdiags.Error,
				fmt.Sprintf("Invalid target %q", tr),
				syntaxDiags[0].Detail,
			))
			continue
		}

		target, targetDiags := addrs.ParseTarget(traversal)
		if targetDiags.HasErrors() {
			diags = diags.Append(tfdiags.Sourceless(
				tfdiags.Error,
				fmt.Sprintf("Invalid target %q", tr),
				targetDiags[0].Description().Detail,
			))
			continue
		}

		o.Targets = append(o.Targets, target.Subject)
	}

	for _, tr := range o.actionTargetsRaw {
		traversal, syntaxDiags := hclsyntax.ParseTraversalAbs([]byte(tr), "", hcl.Pos{Line: 1, Column: 1})
		if syntaxDiags.HasErrors() {
			diags = diags.Append(tfdiags.Sourceless(
				tfdiags.Error,
				fmt.Sprintf("Invalid target %q", tr),
				syntaxDiags[0].Detail,
			))
			continue
		}

		target, targetDiags := addrs.ParseTargetAction(traversal)
		if targetDiags.HasErrors() {
			diags = diags.Append(tfdiags.Sourceless(
				tfdiags.Error,
				fmt.Sprintf("Invalid target %q", tr),
				targetDiags[0].Description().Detail,
			))
			continue
		}

		o.ActionTargets = append(o.ActionTargets, target.Subject)
	}

	if len(o.ActionTargets) > 0 && len(o.Targets) > 0 {
		diags = diags.Append(tfdiags.Sourceless(
			tfdiags.Error,
			"Invalid arguments",
			"The -invoke and -target arguments must not both be specified."))
	}

	if len(o.ActionTargets) > 1 {
		// TEMP: Disallow targeting multiple actions for now.
		diags = diags.Append(tfdiags.Sourceless(
			tfdiags.Error,
			"Invalid arguments",
			"Only one action can be invoked at a time."))
	}

	for _, raw := range o.forceReplaceRaw {
		traversal, syntaxDiags := hclsyntax.ParseTraversalAbs([]byte(raw), "", hcl.Pos{Line: 1, Column: 1})
		if syntaxDiags.HasErrors() {
			diags = diags.Append(tfdiags.Sourceless(
				tfdiags.Error,
				fmt.Sprintf("Invalid force-replace address %q", raw),
				syntaxDiags[0].Detail,
			))
			continue
		}

		addr, addrDiags := addrs.ParseAbsResourceInstance(traversal)
		if addrDiags.HasErrors() {
			diags = diags.Append(tfdiags.Sourceless(
				tfdiags.Error,
				fmt.Sprintf("Invalid force-replace address %q", raw),
				addrDiags[0].Description().Detail,
			))
			continue
		}

		if addr.Resource.Resource.Mode != addrs.ManagedResourceMode {
			diags = diags.Append(tfdiags.Sourceless(
				tfdiags.Error,
				fmt.Sprintf("Invalid force-replace address %q", raw),
				"Only managed resources can be used with the -replace=... option.",
			))
			continue
		}

		o.ForceReplace = append(o.ForceReplace, addr)
	}

	// If you add a new possible value for o.PlanMode here, consider also
	// adding a specialized error message for it in ParseApplyDestroy.
	switch {
	case o.destroyRaw && o.refreshOnlyRaw:
		diags = diags.Append(tfdiags.Sourceless(
			tfdiags.Error,
			"Incompatible plan mode options",
			"The -destroy and -refresh-only options are mutually-exclusive.",
		))
	case o.destroyRaw:
		o.PlanMode = plans.DestroyMode
	case o.refreshOnlyRaw:
		o.PlanMode = plans.RefreshOnlyMode
		if !o.Refresh {
			diags = diags.Append(tfdiags.Sourceless(
				tfdiags.Error,
				"Incompatible refresh options",
				"It doesn't make sense to use -refresh-only at the same time as -refresh=false, because Terraform would have nothing to do.",
			))
		}
	default:
		if len(o.ActionTargets) > 0 {
			o.PlanMode = plans.RefreshOnlyMode
		} else {
			o.PlanMode = plans.NormalMode
		}
	}

	return diags
}

// extendedFlagSet creates a FlagSet with common backend, operation, and vars
// flags used in many commands. Target structs for each subset of flags must be
// provided in order to support those flags.
func extendedFlagSet(name string, state *State, operation *Operation, vars *Vars) *flag.FlagSet {
	f := defaultFlagSet(name)

	if state == nil && operation == nil && vars == nil {
		panic("use defaultFlagSet")
	}

	if state != nil {
		f.BoolVar(&state.Lock, "lock", true, "lock")
		f.DurationVar(&state.LockTimeout, "lock-timeout", 0, "lock-timeout")
		f.StringVar(&state.StatePath, "state", "", "state-path")
		f.StringVar(&state.StateOutPath, "state-out", "", "state-path")
		f.StringVar(&state.BackupPath, "backup", "", "backup-path")
	}

	if operation != nil {
		f.IntVar(&operation.Parallelism, "parallelism", DefaultParallelism, "parallelism")
		f.BoolVar(&operation.DeferralAllowed, "allow-deferral", false, "allow-deferral")
		f.BoolVar(&operation.Refresh, "refresh", true, "refresh")
		f.BoolVar(&operation.destroyRaw, "destroy", false, "destroy")
		f.BoolVar(&operation.refreshOnlyRaw, "refresh-only", false, "refresh-only")
		f.Var((*FlagStringSlice)(&operation.targetsRaw), "target", "target")
		f.Var((*FlagStringSlice)(&operation.actionTargetsRaw), "invoke", "invoke")
		f.Var((*FlagStringSlice)(&operation.forceReplaceRaw), "replace", "replace")
	}

	// Gather all -var and -var-file arguments into one heterogenous structure
	// to preserve the overall order.
	if vars != nil {
		varsFlags := NewFlagNameValueSlice("-var")
		varFilesFlags := varsFlags.Alias("-var-file")
		vars.vars = &varsFlags
		vars.varFiles = &varFilesFlags
		f.Var(vars.vars, "var", "var")
		f.Var(vars.varFiles, "var-file", "var-file")
	}

	return f
}
