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

package command

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"log"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/hashicorp/go-plugin"
	"github.com/hashicorp/go-retryablehttp"
	"github.com/opentofu/opentofu/internal/command/flags"
	"github.com/opentofu/svchost/disco"

	"github.com/opentofu/opentofu/internal/addrs"
	"github.com/opentofu/opentofu/internal/backend"
	"github.com/opentofu/opentofu/internal/backend/local"
	"github.com/opentofu/opentofu/internal/command/arguments"
	"github.com/opentofu/opentofu/internal/command/clistate"
	"github.com/opentofu/opentofu/internal/command/views"
	"github.com/opentofu/opentofu/internal/command/webbrowser"
	"github.com/opentofu/opentofu/internal/command/workdir"
	"github.com/opentofu/opentofu/internal/configs"
	"github.com/opentofu/opentofu/internal/configs/configload"
	"github.com/opentofu/opentofu/internal/getmodules"
	"github.com/opentofu/opentofu/internal/getproviders"
	"github.com/opentofu/opentofu/internal/plugins"
	"github.com/opentofu/opentofu/internal/providers"
	"github.com/opentofu/opentofu/internal/provisioners"
	"github.com/opentofu/opentofu/internal/states"
	"github.com/opentofu/opentofu/internal/tfdiags"
	"github.com/opentofu/opentofu/internal/tofu"
)

// Meta are the meta-options that are available on all or most commands.
type Meta struct {
	// The exported fields below should be set by anyone using a
	// command with a Meta field. These are expected to be set externally
	// (not from within the command itself).

	// WorkingDir is an object representing the "working directory" where we're
	// running commands. In the normal case this literally refers to the
	// working directory of the OpenTofu process, though this can take on
	// a more symbolic meaning when the user has overridden default behavior
	// to specify a different working directory or to override the special
	// data directory where we'll persist settings that must survive between
	// consecutive commands.
	//
	// We're currently gradually migrating the various bits of state that
	// must persist between consecutive commands in a session to be encapsulated
	// in here, but we're not there yet and so there are also some methods on
	// Meta which directly read and modify paths inside the data directory.
	WorkingDir *workdir.Dir

	View *views.View

	GlobalPluginDirs []string // Additional paths to search for plugins

	// Services provides access to remote endpoint information for
	// 'tofu-native' services running at a specific user-facing hostname.
	Services *disco.Disco

	// RunningInAutomation indicates that commands are being run by an
	// automated system rather than directly at a command prompt.
	//
	// This is a hint to various command routines that it may be confusing
	// to print out messages that suggest running specific follow-up
	// commands, since the user consuming the output will not be
	// in a position to run such commands.
	//
	// The intended use-case of this flag is when OpenTofu is running in
	// some sort of workflow orchestration tool which is abstracting away
	// the specific commands being run.
	RunningInAutomation bool

	// CLIConfigDir is the directory from which CLI configuration files were
	// read by the caller and the directory where any changes to CLI
	// configuration files by commands should be made.
	//
	// If this is empty then no configuration directory is available and
	// commands which require one cannot proceed.
	CLIConfigDir string

	// PluginCacheDir, if non-empty, enables caching of downloaded plugins
	// into the given directory.
	PluginCacheDir string

	// PluginCacheMayBreakDependencyLockFile is a temporary CLI configuration-based
	// opt out for the behavior of only using the plugin cache dir if its
	// contents match checksums recorded in the dependency lock file.
	//
	// This is an accommodation for those who currently essentially ignore the
	// dependency lock file -- treating it only as transient working directory
	// state -- and therefore don't care if the plugin cache dir causes the
	// checksums inside to only be sufficient for the computer where OpenTofu
	// is currently running.
	//
	// We intend to remove this exception again (making the CLI configuration
	// setting a silent no-op) in future once we've improved the dependency
	// lock file mechanism so that it's usable for everyone and there are no
	// longer any compelling reasons for folks to not lock their dependencies.
	PluginCacheMayBreakDependencyLockFile bool

	// ProviderSource allows determining the available versions of a provider
	// and determines where a distribution package for a particular
	// provider version can be obtained.
	ProviderSource getproviders.Source

	// ModulePackageFetcher is the client to use when fetching module packages
	// from remote locations. This object effectively represents the policy
	// for how to fetch remote module packages, which is decided by the caller.
	//
	// Leaving this nil means that only local modules (using relative paths
	// in the source address) are supported, which is only reasonable for
	// unit testing.
	ModulePackageFetcher *getmodules.PackageFetcher

	// MakeRegistryHTTPClient is a function called each time a command needs
	// an HTTP client that will be used to make requests to a module or
	// provider registry.
	//
	// This is used by package main to deal with some operator-configurable
	// settings for retries and timeouts. If this isn't set then a new client
	// with reasonable defaults for tests will be used instead.
	MakeRegistryHTTPClient func() *retryablehttp.Client

	// BrowserLauncher is used by commands that need to open a URL in a
	// web browser.
	BrowserLauncher webbrowser.Launcher

	// A context.Context provided by the caller -- typically "package main" --
	// which might be carrying telemetry-related metadata and so should be
	// used when creating downstream traces, etc.
	//
	// This isn't guaranteed to be set, so use [Meta.CommandContext] to
	// safely create a context for the entire execution of a command, which
	// will be connected to this parent context if it's present.
	CallerContext context.Context

	// When this channel is closed, the command will be cancelled.
	ShutdownCh <-chan struct{}

	// ProviderDevOverrides are providers where we ignore the lock file, the
	// configured version constraints, and the local cache directory and just
	// always use exactly the path specified. This is intended to allow
	// provider developers to easily test local builds without worrying about
	// what version number they might eventually be released as, or what
	// checksums they have.
	ProviderDevOverrides map[addrs.Provider]getproviders.PackageLocalDir

	// UnmanagedProviders are a set of providers that exist as processes
	// predating OpenTofu, which OpenTofu should use but not worry about the
	// lifecycle of.
	//
	// This is essentially a more extreme version of ProviderDevOverrides where
	// OpenTofu doesn't even worry about how the provider server gets launched,
	// just trusting that someone else did it before running OpenTofu.
	UnmanagedProviders map[addrs.Provider]*plugin.ReattachConfig

	// AllowExperimentalFeatures controls whether a command that embeds this
	// Meta is permitted to make use of experimental OpenTofu features.
	//
	// Set this field only during the initial creation of Meta. If you change
	// this field after calling methods of type Meta then the resulting
	// behavior is undefined.
	//
	// In normal code this would be set by package main only in builds
	// explicitly marked as being alpha releases or development snapshots,
	// making experimental features unavailable otherwise. Test code may
	// choose to set this if it needs to exercise experimental features.
	//
	// Some experiments predated the addition of this setting, and may
	// therefore still be available even if this flag is false. Our intent
	// is that all/most _future_ experiments will be unavailable unless this
	// flag is set, to reinforce that experiments are not for production use.
	AllowExperimentalFeatures bool

	// ----------------------------------------------------------
	// Protected: commands can set these
	// ----------------------------------------------------------

	// pluginPath is a user defined set of directories to look for plugins.
	// This is set during init with the `-plugin-dir` flag, saved to a file in
	// the data directory.
	// This overrides all other search paths when discovering plugins.
	pluginPath []string

	// Override certain behavior for tests within this package
	testingOverrides *testingOverrides

	// ----------------------------------------------------------
	// Private: do not set these
	// ----------------------------------------------------------

	// configLoader is a shared configuration loader that is used by
	// LoadConfig and other commands that access configuration files.
	// It is initialized on first use.
	configLoader *configload.Loader

	// backendState is the currently active backend state
	backendState *clistate.BackendState

	// Variables for the context (private)
	variableArgs []flags.RawFlag
	input        bool

	// The fields below are expected to be set by the command via
	// command line flags. See the Apply command for an example.
	//
	// statePath is the path to the state file. If this is empty, then
	// no state will be loaded. It is also okay for this to be a path to
	// a file that doesn't exist; it is assumed that this means that there
	// is simply no state.
	//
	// stateOutPath is used to override the output path for the state.
	// If not provided, the StatePath is used causing the old state to
	// be overridden.
	//
	// backupPath is used to backup the state file before writing a modified
	// version. It defaults to stateOutPath + DefaultBackupExtension
	//
	// parallelism is used to control the number of concurrent operations
	// allowed when walking the graph
	//
	// provider is to specify specific resource providers
	//
	// stateLock is set to false to disable state locking
	//
	// stateLockTimeout is the optional duration to retry a state locks locks
	// when it is already locked by another process.
	//
	// forceInitCopy suppresses confirmation for copying state data during
	// init.
	//
	// reconfigure forces init to ignore any stored configuration.
	//
	// migrateState confirms the user wishes to migrate from the prior backend
	// configuration to a new configuration.
	statePath        string
	stateOutPath     string
	backupPath       string
	parallelism      int
	stateLock        bool
	stateLockTimeout time.Duration
	forceInitCopy    bool
	reconfigure      bool
	migrateState     bool

	// Used with commands which write state to allow users to write remote
	// state even if the remote and local OpenTofu versions don't match.
	ignoreRemoteVersion bool

	// Used to cache the root module rootModuleCallCache and known variables.
	// This helps prevent duplicate errors/warnings.
	rootModuleCallCache *configs.StaticModuleCall
	inputVariableCache  map[string]backend.UnparsedVariableValue

	// Since `tofu providers lock` and `tofu providers mirror` have their own
	// logic to create the source to fetch providers through, we had to
	// plumb this configuration through the [Meta] type to reach that part too.
	// In any other cases, this configuration is built and used directly in `realMain`
	// when the providers sources are built.
	ProviderSourceLocationConfig getproviders.LocationConfig
}

type testingOverrides struct {
	Providers    map[addrs.Provider]providers.Factory
	Provisioners map[string]provisioners.Factory
}

// initStatePaths is used to initialize the default values for
// statePath, stateOutPath, and backupPath
func (m *Meta) initStatePaths() {
	if m.statePath == "" {
		m.statePath = arguments.DefaultStateFilename
	}
	if m.stateOutPath == "" {
		m.stateOutPath = m.statePath
	}
	if m.backupPath == "" {
		m.backupPath = m.stateOutPath + DefaultBackupExtension
	}
}

// StateOutPath returns the true output path for the state file
func (m *Meta) StateOutPath() string {
	return m.stateOutPath
}

const (
	// InputModeEnvVar is the environment variable that, if set to "false" or
	// "0", causes tofu commands to behave as if the `-input=false` flag was
	// specified.
	InputModeEnvVar = "TF_INPUT"
)

// InputMode returns the type of input we should ask for in the form of
// tofu.InputMode which is passed directly to Context.Input.
func (m *Meta) InputMode() tofu.InputMode {
	if test || !m.input {
		return 0
	}

	if envVar := os.Getenv(InputModeEnvVar); envVar != "" {
		if v, err := strconv.ParseBool(envVar); err == nil {
			if !v {
				return 0
			}
		}
	}

	var mode tofu.InputMode
	mode |= tofu.InputModeProvider

	return mode
}

// UIInput returns a UIInput object to be used for asking for input.
func (m *Meta) UIInput() tofu.UIInput {
	return &UIInput{
		Colorize: m.View.Colorize(),
	}
}

// InterruptibleContext returns a context.Context that will be cancelled
// if the process is interrupted by a platform-specific interrupt signal.
//
// The typical way to use this is to pass the result of [Meta.CommandContext]
// as the base context, but that's appropriate only if the interruptible
// context is being created directly inside the "Run" method of a particular
// command, to create a context representing the entire remaining runtime of
// that command:
//
// As usual with cancelable contexts, the caller must always call the given
// cancel function once all operations are complete in order to make sure
// that the context resources will still be freed even if there is no
// interruption.
//
//	// This example is only for when using this function very early in
//	// the "Run" method of a Command implementation. If you already have
//	// an active context, pass that in as base instead.
//	ctx, done := c.InterruptibleContext(c.CommandContext())
//	defer done()
func (m *Meta) InterruptibleContext(base context.Context) (context.Context, context.CancelFunc) {
	if m.ShutdownCh == nil {
		// If we're running in a unit testing context without a shutdown
		// channel populated then we'll return an uncancelable channel.
		return base, func() {}
	}

	ctx, cancel := context.WithCancel(base)
	go func() {
		select {
		case <-m.ShutdownCh:
			cancel()
		case <-ctx.Done():
			// finished without being interrupted
		}
	}()
	return ctx, cancel
}

// CommandContext returns the "root context" to use in the main Run function
// of a command.
//
// This method is just a substitute for passing a context directly to the
// "Run" method of a command, which we can't do because that API is owned by
// mitchellh/cli rather than by OpenTofu. Use this only in situations
// comparable to the context having been passed in as an argument to Run.
//
// If the caller (e.g. "package main") provided a context when it instantiated
// the Meta then the returned context will inherit all of its values, deadlines,
// etc. If the caller did not provide a context then the result is an inert
// background context ready to be passed to other functions.
func (m *Meta) CommandContext() context.Context {
	if m.CallerContext == nil {
		return context.Background()
	}
	// We just return the caller context directly for now, since we don't
	// have anything to add to it.
	return m.CallerContext
}

// RunOperation executes the given operation on the given backend, blocking
// until that operation completes or is interrupted, and then returns
// the RunningOperation object representing the completed or
// aborted operation that is, despite the name, no longer running.
//
// An error is returned if the operation either fails to start or is cancelled.
// If the operation runs to completion then no error is returned even if the
// operation itself is unsuccessful. Use the "Result" field of the
// returned operation object to recognize operation-level failure.
func (m *Meta) RunOperation(ctx context.Context, b backend.Enhanced, opReq *backend.Operation) (*backend.RunningOperation, tfdiags.Diagnostics) {
	if opReq.View == nil {
		panic("RunOperation called with nil View")
	}
	if opReq.ConfigDir != "" {
		opReq.ConfigDir = m.WorkingDir.NormalizePath(opReq.ConfigDir)
	}

	// Inject variables and root module call
	var diags, callDiags tfdiags.Diagnostics
	opReq.Variables, diags = m.collectVariableValues()
	opReq.RootCall, callDiags = m.rootModuleCall(ctx, opReq.ConfigDir)
	diags = diags.Append(callDiags)
	if diags.HasErrors() {
		return nil, diags
	}

	op, err := b.Operation(ctx, opReq)
	if err != nil {
		return nil, diags.Append(fmt.Errorf("error starting operation: %w", err))
	}

	// Wait for the operation to complete or an interrupt to occur
	select {
	case <-m.ShutdownCh:
		// gracefully stop the operation
		op.Stop()

		// Notify the user
		opReq.View.Interrupted()

		// Still get the result, since there is still one
		select {
		case <-m.ShutdownCh:
			opReq.View.FatalInterrupt()

			// cancel the operation completely
			op.Cancel()

			// the operation should return asap
			// but timeout just in case
			select {
			case <-op.Done():
			case <-time.After(5 * time.Second):
			}

			return nil, diags.Append(errors.New("operation canceled"))

		case <-op.Done():
			// operation completed after Stop
		}
	case <-op.Done():
		// operation completed normally
	}

	return op, diags
}

// contextOpts returns the options to use to initialize a OpenTofu
// context with the settings from this Meta.
func (m *Meta) contextOpts(ctx context.Context) (*tofu.ContextOpts, error) {
	workspace, err := m.Workspace(ctx)
	if err != nil {
		return nil, err
	}

	var opts tofu.ContextOpts

	opts.UIInput = m.UIInput()
	opts.Parallelism = m.parallelism

	// If testingOverrides are set, we'll skip the plugin discovery process
	// and just work with what we've been given, thus allowing the tests
	// to provide mock providers and provisioners.
	if m.testingOverrides != nil {
		opts.Plugins = plugins.NewLibrary(
			m.testingOverrides.Providers,
			m.testingOverrides.Provisioners,
		)
	} else {
		var providerFactories map[addrs.Provider]providers.Factory
		providerFactories, err = m.providerFactories()
		opts.Plugins = plugins.NewLibrary(
			providerFactories,
			m.provisionerFactories(),
		)
	}

	opts.Meta = &tofu.ContextMeta{
		Env:                workspace,
		OriginalWorkingDir: m.WorkingDir.OriginalWorkingDir(),
	}

	return &opts, err
}

// confirm asks a yes/no confirmation.
func (m *Meta) confirm(opts *tofu.InputOpts) (bool, error) {
	if !m.Input() {
		return false, errors.New("input is disabled")
	}

	for range 2 {
		v, err := m.UIInput().Input(context.Background(), opts)
		if err != nil {
			return false, fmt.Errorf(
				"Error asking for confirmation: %w", err)
		}

		switch strings.ToLower(v) {
		case "no":
			return false, nil
		case "yes":
			return true, nil
		}
	}
	return false, nil
}

// WorkspaceNameEnvVar is the name of the environment variable that can be used
// to set the name of the OpenTofu workspace, overriding the workspace chosen
// by `tofu workspace select`.
//
// Note that this environment variable is ignored by `tofu workspace new`
// and `tofu workspace delete`.
const WorkspaceNameEnvVar = "TF_WORKSPACE"

var errInvalidWorkspaceNameEnvVar = fmt.Errorf("Invalid workspace name set using %s", WorkspaceNameEnvVar)

// Workspace returns the name of the currently configured workspace, corresponding
// to the desired named state.
func (m *Meta) Workspace(ctx context.Context) (string, error) {
	current, overridden := m.WorkspaceOverridden(ctx)
	if overridden && !validWorkspaceName(current) {
		return "", errInvalidWorkspaceNameEnvVar
	}
	return current, nil
}

// WorkspaceOverridden returns the name of the currently configured workspace,
// corresponding to the desired named state, as well as a bool saying whether
// this was set via the TF_WORKSPACE environment variable.
func (m *Meta) WorkspaceOverridden(_ context.Context) (string, bool) {
	if envVar := os.Getenv(WorkspaceNameEnvVar); envVar != "" {
		return envVar, true
	}

	envData, err := os.ReadFile(filepath.Join(m.WorkingDir.DataDir(), local.DefaultWorkspaceFile))
	current := string(bytes.TrimSpace(envData))
	if current == "" {
		current = backend.DefaultStateName
	}

	if err != nil && !os.IsNotExist(err) {
		// always return the default if we can't get a workspace name
		log.Printf("[ERROR] failed to read current workspace: %s", err)
	}

	return current, false
}

// SetWorkspace saves the given name as the current workspace in the local
// filesystem.
func (m *Meta) SetWorkspace(name string) error {
	err := os.MkdirAll(m.WorkingDir.DataDir(), 0755)
	if err != nil {
		return err
	}

	err = os.WriteFile(filepath.Join(m.WorkingDir.DataDir(), local.DefaultWorkspaceFile), []byte(name), 0644)
	if err != nil {
		return err
	}
	return nil
}

// isAutoVarFile determines if the file ends with .auto.tfvars or .auto.tfvars.json
func isAutoVarFile(path string) bool {
	return strings.HasSuffix(path, ".auto.tfvars") ||
		strings.HasSuffix(path, ".auto.tfvars.json")
}

// FIXME: as an interim refactoring step, we apply the contents of the state
// arguments directly to the Meta object. Future work would ideally update the
// code paths which use these arguments to be passed them directly for clarity.
func (m *Meta) applyStateArguments(args *arguments.State) {
	m.stateLock = args.Lock
	m.stateLockTimeout = args.LockTimeout
	m.statePath = args.StatePath
	m.stateOutPath = args.StateOutPath
	m.backupPath = args.BackupPath
}

// checkRequiredVersion loads the config and check if the
// core version requirements are satisfied.
func (m *Meta) checkRequiredVersion(ctx context.Context) tfdiags.Diagnostics {
	var diags tfdiags.Diagnostics

	loader, err := m.initConfigLoader()
	if err != nil {
		diags = diags.Append(err)
		return diags
	}

	pwd, err := os.Getwd()
	if err != nil {
		diags = diags.Append(fmt.Errorf("Error getting pwd: %w", err))
		return diags
	}

	call, callDiags := m.rootModuleCall(ctx, pwd)
	if callDiags.HasErrors() {
		diags = diags.Append(callDiags)
		return diags
	}

	_, configDiags := loader.LoadConfig(ctx, pwd, call)
	if configDiags.HasErrors() {
		diags = diags.Append(configDiags)
		return diags
	}

	// If there were any OpenTofu-version-related errors then they would've
	// already been detected by loader.LoadConfig above.
	return nil
}

// MaybeGetSchemas attempts to load and return the schemas
// If there is not enough information to return the schemas,
// it could potentially return nil without errors. It is the
// responsibility of the caller to handle the lack of schema
// information accordingly
func (c *Meta) MaybeGetSchemas(ctx context.Context, state *states.State, config *configs.Config) (*tofu.Schemas, tfdiags.Diagnostics) {
	var diags tfdiags.Diagnostics

	path, err := os.Getwd()
	if err != nil {
		diags.Append(tfdiags.SimpleWarning(failedToLoadSchemasMessage))
		return nil, diags
	}

	if config == nil {
		config, diags = c.loadConfig(ctx, path)
		if diags.HasErrors() {
			diags.Append(tfdiags.SimpleWarning(failedToLoadSchemasMessage))
			return nil, diags
		}
	}

	if config != nil || state != nil {
		opts, err := c.contextOpts(ctx)
		if err != nil {
			diags = diags.Append(err)
			return nil, diags
		}
		tfCtx, ctxDiags := tofu.NewContext(opts)
		diags = diags.Append(ctxDiags)
		if ctxDiags.HasErrors() {
			return nil, diags
		}
		var schemaDiags tfdiags.Diagnostics
		schemas, schemaDiags := tfCtx.Schemas(ctx, config, state)
		diags = diags.Append(schemaDiags)
		if schemaDiags.HasErrors() {
			return nil, diags
		}
		return schemas, diags

	}
	return nil, diags
}
