// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

// Package backend provides interfaces that the CLI uses to interact with
// OpenTofu. A backend provides the abstraction that allows the same CLI
// to simultaneously support both local and remote operations for seamlessly
// using OpenTofu in a team environment.
package backend

import (
	"context"
	"errors"
	"log"
	"os"

	"github.com/mitchellh/go-homedir"
	"github.com/opentofu/svchost"
	"github.com/zclconf/go-cty/cty"

	"github.com/opentofu/opentofu/internal/addrs"
	"github.com/opentofu/opentofu/internal/command/clistate"
	"github.com/opentofu/opentofu/internal/command/views"
	"github.com/opentofu/opentofu/internal/configs"
	"github.com/opentofu/opentofu/internal/configs/configload"
	"github.com/opentofu/opentofu/internal/configs/configschema"
	"github.com/opentofu/opentofu/internal/depsfile"
	"github.com/opentofu/opentofu/internal/encryption"
	"github.com/opentofu/opentofu/internal/plans"
	"github.com/opentofu/opentofu/internal/plans/planfile"
	"github.com/opentofu/opentofu/internal/states"
	"github.com/opentofu/opentofu/internal/states/statemgr"
	"github.com/opentofu/opentofu/internal/tfdiags"
	"github.com/opentofu/opentofu/internal/tofu"
)

// DefaultStateName is the name of the default, initial state that every
// backend must have. This state cannot be deleted.
const DefaultStateName = "default"

var (
	// ErrDefaultWorkspaceNotSupported is returned when an operation does not
	// support using the default workspace, but requires a named workspace to
	// be selected.
	ErrDefaultWorkspaceNotSupported = errors.New("default workspace not supported\n" +
		"You can create a new workspace with the \"workspace new\" command.")

	// ErrWorkspacesNotSupported is an error returned when a caller attempts
	// to perform an operation on a workspace other than "default" for a
	// backend that doesn't support multiple workspaces.
	//
	// The caller can detect this to do special fallback behavior or produce
	// a specific, helpful error message.
	ErrWorkspacesNotSupported = errors.New("workspaces not supported")
)

// InitFn is used to initialize a new backend.
type InitFn func(encryption.StateEncryption) Backend

// Backend is the minimal interface that must be implemented to enable OpenTofu.
type Backend interface {
	// ConfigSchema returns a description of the expected configuration
	// structure for the receiving backend.
	//
	// This method does not have any side-effects for the backend and can
	// be safely used before configuring.
	ConfigSchema() *configschema.Block

	// PrepareConfig checks the validity of the values in the given
	// configuration, and inserts any missing defaults, assuming that its
	// structure has already been validated per the schema returned by
	// ConfigSchema.
	//
	// This method does not have any side-effects for the backend and can
	// be safely used before configuring. It also does not consult any
	// external data such as environment variables, disk files, etc. Validation
	// that requires such external data should be deferred until the
	// Configure call.
	//
	// If error diagnostics are returned then the configuration is not valid
	// and must not subsequently be passed to the Configure method.
	//
	// This method may return configuration-contextual diagnostics such
	// as tfdiags.AttributeValue, and so the caller should provide the
	// necessary context via the diags.InConfigBody method before returning
	// diagnostics to the user.
	PrepareConfig(cty.Value) (cty.Value, tfdiags.Diagnostics)

	// Configure uses the provided configuration to set configuration fields
	// within the backend.
	//
	// The given configuration is assumed to have already been validated
	// against the schema returned by ConfigSchema and passed validation
	// via PrepareConfig.
	//
	// This method may be called only once per backend instance, and must be
	// called before all other methods except where otherwise stated.
	//
	// If error diagnostics are returned, the internal state of the instance
	// is undefined and no other methods may be called.
	Configure(context.Context, cty.Value) tfdiags.Diagnostics

	// StateMgr returns the state manager for the given workspace name.
	//
	// If the returned state manager also implements statemgr.Locker then
	// it's the caller's responsibility to call Lock and Unlock as appropriate.
	//
	// If the named workspace doesn't exist, or if it has no state, it will
	// be created either immediately on this call or the first time
	// PersistState is called, depending on the state manager implementation.
	StateMgr(ctx context.Context, workspace string) (statemgr.Full, error)

	// DeleteWorkspace removes the workspace with the given name if it exists.
	//
	// DeleteWorkspace cannot prevent deleting a state that is in use. It is
	// the responsibility of the caller to hold a Lock for the state manager
	// belonging to this workspace before calling this method.
	DeleteWorkspace(ctx context.Context, name string, force bool) error

	// States returns a list of the names of all of the workspaces that exist
	// in this backend.
	Workspaces(context.Context) ([]string, error)
}

// HostAlias describes a list of aliases that should be used when initializing an
// Enhanced Backend
type HostAlias struct {
	From svchost.Hostname
	To   svchost.Hostname
}

// Enhanced implements additional behavior on top of a normal backend.
//
// 'Enhanced' backends are an implementation detail only, and are no longer reflected as an external
// 'feature' of backends. In other words, backends refer to plugins for remote state snapshot
// storage only, and the Enhanced interface here is a necessary vestige of the 'local' and
// remote/cloud backends only.
type Enhanced interface {
	Backend

	// Operation performs a OpenTofu operation such as refresh, plan, apply.
	// It is up to the implementation to determine what "performing" means.
	// This DOES NOT BLOCK. The context returned as part of RunningOperation
	// should be used to block for completion.
	// If the state used in the operation can be locked, it is the
	// responsibility of the Backend to lock the state for the duration of the
	// running operation.
	Operation(context.Context, *Operation) (*RunningOperation, error)

	// ServiceDiscoveryAliases returns a mapping of Alias -> Target hosts to
	// configure.
	ServiceDiscoveryAliases() ([]HostAlias, error)
}

// Local implements additional behavior on a Backend that allows local
// operations in addition to remote operations.
//
// This enables more behaviors of OpenTofu that require more data such
// as `console`, `import`, `graph`. These require direct access to
// configurations, variables, and more. Not all backends may support this
// so we separate it out into its own optional interface.
type Local interface {
	// LocalRun uses information in the Operation to prepare a set of objects
	// needed to start running that operation.
	//
	// The operation doesn't need a Type set, but it needs various other
	// options set. This is a rather odd API that tries to treat all
	// operations as the same when they really aren't; see the local and remote
	// backend's implementations of this to understand what this actually
	// does, because this operation has no well-defined contract aside from
	// "whatever it already does".
	LocalRun(context.Context, *Operation) (*LocalRun, statemgr.Full, tfdiags.Diagnostics)
}

// LocalRun represents the assortment of objects that we can collect or
// calculate from an Operation object, which we can then use for local
// operations.
//
// The operation methods on tofu.Context (Plan, Apply, Import, etc) each
// generate new artifacts which supersede parts of the LocalRun object that
// started the operation, so callers should be careful to use those subsequent
// artifacts instead of the fields of LocalRun where appropriate. The LocalRun
// data intentionally doesn't update as a result of calling methods on Context,
// in order to make data flow explicit.
//
// This type is a weird architectural wart resulting from the overly-general
// way our backend API models operations, whereby we behave as if all
// OpenTofu operations have the same inputs and outputs even though they
// are actually all rather different. The exact meaning of the fields in
// this type therefore vary depending on which OperationType was passed to
// Local.Context in order to create an object of this type.
type LocalRun struct {
	// Core is an already-initialized OpenTofu Core context, ready to be
	// used to run operations such as Plan and Apply.
	Core *tofu.Context

	// Config is the configuration we're working with, which typically comes
	// from either config files directly on local disk (when we're creating
	// a plan, or similar) or from a snapshot embedded in a plan file
	// (when we're applying a saved plan).
	Config *configs.Config

	// InputState is the state that should be used for whatever is the first
	// method call to a context created with CoreOpts. When creating a plan
	// this will be the previous run state, but when applying a saved plan
	// this will be the prior state recorded in that plan.
	InputState *states.State

	// PlanOpts are options to pass to a Plan or Plan-like operation.
	//
	// This is nil when we're applying a saved plan, because the plan itself
	// contains enough information about its options to apply it.
	PlanOpts *tofu.PlanOpts

	// Plan is a plan loaded from a saved plan file, if our operation is to
	// apply that saved plan.
	//
	// This is nil when we're not applying a saved plan.
	Plan *plans.Plan

	// ApplyOpts are options that are passed into the Apply operation.
	//
	// This will be nil most of the times except when the apply command is
	// executed with a plan file. In that particular case, the opts will
	// contain the variable values given by the user to the `tofu apply`
	// command. This is later used to merge the variable values defined in
	// the plan with the ones defined in the CLI.
	ApplyOpts *tofu.ApplyOpts
}

// An operation represents an operation for OpenTofu to execute.
//
// Note that not all fields are supported by all backends and can result
// in an error if set. All backend implementations should show user-friendly
// errors explaining any incorrectly set values. For example, the local
// backend doesn't support a PlanId being set.
//
// The operation options are purposely designed to have maximal compatibility
// between OpenTofu and Terraform Servers (a commercial product offered by
// HashiCorp). Therefore, it isn't expected that other implementation support
// every possible option. The struct here is generalized in order to allow
// even partial implementations to exist in the open, without walling off
// remote functionality 100% behind a commercial wall. Anyone can implement
// against this interface and have OpenTofu interact with it just as it
// would with HashiCorp-provided Terraform Servers.
type Operation struct {
	// Type is the operation to perform.
	Type OperationType

	// Encryption is used by enhanced backends for planning and tofu.Context initialization
	Encryption encryption.Encryption

	// PlanId is an opaque value that backends can use to execute a specific
	// plan for an apply operation.
	//
	// PlanOutBackend is the backend to store with the plan. This is the
	// backend that will be used when applying the plan.
	PlanId         string
	PlanRefresh    bool   // PlanRefresh will do a refresh before a plan
	PlanOutPath    string // PlanOutPath is the path to save the plan
	PlanOutBackend *plans.Backend

	// ConfigDir is the path to the directory containing the configuration's
	// root module.
	ConfigDir string

	// ConfigLoader is a configuration loader that can be used to load
	// configuration from ConfigDir.
	ConfigLoader *configload.Loader

	// DependencyLocks represents the locked dependencies associated with
	// the configuration directory given in ConfigDir.
	//
	// Note that if field PlanFile is set then the plan file should contain
	// its own dependency locks. The backend is responsible for correctly
	// selecting between these two sets of locks depending on whether it
	// will be using ConfigDir or PlanFile to get the configuration for
	// this operation.
	DependencyLocks *depsfile.Locks

	// Hooks can be used to perform actions triggered by various events during
	// the operation's lifecycle.
	Hooks []tofu.Hook

	// Plan is a plan that was passed as an argument. This is valid for
	// plan and apply arguments but may not work for all backends.
	PlanFile *planfile.WrappedPlanFile

	// The options below are more self-explanatory and affect the runtime
	// behavior of the operation.
	PlanMode     plans.Mode
	AutoApprove  bool
	Targets      []addrs.Targetable
	Excludes     []addrs.Targetable
	ForceReplace []addrs.AbsResourceInstance
	// Injected by the command creating the operation (plan/apply/refresh/etc...)
	Variables map[string]UnparsedVariableValue
	RootCall  configs.StaticModuleCall
	// SuppressForgetErrorsDuringDestroy suppresses the error that occurs when a
	// destroy operation completes successfully but leaves forgotten instances behind.
	SuppressForgetErrorsDuringDestroy bool
	// Some operations use root module variables only opportunistically or
	// don't need them at all. If this flag is set, the backend must treat
	// all variables as optional and provide an unknown value for any required
	// variables that aren't set in order to allow partial evaluation against
	// the resulting incomplete context.
	//
	// This flag is honored only if PlanFile isn't set. If PlanFile is set then
	// the variables set in the plan are used instead, and they must be valid.
	AllowUnsetVariables bool

	// View implements the logic for all UI interactions.
	View views.Operation

	// Input/output/control options.
	UIIn tofu.UIInput

	// StateLocker is used to lock the state while providing UI feedback to the
	// user. This will be replaced by the Backend to update the context.
	//
	// If state locking is not necessary, this should be set to a no-op
	// implementation of clistate.Locker.
	StateLocker clistate.Locker

	// Workspace is the name of the workspace that this operation should run
	// in, which controls which named state is used.
	Workspace string

	// GenerateConfigOut tells the operation both that it should generate config
	// for unmatched import targets and where any generated config should be
	// written to.
	GenerateConfigOut string
}

// HasConfig returns true if and only if the operation has a ConfigDir value
// that refers to a directory containing at least one OpenTofu configuration
// file.
func (o *Operation) HasConfig() bool {
	return o.ConfigLoader.IsConfigDir(o.ConfigDir)
}

// ReportResult is a helper for the common chore of setting the status of
// a running operation and showing any diagnostics produced during that
// operation.
//
// If the given diagnostics contains errors then the operation's result
// will be set to backend.OperationFailure. It will be set to
// backend.OperationSuccess otherwise. It will then use o.View.Diagnostics
// to show the given diagnostics before returning.
//
// Callers should feel free to do each of these operations separately in
// more complex cases where e.g. diagnostics are interleaved with other
// output, but terminating immediately after reporting error diagnostics is
// common and can be expressed concisely via this method.
func (o *Operation) ReportResult(op *RunningOperation, diags tfdiags.Diagnostics) {
	if diags.HasErrors() {
		op.Result = OperationFailure
	} else {
		op.Result = OperationSuccess
	}
	if o.View != nil {
		o.View.Diagnostics(diags)
	} else {
		// Shouldn't generally happen, but if it does then we'll at least
		// make some noise in the logs to help us spot it.
		if len(diags) != 0 {
			log.Printf(
				"[ERROR] Backend needs to report diagnostics but View is not set:\n%s",
				diags.ErrWithWarnings(),
			)
		}
	}
}

// RunningOperation is the result of starting an operation.
type RunningOperation struct {
	// For implementers of a backend, this context should not wrap the
	// passed in context. Otherwise, cancelling the parent context will
	// immediately mark this context as "done" but those aren't the semantics
	// we want: we want this context to be done only when the operation itself
	// is fully done.
	context.Context

	// Stop requests the operation to complete early, by calling Stop on all
	// the plugins. If the process needs to terminate immediately, call Cancel.
	Stop context.CancelFunc

	// Cancel is the context.CancelFunc associated with the embedded context,
	// and can be called to terminate the operation early.
	// Once Cancel is called, the operation should return as soon as possible
	// to avoid running operations during process exit.
	Cancel context.CancelFunc

	// Result is the exit status of the operation, populated only after the
	// operation has completed.
	Result OperationResult

	// PlanEmpty is populated after a Plan operation completes to note whether
	// a plan is empty or has changes. This is only used in the CLI to determine
	// the exit status because the plan value is not available at that point.
	PlanEmpty bool

	// State is the final state after the operation completed. Persisting
	// this state is managed by the backend. This should only be read
	// after the operation completes to avoid read/write races.
	State *states.State
}

// OperationResult describes the result status of an operation.
type OperationResult int

const (
	// OperationSuccess indicates that the operation completed as expected.
	OperationSuccess OperationResult = 0

	// OperationFailure indicates that the operation encountered some sort
	// of error, and thus may have been only partially performed or not
	// performed at all.
	OperationFailure OperationResult = 1
)

func (r OperationResult) ExitStatus() int {
	return int(r)
}

// If the argument is a path, ReadPathOrContents loads it and returns the contents,
// otherwise the argument is assumed to be the desired contents and is simply
// returned.
func ReadPathOrContents(poc string) (string, error) {
	if len(poc) == 0 {
		return poc, nil
	}

	path := poc
	if path[0] == '~' {
		var err error
		path, err = homedir.Expand(path)
		if err != nil {
			return path, err
		}
	}

	if _, err := os.Stat(path); err == nil {
		contents, err := os.ReadFile(path)
		if err != nil {
			return string(contents), err
		}
		return string(contents), nil
	}

	return poc, nil
}
