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

package command

import (
	"context"
	"errors"
	"fmt"
	"strings"

	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/terraform/internal/addrs"
	"github.com/hashicorp/terraform/internal/backend"
	"github.com/hashicorp/terraform/internal/cloud"
	"github.com/hashicorp/terraform/internal/command/arguments"
	"github.com/hashicorp/terraform/internal/command/views"
	"github.com/hashicorp/terraform/internal/configs"
	"github.com/hashicorp/terraform/internal/depsfile"
	"github.com/hashicorp/terraform/internal/getproviders"
	"github.com/hashicorp/terraform/internal/states"
	"github.com/hashicorp/terraform/internal/terraform"
	"github.com/hashicorp/terraform/internal/tfdiags"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/codes"
	"go.opentelemetry.io/otel/trace"
)

func (c *InitCommand) run(initArgs *arguments.Init, view views.Init) int {
	var diags tfdiags.Diagnostics

	c.forceInitCopy = initArgs.ForceInitCopy
	c.Meta.stateLock = initArgs.StateLock
	c.Meta.stateLockTimeout = initArgs.StateLockTimeout
	c.reconfigure = initArgs.Reconfigure
	c.migrateState = initArgs.MigrateState
	c.Meta.ignoreRemoteVersion = initArgs.IgnoreRemoteVersion
	c.Meta.input = initArgs.InputEnabled
	c.Meta.targetFlags = initArgs.TargetFlags
	c.Meta.compactWarnings = initArgs.CompactWarnings

	// Copying the state only happens during backend migration, so setting
	// -force-copy implies -migrate-state
	if c.forceInitCopy {
		c.migrateState = true
	}

	if len(initArgs.PluginPath) > 0 {
		c.pluginPath = initArgs.PluginPath
	}

	// Validate the arg count and get the working directory
	path, err := ModulePath(initArgs.Args)
	if err != nil {
		diags = diags.Append(err)
		view.Diagnostics(diags)
		return 1
	}

	if err := c.storePluginPath(c.pluginPath); err != nil {
		diags = diags.Append(fmt.Errorf("Error saving -plugin-dir to workspace directory: %s", err))
		view.Diagnostics(diags)
		return 1
	}

	// Initialization can be aborted by interruption signals
	ctx, done := c.InterruptibleContext(c.CommandContext())
	defer done()

	// This will track whether we outputted anything so that we know whether
	// to output a newline before the success message
	var header bool

	if initArgs.FromModule != "" {
		src := initArgs.FromModule

		empty, err := configs.IsEmptyDir(path, initArgs.TestsDirectory)
		if err != nil {
			diags = diags.Append(fmt.Errorf("Error validating destination directory: %s", err))
			view.Diagnostics(diags)
			return 1
		}
		if !empty {
			diags = diags.Append(errors.New(strings.TrimSpace(errInitCopyNotEmpty)))
			view.Diagnostics(diags)
			return 1
		}

		view.Output(views.CopyingConfigurationMessage, src)
		header = true

		hooks := uiModuleInstallHooks{
			Ui:             c.Ui,
			ShowLocalPaths: false, // since they are in a weird location for init
			View:           view,
		}

		ctx, span := tracer.Start(ctx, "-from-module=...", trace.WithAttributes(
			attribute.String("module_source", src),
		))

		initDirFromModuleAbort, initDirFromModuleDiags := c.initDirFromModule(ctx, path, src, hooks)
		diags = diags.Append(initDirFromModuleDiags)
		if initDirFromModuleAbort || initDirFromModuleDiags.HasErrors() {
			view.Diagnostics(diags)
			span.SetStatus(codes.Error, "module installation failed")
			span.End()
			return 1
		}
		span.End()

		view.Output(views.EmptyMessage)
	}

	// If our directory is empty, then we're done. We can't get or set up
	// the backend with an empty directory.
	empty, err := configs.IsEmptyDir(path, initArgs.TestsDirectory)
	if err != nil {
		diags = diags.Append(fmt.Errorf("Error checking configuration: %s", err))
		view.Diagnostics(diags)
		return 1
	}
	if empty {
		view.Output(views.OutputInitEmptyMessage)
		return 0
	}

	// Load just the root module to begin backend and module initialization
	rootModEarly, earlyConfDiags := c.loadSingleModuleWithTests(path, initArgs.TestsDirectory)

	// There may be parsing errors in config loading but these will be shown later _after_
	// checking for core version requirement errors. Not meeting the version requirement should
	// be the first error displayed if that is an issue, but other operations are required
	// before being able to check core version requirements.
	if rootModEarly == nil {
		diags = diags.Append(errors.New(view.PrepareMessage(views.InitConfigError)), earlyConfDiags)
		view.Diagnostics(diags)

		return 1
	}
	if !(c.Meta.AllowExperimentalFeatures && initArgs.EnablePssExperiment) && rootModEarly.StateStore != nil {
		// TODO(SarahFrench/radeksimko) - remove when this feature isn't experimental.
		// This approach for making the feature experimental is required
		// to let us assert the feature is gated behind an experiment in tests.
		// See https://github.com/hashicorp/terraform/pull/37350#issuecomment-3168555619

		detail := "Pluggable state store is an experiment which requires"
		if !c.Meta.AllowExperimentalFeatures {
			detail += " an experimental build of terraform"
		}
		if !initArgs.EnablePssExperiment {
			if !c.Meta.AllowExperimentalFeatures {
				detail += " and"
			}
			detail += " -enable-pluggable-state-storage-experiment flag"
		}

		diags = diags.Append(earlyConfDiags)
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Pluggable state store experiment not supported",
			Detail:   detail,
			Subject:  &rootModEarly.StateStore.TypeRange,
		})
		view.Diagnostics(diags)

		return 1
	}

	if rootModEarly.StateStore != nil { // We know rootModEarly is not nil.
		rootModEarly.StateStore.ProviderSupplyMode = c.Meta.getProviderSupplyModeForStateStore(rootModEarly)
		if rootModEarly.StateStore.ProviderSupplyMode == getproviders.Unset {
			panic("unset provider supply mode for state store")
		}
	}

	if initArgs.Get {
		modsOutput, modsAbort, modsDiags := c.getModules(ctx, path, initArgs.TestsDirectory, rootModEarly, initArgs.Upgrade, view)
		diags = diags.Append(modsDiags)
		if modsAbort || modsDiags.HasErrors() {
			view.Diagnostics(diags)
			return 1
		}
		if modsOutput {
			header = true
		}
	}

	// With all of the modules (hopefully) installed, we can now try to load the
	// whole configuration tree.
	config, confDiags := c.loadConfigWithTests(path, initArgs.TestsDirectory)
	if config != nil && config.Module != nil && config.Module.StateStore != nil {
		config.Module.StateStore.ProviderSupplyMode = c.Meta.getProviderSupplyModeForStateStore(config.Module)
		if config.Module.StateStore.ProviderSupplyMode == getproviders.Unset {
			panic("unset provider supply mode for state store")
		}
	}
	// configDiags will be handled after:
	// - the version constraint check has happened
	// - and, the backend/state_store is initialised

	// Before we go further, we'll check to make sure none of the modules in
	// the configuration declare that they don't support this Terraform
	// version, so we can produce a version-related error message rather than
	// potentially-confusing downstream errors.
	versionDiags := terraform.CheckCoreVersionRequirements(config)
	if versionDiags.HasErrors() {
		view.Diagnostics(versionDiags)
		return 1
	}

	earlyBdiags := c.earlyValidateBackend(rootModEarly, initArgs)
	diags = diags.Append(earlyBdiags)

	// We've passed the core version check, now we can show errors from the early configuration.
	// This prevents trying to initialise the backend with faulty configuration.
	if earlyConfDiags.HasErrors() || earlyBdiags.HasErrors() {
		diags = diags.Append(errors.New(view.PrepareMessage(views.InitConfigError)), earlyConfDiags)
		view.Diagnostics(diags)
		return 1
	}

	// Now the full configuration is loaded, we can download the providers specified in the configuration.
	// This is step one of a two-step provider download process
	// Providers may be downloaded by this code, but the dependency lock file is only updated later in `init`
	// after step two of provider download is complete.
	previousLocks, moreDiags := c.lockedDependencies()
	diags = diags.Append(moreDiags)

	// If -state-provider-lock-file is set, we'll use that to obtain a new lock used for the state store provider
	// This will be 'upserted': it may be that the previous locks don't contain the provider being added. potentially due to being empty, or contain a different version.
	// The lock added will be used in the first step of provider download.
	//
	// We leave `previousLocks` unchanged so it can be used to accurately detect changes to the locks when the lock file is updated later.
	alteredPreviousLocks := previousLocks.DeepCopy()
	if initArgs.StateStoreProviderLockFile != "" {
		stateStoreLocks, lockDiags := depsfile.LoadLocksFromFile(initArgs.StateStoreProviderLockFile)
		if lockDiags.HasErrors() {
			diags = diags.Append(tfdiags.Sourceless(
				tfdiags.Error,
				"Error loading -state-provider-lock-file lock file",
				fmt.Sprintf("Terraform experienced an error loading the file at %q: %s", initArgs.StateStoreProviderLockFile, lockDiags.Err()),
			))
			view.Diagnostics(diags)
			return 1
		}
		diags = diags.Append(lockDiags) // capture any warnings

		lock := stateStoreLocks.Provider(config.Module.StateStore.ProviderAddr)
		if lock == nil {
			diags = diags.Append(tfdiags.Sourceless(
				tfdiags.Error,
				"State store provider not found in -state-provider-lock-file dependency lock file",
				fmt.Sprintf("Terraform could not find the state store provider %q (%s) in the dependency lock file %q provided via the -state-provider-lock-file flag. Please ensure the lock file contains a lock for the state store provider and try again.",
					config.Module.StateStore.ProviderAddr.Type,
					config.Module.StateStore.ProviderAddr.ForDisplay(),
					initArgs.StateStoreProviderLockFile,
				),
			))
			view.Diagnostics(diags)
			return 1
		}

		// Overwrite or add the state store provider lock to the other locks for this project
		alteredPreviousLocks.SetProvider(
			lock.Provider(),
			lock.Version(),
			lock.VersionConstraints(),
			lock.PreferredHashes(),
		)
	}

	var pssLocks *depsfile.Locks
	if config != nil && config.Module != nil && config.Module.StateStore != nil {
		var configProvidersOutput bool
		var safeInitAction SafeInitAction
		var stateStoreProviderAuthResult *getproviders.PackageAuthenticationResult
		var configProviderDiags tfdiags.Diagnostics

		// The init command is not allowed to upgrade the provider used for state storage
		// We warn that upgrades will not impact the provider, and upgrades will only work via `terraform state migrate -upgrade`.
		var allowUpgrade bool
		if initArgs.Upgrade {
			if initArgs.Reconfigure {
				allowUpgrade = true // user is opting out of migrating state; whatever happens, happens
			} else {
				allowUpgrade = false // the installer will only be able to reuse the old version.
				diags = diags.Append(tfdiags.Sourceless(
					tfdiags.Warning,
					"Cannot upgrade the provider used for state storage during \"terraform init -upgrade\"",
					fmt.Sprintf(`Terraform will not upgrade the %s (%q) provider as part of this operation because it is used for state storage.

Please use \"terraform state migrate -upgrade\" to upgrade the state store provider and navigate migrating your state between the two versions.`,
						config.Module.StateStore.ProviderAddr.Type,
						config.Module.StateStore.ProviderAddr.ForDisplay(),
					),
				),
				)
			}
		}

		configProvidersOutput, pssLocks, safeInitAction, stateStoreProviderAuthResult, configProviderDiags = c.getProvidersFromPSSConfig(ctx, config, alteredPreviousLocks, allowUpgrade, initArgs.PluginPath, initArgs.Lockfile, view)
		diags = diags.Append(configProviderDiags)
		if configProviderDiags.HasErrors() {
			view.Diagnostics(diags)
			return 1
		}
		if configProvidersOutput {
			header = true
		}

		// Course of action depends on the safeInitAction returned from getProvidersFromPSSConfig
		switch safeInitAction {
		case SafeInitActionProceed:
			// do nothing; provider is already trusted and there's no need to notify the user.
		case SafeInitActionRequireApproval:
			if c.input {
				// Prompt the user about trusting the provider used for state storage.
				diags = diags.Append(c.promptStateStorageProviderApproval(config.Module.StateStore.ProviderAddr, pssLocks, stateStoreProviderAuthResult))
				if diags.HasErrors() {
					view.Output(views.StateStoreProviderInteractiveRejectedMessage)
					view.Diagnostics(diags)
					return 1
				}
				view.Output(views.StateStoreProviderInteractiveApprovedMessage)
			} else {
				// Confirm that a lock was used to control download.
				// Note: we have to wait and do that here because at this point we know the provider was downloaded from a source that requires additional info about trust.
				if alteredPreviousLocks.Provider(config.Module.StateStore.ProviderAddr) == nil {
					// No lock was provided for the state store provider either through pre-existing locks or through the -state-provider-lock-file flag.
					diags = diags.Append(tfdiags.Sourceless(
						tfdiags.Error,
						"Missing lock for state store provider",
						"Terraform is initializing a state store for the first time in a non-interactive mode. In this scenario Terraform needs a pre-existing dependency lock for the state store provider to be present in the working directory's dependency lock file, or present in another file supplied via the -state-provider-lock-file flag. No lock was found for the state store provider. Please re-run the command using the -state-provider-lock-file flag.",
					))
					view.Diagnostics(diags)
					return 1
				}
				view.Output(views.StateStoreProviderAutomationApprovedMessage)
			}
		default:
			// Handle SafeInitActionInvalid or unexpected action types
			panic(fmt.Sprintf("When installing providers described in the config Terraform couldn't determine what 'safe init' action should be taken and returned action type %T. This is a bug in Terraform and should be reported.", safeInitAction))
		}
	}

	// If we outputted information, then we need to output a newline
	// so that our success message is nicely spaced out from prior text.
	if header {
		view.Output(views.EmptyMessage)
	}

	var back backend.Backend

	var backDiags tfdiags.Diagnostics
	var backendOutput bool
	switch {
	case initArgs.Cloud && rootModEarly.CloudConfig != nil:
		back, backendOutput, backDiags = c.initCloud(ctx, rootModEarly, initArgs.BackendConfig, initArgs.ViewType, view)
	case initArgs.Backend:
		back, backendOutput, backDiags = c.initBackend(ctx, rootModEarly, initArgs, pssLocks, view)
	default:
		// load the previously-stored backend config
		back, backDiags = c.Meta.backendFromState(ctx)
	}
	if backendOutput {
		header = true
	}
	if header {
		// If we outputted information, then we need to output a newline
		// so that our success message is nicely spaced out from prior text.
		view.Output(views.EmptyMessage)
	}

	// Show any errors from initializing the backend.
	// No preamble using `InitConfigError` is present, as we expect
	// any errors to from configuring the backend itself.
	diags = diags.Append(backDiags)
	if backDiags.HasErrors() {
		view.Diagnostics(diags)
		return 1
	}

	// If everything is ok with the core version check and backend/state_store initialization,
	// show other errors from loading the full configuration tree.
	diags = diags.Append(confDiags)
	if confDiags.HasErrors() {
		diags = diags.Append(errors.New(view.PrepareMessage(views.InitConfigError)))
		view.Diagnostics(diags)
		return 1
	}

	var state *states.State

	// If we have a functional backend (either just initialized or initialized
	// on a previous run) we'll use the current state as a potential source
	// of provider dependencies.
	if back != nil {
		c.ignoreRemoteVersionConflict(back)
		workspace, err := c.Workspace()
		if err != nil {
			diags = diags.Append(fmt.Errorf("Error selecting workspace: %s", err))
			view.Diagnostics(diags)
			return 1
		}
		sMgr, sDiags := back.StateMgr(workspace)
		if sDiags.HasErrors() {
			diags = diags.Append(fmt.Errorf("Error loading state: %s", sDiags.Err()))
			view.Diagnostics(diags)
			return 1
		}

		if err := sMgr.RefreshState(); err != nil {
			diags = diags.Append(fmt.Errorf("Error refreshing state: %s", err))
			view.Diagnostics(diags)
			return 1
		}

		state = sMgr.State()
	}

	stateProvidersOutput, stateLocks, stateProvidersDiags := c.getProviders(ctx, config, state, initArgs.Upgrade, pssLocks, initArgs.PluginPath, view)
	diags = diags.Append(stateProvidersDiags)
	if stateProvidersDiags.HasErrors() {
		view.Diagnostics(diags)
		return 1
	}
	if stateProvidersOutput {
		header = true
	}
	if header {
		// If we outputted information, then we need to output a newline
		// so that our success message is nicely spaced out from prior text.
		view.Output(views.EmptyMessage)
	}

	// Now the two steps of provider download have happened, update the dependency lock file if it has changed.
	lockFileOutput, lockFileDiags := c.saveDependencyLockFile(previousLocks, pssLocks, stateLocks, initArgs.Lockfile, view)
	diags = diags.Append(lockFileDiags)
	if lockFileDiags.HasErrors() {
		view.Diagnostics(diags)
		return 1
	}
	if lockFileOutput {
		header = true
	}
	if header {
		// If we outputted information, then we need to output a newline
		// so that our success message is nicely spaced out from prior text.
		view.Output(views.EmptyMessage)
	}

	if cb, ok := back.(*cloud.Cloud); ok {
		if c.RunningInAutomation {
			if err := cb.AssertImportCompatible(config); err != nil {
				diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Compatibility error", err.Error()))
				view.Diagnostics(diags)
				return 1
			}
		}
	}

	// If we accumulated any warnings along the way that weren't accompanied
	// by errors then we'll output them here so that the success message is
	// still the final thing shown.
	view.Diagnostics(diags)
	_, cloud := back.(*cloud.Cloud)
	output := views.OutputInitSuccessMessage
	if cloud {
		output = views.OutputInitSuccessCloudMessage
	}

	view.Output(output)

	if !c.RunningInAutomation {
		// If we're not running in an automation wrapper, give the user
		// some more detailed next steps that are appropriate for interactive
		// shell usage.
		output = views.OutputInitSuccessCLIMessage
		if cloud {
			output = views.OutputInitSuccessCLICloudMessage
		}
		view.Output(output)
	}
	return 0
}

// promptStateStorageProviderApproval is used when Terraform is unsure about the safety of the provider downloaded for state storage
// purposes, and we need to prompt the user to approve or reject using it.
func (c *InitCommand) promptStateStorageProviderApproval(stateStorageProvider addrs.Provider, configLocks *depsfile.Locks, authResult *getproviders.PackageAuthenticationResult) tfdiags.Diagnostics {
	var diags tfdiags.Diagnostics

	// If we can receive input then we prompt for ok from the user
	lock := configLocks.Provider(stateStorageProvider)

	var hashList strings.Builder
	for _, hash := range lock.PreferredHashes() {
		hashList.WriteString(fmt.Sprintf("- %s\n", hash))
	}

	var authentication string
	if authResult != nil && authResult.KeyID != "" {
		authentication = fmt.Sprintf("%s, key ID %s", authResult.String(), authResult.KeyID)
	} else {
		authentication = authResult.String()
	}

	v, err := c.UIInput().Input(context.Background(), &terraform.InputOpts{
		Id: "approve",
		Query: fmt.Sprintf(`Do you want to use provider %q (%s), version %s, for managing state?
Platform: %s
Authentication: %s
Hashes:
%s
`,
			lock.Provider().Type,
			lock.Provider(),
			lock.Version(),
			getproviders.CurrentPlatform.String(),
			authentication,
			hashList.String(),
		),
		Description: fmt.Sprintf(`Check the details above for provider %q and confirm that you trust the provider.
	Only 'yes' will be accepted to confirm.`, lock.Provider().Type),
	})
	if err != nil {
		return diags.Append(fmt.Errorf("Failed to approve use of state storage provider: %s", err))
	}
	if v != "yes" {
		return diags.Append(
			fmt.Errorf("State store provider %q (%s) was not approved, so init cannot continue.",
				lock.Provider().Type,
				lock.Provider(),
			),
		)
	}
	return diags
}
