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

package command

import (
	"context"
	"fmt"
	"log"
	"maps"
	"reflect"
	"slices"
	"sort"
	"strings"

	"github.com/hashicorp/hcl/v2"
	svchost "github.com/hashicorp/terraform-svchost"
	"github.com/posener/complete"
	"github.com/zclconf/go-cty/cty"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/trace"

	"github.com/hashicorp/terraform/internal/addrs"
	"github.com/hashicorp/terraform/internal/backend"
	backendInit "github.com/hashicorp/terraform/internal/backend/init"
	"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/configs/configschema"
	"github.com/hashicorp/terraform/internal/depsfile"
	"github.com/hashicorp/terraform/internal/didyoumean"
	"github.com/hashicorp/terraform/internal/getproviders"
	"github.com/hashicorp/terraform/internal/getproviders/providerreqs"
	"github.com/hashicorp/terraform/internal/providercache"
	"github.com/hashicorp/terraform/internal/states"
	"github.com/hashicorp/terraform/internal/tfdiags"
	tfversion "github.com/hashicorp/terraform/version"
)

// InitCommand is a Command implementation that takes a Terraform
// module and clones it to the working directory.
type InitCommand struct {
	Meta

	incompleteProviders []string
}

func (c *InitCommand) Run(args []string) int {
	var diags tfdiags.Diagnostics
	args = c.Meta.process(args)
	initArgs, initDiags := arguments.ParseInit(args, c.Meta.AllowExperimentalFeatures)
	diags = diags.Append(initDiags)

	view := views.NewInit(initArgs.ViewType, c.View)

	loader, err := c.initConfigLoader()
	if err != nil {
		diags = diags.Append(err)
		view.Diagnostics(diags)
		return 1
	}

	var varDiags tfdiags.Diagnostics
	c.VariableValues, varDiags = initArgs.Vars.CollectValues(func(filename string, src []byte) {
		loader.Parser().ForceFileSource(filename, src)
	})
	diags = diags.Append(varDiags)

	if diags.HasErrors() {
		view.Diagnostics(diags)
		return 1
	}

	return c.run(initArgs, view)
}

func (c *InitCommand) getModules(ctx context.Context, path, testsDir string, earlyRoot *configs.Module, upgrade bool, view views.Init) (output bool, abort bool, diags tfdiags.Diagnostics) {
	testModules := false // We can also have modules buried in test files.
	for _, file := range earlyRoot.Tests {
		for _, run := range file.Runs {
			if run.Module != nil {
				testModules = true
			}
		}
	}

	if len(earlyRoot.ModuleCalls) == 0 && !testModules {
		// Nothing to do
		return false, false, nil
	}

	ctx, span := tracer.Start(ctx, "install modules", trace.WithAttributes(
		attribute.Bool("upgrade", upgrade),
	))
	defer span.End()

	if upgrade {
		view.Output(views.UpgradingModulesMessage)
	} else {
		view.Output(views.InitializingModulesMessage)
	}

	hooks := uiModuleInstallHooks{
		Ui:             c.Ui,
		ShowLocalPaths: true,
		View:           view,
	}

	installAbort, installDiags := c.installModules(ctx, path, testsDir, upgrade, false, hooks)
	diags = diags.Append(installDiags)

	// At this point, installModules may have generated error diags or been
	// aborted by SIGINT. In any case we continue and the manifest as best
	// we can.

	// Since module installer has modified the module manifest on disk, we need
	// to refresh the cache of it in the loader.
	if c.configLoader != nil {
		if err := c.configLoader.RefreshModules(); err != nil {
			// Should never happen
			diags = diags.Append(tfdiags.Sourceless(
				tfdiags.Error,
				"Failed to read module manifest",
				fmt.Sprintf("After installing modules, Terraform could not re-read the manifest of installed modules. This is a bug in Terraform. %s.", err),
			))
		}
	}

	return true, installAbort, diags
}

func (c *InitCommand) initCloud(ctx context.Context, root *configs.Module, extraConfig arguments.FlagNameValueSlice, viewType arguments.ViewType, view views.Init) (be backend.Backend, output bool, diags tfdiags.Diagnostics) {
	ctx, span := tracer.Start(ctx, "initialize HCP Terraform")
	_ = ctx // prevent staticcheck from complaining to avoid a maintenance hazard of having the wrong ctx in scope here
	defer span.End()

	view.Output(views.InitializingTerraformCloudMessage)

	if len(extraConfig.AllItems()) != 0 {
		diags = diags.Append(tfdiags.Sourceless(
			tfdiags.Error,
			"Invalid command-line option",
			"The -backend-config=... command line option is only for state backends, and is not applicable to HCP Terraform-based configurations.\n\nTo change the set of workspaces associated with this configuration, edit the Cloud configuration block in the root module.",
		))
		return nil, true, diags
	}

	backendConfig := root.CloudConfig.ToBackendConfig()

	opts := &BackendOpts{
		BackendConfig: &backendConfig,
		Init:          true,
		ViewType:      viewType,
	}

	back, backDiags := c.Backend(opts)
	diags = diags.Append(backDiags)
	return back, true, diags
}

func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, initArgs *arguments.Init, configLocks *depsfile.Locks, view views.Init) (be backend.Backend, output bool, diags tfdiags.Diagnostics) {
	ctx, span := tracer.Start(ctx, "initialize backend")
	_ = ctx // prevent staticcheck from complaining to avoid a maintenance hazard of having the wrong ctx in scope here
	defer span.End()

	if root.StateStore != nil {
		view.Output(views.InitializingStateStoreMessage, root.StateStore.Type)
	} else {
		view.Output(views.InitializingBackendMessage)
	}

	var opts *BackendOpts
	switch {
	case root.StateStore != nil:
		// state_store config present
		factory, fDiags := c.Meta.StateStoreProviderFactoryFromConfig(root.StateStore, configLocks)
		diags = diags.Append(fDiags)
		if fDiags.HasErrors() {
			return nil, true, diags
		}

		// If overrides supplied by -backend-config CLI flag, process them
		var configOverride hcl.Body
		if !initArgs.BackendConfig.Empty() {
			// We need to launch an instance of the provider to get the config of the state store for processing any overrides.
			provider, err := factory()
			defer provider.Close() // Stop the child process once we're done with it here.
			if err != nil {
				diags = diags.Append(fmt.Errorf("error when obtaining provider instance during state store initialization: %w", err))
				return nil, true, diags
			}

			resp := provider.GetProviderSchema()

			if len(resp.StateStores) == 0 {
				diags = diags.Append(&hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Provider does not support pluggable state storage",
					Detail: fmt.Sprintf("There are no state stores implemented by provider %s (%q)",
						root.StateStore.Provider.Name,
						root.StateStore.ProviderAddr),
					Subject: &root.StateStore.DeclRange,
				})
				return nil, true, diags
			}

			stateStoreSchema, exists := resp.StateStores[root.StateStore.Type]
			if !exists {
				suggestions := slices.Sorted(maps.Keys(resp.StateStores))
				suggestion := didyoumean.NameSuggestion(root.StateStore.Type, suggestions)
				if suggestion != "" {
					suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
				}
				diags = diags.Append(&hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "State store not implemented by the provider",
					Detail: fmt.Sprintf("State store %q is not implemented by provider %s (%q)%s",
						root.StateStore.Type, root.StateStore.Provider.Name,
						root.StateStore.ProviderAddr, suggestion),
					Subject: &root.StateStore.DeclRange,
				})
				return nil, true, diags
			}

			// Handle any overrides supplied via -backend-config CLI flags
			var overrideDiags tfdiags.Diagnostics
			configOverride, overrideDiags = c.backendConfigOverrideBody(initArgs.BackendConfig, stateStoreSchema.Body)
			diags = diags.Append(overrideDiags)
			if overrideDiags.HasErrors() {
				return nil, true, diags
			}
		}

		opts = &BackendOpts{
			StateStoreConfig: root.StateStore,
			Locks:            configLocks,
			ConfigOverride:   configOverride,
			Init:             true,
			ViewType:         initArgs.ViewType,
		}

	case root.Backend != nil:
		// backend config present
		backendType := root.Backend.Type
		bf := backendInit.Backend(backendType)
		b := bf()
		backendSchema := b.ConfigSchema()
		backendConfig := root.Backend

		// If overrides supplied by -backend-config CLI flag, process them
		var configOverride hcl.Body
		if !initArgs.BackendConfig.Empty() {
			var overrideDiags tfdiags.Diagnostics
			configOverride, overrideDiags = c.backendConfigOverrideBody(initArgs.BackendConfig, backendSchema)
			diags = diags.Append(overrideDiags)
			if overrideDiags.HasErrors() {
				return nil, true, diags
			}
		}

		opts = &BackendOpts{
			BackendConfig:  backendConfig,
			Locks:          configLocks,
			ConfigOverride: configOverride,
			Init:           true,
			ViewType:       initArgs.ViewType,
		}

	default:
		// No config; defaults to local state storage
		opts = &BackendOpts{
			Init:     true,
			Locks:    configLocks,
			ViewType: initArgs.ViewType,
		}
	}

	back, backDiags := c.Backend(opts)
	diags = diags.Append(backDiags)
	return back, true, diags
}

func (c *InitCommand) earlyValidateBackend(root *configs.Module, initArgs *arguments.Init) (diags tfdiags.Diagnostics) {
	switch {
	case root.StateStore != nil && root.Backend != nil:
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Conflicting backend and state_store configurations present during init",
			Detail: fmt.Sprintf("When initializing the backend there was configuration data present for both backend %q and state store %q. This is a bug in Terraform and should be reported.",
				root.Backend.Type,
				root.StateStore.Type,
			),
			Subject: &root.Backend.TypeRange,
		})
		return diags
	case root.StateStore != nil:
		// validation requires the provider to be installed so cannot be done early
	case root.Backend != nil:
		backendType := root.Backend.Type
		if backendType == "cloud" {
			diags = diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Unsupported backend type",
				Detail:   fmt.Sprintf("There is no explicit backend type named %q. To configure HCP Terraform, declare a 'cloud' block instead.", backendType),
				Subject:  &root.Backend.TypeRange,
			})
			return diags
		}

		if !backendInit.BackendExists(backendType) {
			detail := fmt.Sprintf("There is no backend type named %q.", backendType)
			if msg, removed := backendInit.RemovedBackends[backendType]; removed {
				detail = msg
			}

			diags = diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Unsupported backend type",
				Detail:   detail,
				Subject:  &root.Backend.TypeRange,
			})
			return diags
		}
	default:
		// No config; defaults to local state storage

		// If the user supplied a -backend-config on the CLI but no backend
		// block was found in the configuration, it's likely - but not
		// necessarily - a mistake. Return a warning.
		if !initArgs.BackendConfig.Empty() {
			diags = diags.Append(tfdiags.Sourceless(
				tfdiags.Warning,
				"Missing backend configuration",
				`-backend-config was used without a "backend" block in the configuration.

If you intended to override the default local backend configuration,
no action is required, but you may add an explicit backend block to your
configuration to clear this warning:

terraform {
  backend "local" {}
}

However, if you intended to override a defined backend, please verify that
the backend configuration is present and valid.
`,
			))
		}
	}
	return diags
}

// SafeInitAction describes the action that should be taken by Terraform based on whether
// pluggable state storage is in use, if the provider is going to be downloaded via HTTP or not,
// and whether Terraform is being run in automation or not.
type SafeInitAction rune

const (
	SafeInitActionInvalid         SafeInitAction = 0
	SafeInitActionProceed         SafeInitAction = 'P'
	SafeInitActionRequireApproval SafeInitAction = 'I'
)

// getProvidersFromPSSConfig determines what provider is required given state store configuration
// and downloads the provider that isn't already downloaded and then returns
// updated dependency lock data. The dependency lock file itself isn't updated here.
//
// Calling code is responsible for validating inputs to this method, e.g. mutually exclusive flags.
func (c *InitCommand) getProvidersFromPSSConfig(ctx context.Context, config *configs.Config, previousLocks *depsfile.Locks, upgrade bool, pluginDirs []string, flagLockfile string, view views.Init) (output bool, resultingLocks *depsfile.Locks, safeInitAction SafeInitAction, authResult *getproviders.PackageAuthenticationResult, diags tfdiags.Diagnostics) {
	ctx, span := tracer.Start(ctx, "install providers for state store")
	defer span.End()

	// Dev overrides and unmanaged providers change the installation process in "terraform init";
	// overridden and unmanaged providers are skipped during installation.
	// This means that impacted providers won't be downloaded from external sources nor added
	// to the dependency lock file if they aren't already recorded there. Similarly, any attempt
	// to upgrade providers will not affect providers impacted by overrides or unmanaged providers.
	//
	// So, we'll warn users about it to avoid later confusion when Terraform ends up using
	// a different provider than the lock file called for, or doesn't make expected changes
	// to the lock file.
	//
	// This warning is only shown once, here, to avoid duplication; not raised in getProvidersFromState.
	diags = diags.Append(c.providerDevOverrideInitWarnings())
	diags = diags.Append(c.providerUnmanagedInitWarnings())

	// Collect the provider dependencies from the configuration.
	allReqs, hclDiags := config.ProviderRequirements()
	diags = diags.Append(hclDiags)
	if hclDiags.HasErrors() {
		return false, nil, SafeInitActionInvalid, nil, diags
	}

	// filter out only PSS providers from allReqs
	reqs := make(providerreqs.Requirements, 1)
	for providerReq, cons := range allReqs {
		if providerReq.Equals(config.Module.StateStore.ProviderAddr) {
			reqs[providerReq] = cons
		}
	}

	for providerAddr := range reqs {
		if providerAddr.IsLegacy() {
			diags = diags.Append(tfdiags.Sourceless(
				tfdiags.Error,
				"Invalid legacy provider address",
				fmt.Sprintf(
					"This configuration or its associated state refers to the unqualified provider %q.\n\nYou must complete the Terraform 0.13 upgrade process before upgrading to later versions.",
					providerAddr.Type,
				),
			))
		}
	}
	if diags.HasErrors() {
		return false, nil, SafeInitActionInvalid, nil, diags
	}

	var inst *providercache.Installer
	if len(pluginDirs) == 0 {
		// By default we use a source that looks for providers in all of the
		// standard locations, possibly customized by the user in CLI config.
		inst = c.providerInstaller()
	} else {
		// If the user passes at least one -plugin-dir then that circumvents
		// the usual sources and forces Terraform to consult only the given
		// directories. Anything not available in one of those directories
		// is not available for installation.
		source := c.providerCustomLocalDirectorySource(pluginDirs)
		inst = c.providerInstallerCustomSource(source)

		// The default (or configured) search paths are logged earlier, in provider_source.go
		// Log that those are being overridden by the `-plugin-dir` command line options
		log.Println("[DEBUG] init: overriding provider plugin search paths")
		log.Printf("[DEBUG] will search for provider plugins in %s", pluginDirs)
	}

	// Prepare callback functions for the installer.
	// These allow us to send output to the terminal as events happen, catch
	// diagnostics, etc.
	//
	// We use some callbacks to capture data that's surfaced during the
	// installation process:
	// - provider authentication info.
	// - info about what type of location a provider is sourced from.
	// These pieces of data are used to determine if additional security features
	// need to be enabled.
	providerLocations := make(map[addrs.Provider]getproviders.PackageLocation)
	var stateStoreProviderAuthResult *getproviders.PackageAuthenticationResult
	evts := &providercache.InstallerEvents{
		PendingProviders: func(reqs map[addrs.Provider]getproviders.VersionConstraints) {
			view.Output(views.InitializingStateStoreProviderPluginMessage, config.Module.StateStore.Type)
		},
		ProviderAlreadyInstalled: providerAlreadyInstalledCallback(view),
		BuiltInProviderAvailable: builtInProviderAvailableCallback(view),
		BuiltInProviderFailure:   builtInProviderFailureCallback(&diags),
		QueryPackagesBegin: func(provider addrs.Provider, versionConstraints getproviders.VersionConstraints, locked bool) {
			if locked {
				view.LogInitMessage(views.ReusingPreviousVersionInfo, provider.ForDisplay())
			} else {
				if len(versionConstraints) > 0 {
					view.LogInitMessage(views.FindingMatchingVersionMessage, provider.ForDisplay(), getproviders.VersionConstraintsString(versionConstraints))
				} else {
					view.LogInitMessage(views.FindingLatestVersionMessage, provider.ForDisplay())
				}
			}
		},
		LinkFromCacheBegin: linkFromCacheBeginCallback(view),
		FetchPackageBegin: func(provider addrs.Provider, version getproviders.Version, location getproviders.PackageLocation) {
			// 1) Record the location of this provider.
			//
			// FetchPackageBegin is the callback hook at the start of the process of obtaining a provider that isn't yet
			// in the dependency lock file. Providers that are processed here will not be processed here on the next init,
			// as then they will be in the lock file. The same provider type would only be processed here again if the
			// provider version changed via an `init -upgrade` command.
			providerLocations[provider] = location

			// 2) Call the shared callback for FetchPackageBegin.
			cb := fetchPackageBeginCallback(view)
			cb(provider, version, location)
		},
		QueryPackagesFailure: queryPackagesFailureCallback(&diags, ctx, inst.ProviderSource(), reqs),
		QueryPackagesWarning: queryPackagesWarningCallback(&diags),
		LinkFromCacheFailure: linkFromCacheFailureCallback(&diags),
		FetchPackageFailure:  fetchPackageFailureCallback(&diags, reqs),
		FetchPackageSuccess: func(provider addrs.Provider, version getproviders.Version, localDir string, authResult *getproviders.PackageAuthenticationResult) {
			// 1. Capture auth result if this provider is used for state storage.
			if config.Module.StateStore != nil && provider.Equals(config.Module.StateStore.ProviderAddr) {
				log.Printf("[TRACE] getProvidersFromConfig: state storage provider %s (%q) auth result: %q", config.Module.StateStore.ProviderAddr.Type, config.Module.StateStore.ProviderAddr.ForDisplay(), stateStoreProviderAuthResult.String())
				stateStoreProviderAuthResult = authResult
			}

			// 2. Call the shared callback for FetchPackageSuccess
			cb := fetchPackageSuccessCallback(view)
			cb(provider, version, localDir, authResult)
		},
		ProvidersLockUpdated: providersLockUpdatedCallback(&c.incompleteProviders),
		ProvidersFetched:     providersFetchedCallback(view),
	}
	ctx = evts.OnContext(ctx)

	mode := providercache.InstallNewProvidersOnly
	if upgrade {
		mode = providercache.InstallUpgrades
	}

	// Determine which required providers are already downloaded, and download any
	// new providers or newer versions of providers
	configLocks, err := inst.EnsureProviderVersions(ctx, previousLocks, reqs, mode)
	if ctx.Err() == context.Canceled {
		diags = diags.Append(fmt.Errorf("Provider installation was canceled by an interrupt signal."))
		view.Diagnostics(diags)
		return true, nil, SafeInitActionInvalid, nil, diags
	}
	if err != nil {
		// The errors captured in "err" should be redundant with what we
		// received via the InstallerEvents callbacks above, so we'll
		// just return those as long as we have some.
		if !diags.HasErrors() {
			diags = diags.Append(err)
		}

		return true, nil, SafeInitActionInvalid, nil, diags
	}

	// Return advice to the calling code about what to do regarding safe init feature related to state storage providers
	location, ok := providerLocations[config.Module.StateStore.ProviderAddr]
	if !ok {
		// The provider was not processed in the FetchPackageBegin callback.
		// A provider that wasn't downloaded during this init could be because:
		// * It was already present from a previous installation.
		// * If upgrading, no newer version was available that matched version constraints.
		// * Or, the provider is unmanaged/reattached and so download was skipped.
		log.Printf("[TRACE] init (getProvidersFromPSSConfig): the state storage provider %s (%q) will not be changed in the dependency lock file after provider installation. Either it was already present and/or there was no available upgrade version that matched version constraints.", config.Module.StateStore.ProviderAddr.Type, config.Module.StateStore.ProviderAddr)
		safeInitAction = SafeInitActionProceed
	} else {
		// The provider was processed in the FetchPackageBegin callback, so either it's being downloaded for the first time, or upgraded.
		log.Printf("[TRACE] init (getProvidersFromConfig): the state storage provider %s (%q) will be changed in the dependency lock file during provider installation.", config.Module.StateStore.ProviderAddr.Type, config.Module.StateStore.ProviderAddr)

		switch location.(type) {
		case getproviders.PackageLocalArchive, getproviders.PackageLocalDir:
			// If the provider is downloaded from a local source we assume it's safe.
			// We don't require presence of the -safe-init flag, or require input from the user to approve its usage.
			log.Printf("[TRACE] init (getProvidersFromConfig): the state storage provider %s (%q) is downloaded from a local source, so we consider it safe.", config.Module.StateStore.ProviderAddr.Type, config.Module.StateStore.ProviderAddr)
			safeInitAction = SafeInitActionProceed
		case getproviders.PackageHTTPURL:
			log.Printf("[DEBUG] init (getProvidersFromConfig): the state storage provider %s (%q) is downloaded via HTTP, so we consider it potentially unsafe.", config.Module.StateStore.ProviderAddr.Type, config.Module.StateStore.ProviderAddr)
			safeInitAction = SafeInitActionRequireApproval
		default:
			panic(fmt.Sprintf("init (getProvidersFromConfig): unexpected provider location type for state storage provider %q: %T", config.Module.StateStore.ProviderAddr, location))
		}
	}

	return true, configLocks, safeInitAction, stateStoreProviderAuthResult, diags
}

// getProviders determines what providers are required by the config and state
// and downloads any missing providers that aren't already downloaded and then returns
// updated dependency lock data. The dependency lock *file* itself isn't updated here.
//
// See getProvidersFromPSSConfig which is equivalent for state store providers.
func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config, state *states.State, upgrade bool, configLocks *depsfile.Locks, pluginDirs []string, view views.Init) (output bool, resultingLocks *depsfile.Locks, diags tfdiags.Diagnostics) {
	ctx, span := tracer.Start(ctx, "install providers")
	defer span.End()

	// Dev overrides cause the result of "terraform init" to be irrelevant for
	// any overridden providers, so we'll warn about it to avoid later
	// confusion when Terraform ends up using a different provider than the
	// lock file called for.
	diags = diags.Append(c.providerDevOverrideInitWarnings())
	diags = diags.Append(c.providerUnmanagedInitWarnings())

	// First we'll collect all the provider dependencies we can see in the
	// configuration and the state.
	reqs, hclDiags := config.ProviderRequirements()
	diags = diags.Append(hclDiags)
	if hclDiags.HasErrors() {
		return false, nil, diags
	}
	if state != nil {
		stateReqs := state.ProviderRequirements()
		reqs = reqs.Merge(stateReqs)
	}

	for providerAddr := range reqs {
		if providerAddr.IsLegacy() {
			diags = diags.Append(tfdiags.Sourceless(
				tfdiags.Error,
				"Invalid legacy provider address",
				fmt.Sprintf(
					"This configuration or its associated state refers to the unqualified provider %q.\n\nYou must complete the Terraform 0.13 upgrade process before upgrading to later versions.",
					providerAddr.Type,
				),
			))
		}
	}
	if diags.HasErrors() {
		return false, nil, diags
	}

	// The locks below are used to avoid re-downloading any providers in the
	// second download step.
	// We combine any locks from the dependency lock file and locks identified
	// from the configuration
	var moreDiags tfdiags.Diagnostics
	previousLocks, moreDiags := c.lockedDependencies()
	diags = diags.Append(moreDiags)
	if diags.HasErrors() {
		return false, nil, diags
	}
	inProgressLocks := c.mergeLockedDependencies(configLocks, previousLocks)

	var inst *providercache.Installer
	if len(pluginDirs) == 0 {
		// By default we use a source that looks for providers in all of the
		// standard locations, possibly customized by the user in CLI config.
		inst = c.providerInstaller()
	} else {
		// If the user passes at least one -plugin-dir then that circumvents
		// the usual sources and forces Terraform to consult only the given
		// directories. Anything not available in one of those directories
		// is not available for installation.
		source := c.providerCustomLocalDirectorySource(pluginDirs)
		inst = c.providerInstallerCustomSource(source)

		// The default (or configured) search paths are logged earlier, in provider_source.go
		// Log that those are being overridden by the `-plugin-dir` command line options
		log.Println("[DEBUG] init: overriding provider plugin search paths")
		log.Printf("[DEBUG] will search for provider plugins in %s", pluginDirs)
	}

	// Because we're currently just streaming a series of events sequentially
	// into the terminal, we're showing only a subset of the events to keep
	// things relatively concise. Later it'd be nice to have a progress UI
	// where statuses update in-place, but we can't do that as long as we
	// are shimming our vt100 output to the legacy console API on Windows.
	evts := &providercache.InstallerEvents{
		PendingProviders: func(reqs map[addrs.Provider]getproviders.VersionConstraints) {
			view.Output(views.InitializingProviderPluginMessage)
		},
		ProviderAlreadyInstalled: providerAlreadyInstalledCallback(view),
		BuiltInProviderAvailable: builtInProviderAvailableCallback(view),
		BuiltInProviderFailure:   builtInProviderFailureCallback(&diags),
		QueryPackagesBegin: func(provider addrs.Provider, versionConstraints getproviders.VersionConstraints, locked bool) {
			if locked {
				view.LogInitMessage(views.ReusingPreviousVersionInfo, provider.ForDisplay())
			} else {
				if len(versionConstraints) > 0 {
					view.LogInitMessage(views.FindingMatchingVersionMessage, provider.ForDisplay(), getproviders.VersionConstraintsString(versionConstraints))
				} else {
					view.LogInitMessage(views.FindingLatestVersionMessage, provider.ForDisplay())
				}
			}
		},
		LinkFromCacheBegin:   linkFromCacheBeginCallback(view),
		FetchPackageBegin:    fetchPackageBeginCallback(view),
		QueryPackagesFailure: queryPackagesFailureCallback(&diags, ctx, inst.ProviderSource(), reqs),
		QueryPackagesWarning: queryPackagesWarningCallback(&diags),
		LinkFromCacheFailure: linkFromCacheFailureCallback(&diags),
		FetchPackageFailure:  fetchPackageFailureCallback(&diags, reqs),
		FetchPackageSuccess:  fetchPackageSuccessCallback(view),
		ProvidersLockUpdated: providersLockUpdatedCallback(&c.incompleteProviders),
		ProvidersFetched:     providersFetchedCallback(view),
	}
	ctx = evts.OnContext(ctx)

	mode := providercache.InstallNewProvidersOnly
	if upgrade {
		mode = providercache.InstallUpgrades
	}

	// We don't handle upgrade flags here, i.e. what happens at this point in getProvidersFromConfig:
	// > We cannot upgrade a provider used only by the state, as there are no version constraints in state.
	//    > Given the overlap between providers in the config and state, using the upgrade mode here
	//      would remove the effects of version constraints from the config.
	// > Any validation of CLI flag usage is already done in getProvidersFromConfig

	newLocks, err := inst.EnsureProviderVersions(ctx, inProgressLocks, reqs, mode)
	if ctx.Err() == context.Canceled {
		diags = diags.Append(fmt.Errorf("Provider installation was canceled by an interrupt signal."))
		view.Diagnostics(diags)
		return true, nil, diags
	}
	if err != nil {
		// The errors captured in "err" should be redundant with what we
		// received via the InstallerEvents callbacks above, so we'll
		// just return those as long as we have some.
		if !diags.HasErrors() {
			diags = diags.Append(err)
		}

		return true, nil, diags
	}

	return true, newLocks, diags
}

// saveDependencyLockFile overwrites the contents of the dependency lock file.
// The calling code is expected to provide the previous locks (if any) and the two sets of locks determined from
// configuration and state data.
func (c *InitCommand) saveDependencyLockFile(previousLocks, configLocks, stateLocks *depsfile.Locks, flagLockfile string, view views.Init) (output bool, diags tfdiags.Diagnostics) {
	// Get the combination of config and state locks
	newLocks := c.mergeLockedDependencies(configLocks, stateLocks)

	// If the provider dependencies have changed since the last run then we'll
	// say a little about that in case the reader wasn't expecting a change.
	// (When we later integrate module dependencies into the lock file we'll
	// probably want to refactor this so that we produce one lock-file related
	// message for all changes together, but this is here for now just because
	// it's the smallest change relative to what came before it, which was
	// a hidden JSON file specifically for tracking providers.)
	if !newLocks.Equal(previousLocks) {
		// if readonly mode
		if flagLockfile == "readonly" {
			// check if required provider dependencies change
			if !newLocks.EqualProviderAddress(previousLocks) {
				diags = diags.Append(tfdiags.Sourceless(
					tfdiags.Error,
					`Provider dependency changes detected`,
					`Changes to the required provider dependencies were detected, but the lock file is read-only. To use and record these requirements, run "terraform init" without the "-lockfile=readonly" flag.`,
				))
				return output, diags
			}
			// suppress updating the file to record any new information it learned,
			// such as a hash using a new scheme.
			diags = diags.Append(tfdiags.Sourceless(
				tfdiags.Warning,
				`Provider lock file not updated`,
				`Changes to the provider selections were detected, but not saved in the .terraform.lock.hcl file. To record these selections, run "terraform init" without the "-lockfile=readonly" flag.`,
			))
			return output, diags
		}
		// Jump in here and add a warning if any of the providers are incomplete.
		if len(c.incompleteProviders) > 0 {
			// We don't really care about the order here, we just want the
			// output to be deterministic.
			sort.Slice(c.incompleteProviders, func(i, j int) bool {
				return c.incompleteProviders[i] < c.incompleteProviders[j]
			})
			diags = diags.Append(tfdiags.Sourceless(
				tfdiags.Warning,
				incompleteLockFileInformationHeader,
				fmt.Sprintf(
					incompleteLockFileInformationBody,
					strings.Join(c.incompleteProviders, "\n  - "),
					getproviders.CurrentPlatform.String())))
		}
		if previousLocks.Empty() {
			// A change from empty to non-empty is special because it suggests
			// we're running "terraform init" for the first time against a
			// new configuration. In that case we'll take the opportunity to
			// say a little about what the dependency lock file is, for new
			// users or those who are upgrading from a previous Terraform
			// version that didn't have dependency lock files.
			view.Output(views.LockInfo)
			output = true
		} else {
			view.Output(views.DependenciesLockChangesInfo)
			output = true
		}
		lockFileDiags := c.replaceLockedDependencies(newLocks)
		diags = diags.Append(lockFileDiags)
	}
	return output, diags
}

// backendConfigOverrideBody interprets the raw values of -backend-config
// arguments into a hcl Body that should override the backend settings given
// in the configuration.
//
// If the result is nil then no override needs to be provided.
//
// If the returned diagnostics contains errors then the returned body may be
// incomplete or invalid.
func (c *InitCommand) backendConfigOverrideBody(flags arguments.FlagNameValueSlice, schema *configschema.Block) (hcl.Body, tfdiags.Diagnostics) {
	items := flags.AllItems()
	if len(items) == 0 {
		return nil, nil
	}

	var ret hcl.Body
	var diags tfdiags.Diagnostics
	synthVals := make(map[string]cty.Value)

	mergeBody := func(newBody hcl.Body) {
		if ret == nil {
			ret = newBody
		} else {
			ret = configs.MergeBodies(ret, newBody)
		}
	}
	flushVals := func() {
		if len(synthVals) == 0 {
			return
		}
		newBody := configs.SynthBody("-backend-config=...", synthVals)
		mergeBody(newBody)
		synthVals = make(map[string]cty.Value)
	}

	if len(items) == 1 && items[0].Value == "" {
		// Explicitly remove all -backend-config options.
		// We do this by setting an empty but non-nil ConfigOverrides.
		return configs.SynthBody("-backend-config=''", synthVals), diags
	}

	for _, item := range items {
		eq := strings.Index(item.Value, "=")

		if eq == -1 {
			// The value is interpreted as a filename.
			newBody, fileDiags := c.loadHCLFile(item.Value)
			diags = diags.Append(fileDiags)
			if fileDiags.HasErrors() {
				continue
			}
			// Generate an HCL body schema for the backend block.
			var bodySchema hcl.BodySchema
			for name := range schema.Attributes {
				// We intentionally ignore the `Required` attribute here
				// because backend config override files can be partial. The
				// goal is to make sure we're not loading a file with
				// extraneous attributes or blocks.
				bodySchema.Attributes = append(bodySchema.Attributes, hcl.AttributeSchema{
					Name: name,
				})
			}
			for name, block := range schema.BlockTypes {
				var labelNames []string
				if block.Nesting == configschema.NestingMap {
					labelNames = append(labelNames, "key")
				}
				bodySchema.Blocks = append(bodySchema.Blocks, hcl.BlockHeaderSchema{
					Type:       name,
					LabelNames: labelNames,
				})
			}
			// Verify that the file body matches the expected backend schema.
			_, schemaDiags := newBody.Content(&bodySchema)
			diags = diags.Append(schemaDiags)
			if schemaDiags.HasErrors() {
				continue
			}
			flushVals() // deal with any accumulated individual values first
			mergeBody(newBody)
		} else {
			name := item.Value[:eq]
			rawValue := item.Value[eq+1:]
			attrS := schema.Attributes[name]
			if attrS == nil {
				diags = diags.Append(tfdiags.Sourceless(
					tfdiags.Error,
					"Invalid backend configuration argument",
					fmt.Sprintf("The backend configuration argument %q given on the command line is not expected for the selected backend type.", name),
				))
				continue
			}
			value, valueDiags := configValueFromCLI(item.String(), rawValue, attrS.Type)
			diags = diags.Append(valueDiags)
			if valueDiags.HasErrors() {
				continue
			}
			synthVals[name] = value
		}
	}

	flushVals()

	return ret, diags
}

func (c *InitCommand) AutocompleteArgs() complete.Predictor {
	return complete.PredictDirs("")
}

func (c *InitCommand) AutocompleteFlags() complete.Flags {
	return complete.Flags{
		"-backend":        completePredictBoolean,
		"-cloud":          completePredictBoolean,
		"-backend-config": complete.PredictFiles("*.tfvars"), // can also be key=value, but we can't "predict" that
		"-force-copy":     complete.PredictNothing,
		"-from-module":    completePredictModuleSource,
		"-get":            completePredictBoolean,
		"-input":          completePredictBoolean,
		"-lock":           completePredictBoolean,
		"-lock-timeout":   complete.PredictAnything,
		"-no-color":       complete.PredictNothing,
		"-json":           complete.PredictNothing,
		"-plugin-dir":     complete.PredictDirs(""),
		"-reconfigure":    complete.PredictNothing,
		"-migrate-state":  complete.PredictNothing,
		"-upgrade":        completePredictBoolean,
	}
}

func (c *InitCommand) Help() string {
	helpText := `
Usage: terraform [global options] init [options]

  Initialize a new or existing Terraform working directory by creating
  initial files, loading any remote state, downloading modules, etc.

  This is the first command that should be run for any new or existing
  Terraform configuration per machine. This sets up all the local data
  necessary to run Terraform that is typically not committed to version
  control.

  This command is always safe to run multiple times. Though subsequent runs
  may give errors, this command will never delete your configuration or
  state. Even so, if you have important information, please back it up prior
  to running this command, just in case.

Options:

  -backend=false          Disable backend or HCP Terraform initialization
                          for this configuration and use what was previously
                          initialized instead.

                          aliases: -cloud=false

  -backend-config=path    Configuration to be merged with what is in the
                          configuration file's 'backend' block. This can be
                          either a path to an HCL file with key/value
                          assignments (same format as terraform.tfvars) or a
                          'key=value' format, and can be specified multiple
                          times. The backend type must be in the configuration
                          itself.

  -force-copy             Suppress prompts about copying state data when
                          initializing a new state backend. This is
                          equivalent to providing a "yes" to all confirmation
                          prompts.

  -from-module=SOURCE     Copy the contents of the given module into the target
                          directory before initialization.

  -get=false              Disable downloading modules for this configuration.

  -input=false            Disable interactive prompts. Note that some actions may
                          require interactive prompts and will error if input is
                          disabled.

  -lock=false             Don't hold a state lock during backend migration.
                          This is dangerous if others might concurrently run
                          commands against the same workspace.

  -lock-timeout=0s        Duration to retry a state lock.

  -no-color               If specified, output won't contain any color.

  -json                   If specified, machine readable output will be
                          printed in JSON format.

  -plugin-dir             Directory containing plugin binaries. This overrides all
                          default search paths for plugins, and prevents the
                          automatic installation of plugins. This flag can be used
                          multiple times.

  -reconfigure            Reconfigure a backend, ignoring any saved
                          configuration.

  -migrate-state          Reconfigure a backend, and attempt to migrate any
                          existing state.

  -upgrade                Install the latest module and provider versions
                          allowed within configured constraints, overriding the
                          default behavior of selecting exactly the version
                          recorded in the dependency lockfile.

  -lockfile=MODE          Set a dependency lockfile mode.
                          Currently only "readonly" is valid.

  -ignore-remote-version  A rare option used for HCP Terraform and the remote backend
                          only. Set this to ignore checking that the local and remote
                          Terraform versions use compatible state representations, making
                          an operation proceed even when there is a potential mismatch.
                          See the documentation on configuring Terraform with
                          HCP Terraform or Terraform Enterprise for more information.

  -test-directory=path    Set the Terraform test directory, defaults to "tests".

  -var 'foo=bar'          Set a value for one of the input variables in the root
                          module of the configuration. Use this option more than
                          once to set more than one variable.

  -var-file=filename      Load variable values from the given file, in addition
                          to the default files terraform.tfvars and *.auto.tfvars.
                          Use this option more than once to include more than one
                          variables file.

  -enable-pluggable-state-storage-experiment [EXPERIMENTAL]
                          A flag to enable an alternative init command that allows use of
                          pluggable state storage. Only usable with experiments enabled.

  -state-provider-lock-file [EXPERIMENTAL]
                          Specifies a lock file Terraform should use to establish trust in 
                          a provider before initializing a state store for the first time.
                          Only usable when input is disabled through -input=false.
                          Only usable with experiments enabled and the
                          -enable-pluggable-state-storage-experiment flag present.
`
	return strings.TrimSpace(helpText)
}

func (c *InitCommand) Synopsis() string {
	return "Prepare your working directory for other commands"
}

// Returns a reused callback function for the ProviderAlreadyInstalled event in a providercache.InstallerEvents struct.
func providerAlreadyInstalledCallback(view views.Init) func(provider addrs.Provider, selectedVersion getproviders.Version) {
	return func(provider addrs.Provider, selectedVersion getproviders.Version) {
		view.LogInitMessage(views.ProviderAlreadyInstalledMessage, provider.ForDisplay(), selectedVersion)
	}
}

// Returns a reused callback function for the BuiltInProviderAvailable event in a providercache.InstallerEvents struct.
func builtInProviderAvailableCallback(view views.Init) func(provider addrs.Provider) {
	return func(provider addrs.Provider) {
		view.LogInitMessage(views.BuiltInProviderAvailableMessage, provider.ForDisplay())
	}
}

// Returns a reused callback function for the BuiltinProviderFailure event in a providercache.InstallerEvents struct.
func builtInProviderFailureCallback(diags *tfdiags.Diagnostics) func(provider addrs.Provider, err error) {
	return func(provider addrs.Provider, err error) {
		*diags = diags.Append(tfdiags.Sourceless(
			tfdiags.Error,
			"Invalid dependency on built-in provider",
			fmt.Sprintf("Cannot use %s: %s.", provider.ForDisplay(), err),
		))
	}
}

// Returns a reused callback function for the LinkFromCacheBegin event in a providercache.InstallerEvents struct.
func linkFromCacheBeginCallback(view views.Init) func(provider addrs.Provider, version getproviders.Version, cacheRoot string) {
	return func(provider addrs.Provider, version getproviders.Version, cacheRoot string) {
		view.LogInitMessage(views.UsingProviderFromCacheDirInfo, provider.ForDisplay(), version)
	}
}

// Returns a reused callback function for the FetchPackageBegin event in a providercache.InstallerEvents struct.
func fetchPackageBeginCallback(view views.Init) func(provider addrs.Provider, version getproviders.Version, location getproviders.PackageLocation) {
	return func(provider addrs.Provider, version getproviders.Version, location getproviders.PackageLocation) {
		view.LogInitMessage(views.InstallingProviderMessage, provider.ForDisplay(), version)
	}
}

// Returns a reused callback function for the QueryPackagesFailure event in a providercache.InstallerEvents struct.
func queryPackagesFailureCallback(diags *tfdiags.Diagnostics, ctx context.Context, source getproviders.Source, reqs getproviders.Requirements) func(provider addrs.Provider, err error) {
	return func(provider addrs.Provider, err error) {
		switch errorTy := err.(type) {
		case getproviders.ErrProviderNotFound:
			sources := errorTy.Sources
			displaySources := make([]string, len(sources))
			for i, source := range sources {
				displaySources[i] = fmt.Sprintf("  - %s", source)
			}
			*diags = diags.Append(tfdiags.Sourceless(
				tfdiags.Error,
				"Failed to query available provider packages",
				fmt.Sprintf("Could not retrieve the list of available versions for provider %s: %s\n\n%s",
					provider.ForDisplay(), err, strings.Join(displaySources, "\n"),
				),
			))
		case getproviders.ErrRegistryProviderNotKnown:
			// We might be able to suggest an alternative provider to use
			// instead of this one.
			suggestion := fmt.Sprintf("\n\nAll modules should specify their required_providers so that external consumers will get the correct providers when using a module. To see which modules are currently depending on %s, run the following command:\n    terraform providers", provider.ForDisplay())
			alternative := getproviders.MissingProviderSuggestion(ctx, provider, source, reqs)
			if alternative != provider {
				suggestion = fmt.Sprintf(
					"\n\nDid you intend to use %s? If so, you must specify that source address in each module which requires that provider. To see which modules are currently depending on %s, run the following command:\n    terraform providers",
					alternative.ForDisplay(), provider.ForDisplay(),
				)
			}

			*diags = diags.Append(tfdiags.Sourceless(
				tfdiags.Error,
				"Failed to query available provider packages",
				fmt.Sprintf("Could not retrieve the list of available versions for provider %s: %s%s",
					provider.ForDisplay(), err, suggestion,
				),
			))
		case getproviders.ErrHostNoProviders:
			switch {
			case errorTy.Hostname == svchost.Hostname("github.com") && !errorTy.HasOtherVersion:
				// If a user copies the URL of a GitHub repository into
				// the source argument and removes the schema to make it
				// provider-address-shaped then that's one way we can end up
				// here. We'll use a specialized error message in anticipation
				// of that mistake. We only do this if github.com isn't a
				// provider registry, to allow for the (admittedly currently
				// rather unlikely) possibility that github.com starts being
				// a real Terraform provider registry in the future.
				*diags = diags.Append(tfdiags.Sourceless(
					tfdiags.Error,
					"Invalid provider registry host",
					fmt.Sprintf("The given source address %q specifies a GitHub repository rather than a Terraform provider. Refer to the documentation of the provider to find the correct source address to use.",
						provider.String(),
					),
				))

			case errorTy.HasOtherVersion:
				*diags = diags.Append(tfdiags.Sourceless(
					tfdiags.Error,
					"Invalid provider registry host",
					fmt.Sprintf("The host %q given in provider source address %q does not offer a Terraform provider registry that is compatible with this Terraform version, but it may be compatible with a different Terraform version.",
						errorTy.Hostname, provider.String(),
					),
				))

			default:
				*diags = diags.Append(tfdiags.Sourceless(
					tfdiags.Error,
					"Invalid provider registry host",
					fmt.Sprintf("The host %q given in provider source address %q does not offer a Terraform provider registry.",
						errorTy.Hostname, provider.String(),
					),
				))
			}

		case getproviders.ErrRequestCanceled:
			// We don't attribute cancellation to any particular operation,
			// but rather just emit a single general message about it at
			// the end, by checking ctx.Err().

		default:
			suggestion := fmt.Sprintf("\n\nTo see which modules are currently depending on %s and what versions are specified, run the following command:\n    terraform providers", provider.ForDisplay())
			*diags = diags.Append(tfdiags.Sourceless(
				tfdiags.Error,
				"Failed to query available provider packages",
				fmt.Sprintf("Could not retrieve the list of available versions for provider %s: %s%s",
					provider.ForDisplay(), err, suggestion,
				),
			))
		}
	}
}

// Returns a reused callback function for the QueryPackagesWarning event in a providercache.InstallerEvents struct.
func queryPackagesWarningCallback(diags *tfdiags.Diagnostics) func(provider addrs.Provider, warnings []string) {
	return func(provider addrs.Provider, warnings []string) {
		displayWarnings := make([]string, len(warnings))
		for i, warning := range warnings {
			displayWarnings[i] = fmt.Sprintf("- %s", warning)
		}

		*diags = diags.Append(tfdiags.Sourceless(
			tfdiags.Warning,
			"Additional provider information from registry",
			fmt.Sprintf("The remote registry returned warnings for %s:\n%s",
				provider.String(),
				strings.Join(displayWarnings, "\n"),
			),
		))
	}
}

// Returns a reused callback function for the LinkFromCacheFailure event in a providercache.InstallerEvents struct.
func linkFromCacheFailureCallback(diags *tfdiags.Diagnostics) func(provider addrs.Provider, version getproviders.Version, err error) {
	return func(provider addrs.Provider, version getproviders.Version, err error) {
		*diags = diags.Append(tfdiags.Sourceless(
			tfdiags.Error,
			"Failed to install provider from shared cache",
			fmt.Sprintf("Error while importing %s v%s from the shared cache directory: %s.", provider.ForDisplay(), version, err),
		))
	}
}

// Returns a reused callback function for the FetchPackageFailure event in a providercache.InstallerEvents struct.
func fetchPackageFailureCallback(diags *tfdiags.Diagnostics, reqs getproviders.Requirements) func(provider addrs.Provider, version getproviders.Version, err error) {
	return func(provider addrs.Provider, version getproviders.Version, err error) {
		const summaryIncompatible = "Incompatible provider version"
		switch err := err.(type) {
		case getproviders.ErrProtocolNotSupported:
			closestAvailable := err.Suggestion
			switch {
			case closestAvailable == getproviders.UnspecifiedVersion:
				*diags = diags.Append(tfdiags.Sourceless(
					tfdiags.Error,
					summaryIncompatible,
					fmt.Sprintf(errProviderVersionIncompatible, provider.String()),
				))
			case version.GreaterThan(closestAvailable):
				*diags = diags.Append(tfdiags.Sourceless(
					tfdiags.Error,
					summaryIncompatible,
					fmt.Sprintf(providerProtocolTooNew, provider.ForDisplay(),
						version, tfversion.String(), closestAvailable, closestAvailable,
						getproviders.VersionConstraintsString(reqs[provider]),
					),
				))
			default: // version is less than closestAvailable
				*diags = diags.Append(tfdiags.Sourceless(
					tfdiags.Error,
					summaryIncompatible,
					fmt.Sprintf(providerProtocolTooOld, provider.ForDisplay(),
						version, tfversion.String(), closestAvailable, closestAvailable,
						getproviders.VersionConstraintsString(reqs[provider]),
					),
				))
			}
		case getproviders.ErrPlatformNotSupported:
			switch {
			case err.MirrorURL != nil:
				// If we're installing from a mirror then it may just be
				// the mirror lacking the package, rather than it being
				// unavailable from upstream.
				*diags = diags.Append(tfdiags.Sourceless(
					tfdiags.Error,
					summaryIncompatible,
					fmt.Sprintf(
						"Your chosen provider mirror at %s does not have a %s v%s package available for your current platform, %s.\n\nProvider releases are separate from Terraform CLI releases, so this provider might not support your current platform. Alternatively, the mirror itself might have only a subset of the plugin packages available in the origin registry, at %s.",
						err.MirrorURL, err.Provider, err.Version, err.Platform,
						err.Provider.Hostname,
					),
				))
			default:
				*diags = diags.Append(tfdiags.Sourceless(
					tfdiags.Error,
					summaryIncompatible,
					fmt.Sprintf(
						"Provider %s v%s does not have a package available for your current platform, %s.\n\nProvider releases are separate from Terraform CLI releases, so not all providers are available for all platforms. Other versions of this provider may have different platforms supported.",
						err.Provider, err.Version, err.Platform,
					),
				))
			}

		case getproviders.ErrRequestCanceled:
			// We don't attribute cancellation to any particular operation,
			// but rather just emit a single general message about it at
			// the end, by checking ctx.Err().

		default:
			// We can potentially end up in here under cancellation too,
			// in spite of our getproviders.ErrRequestCanceled case above,
			// because not all of the outgoing requests we do under the
			// "fetch package" banner are source metadata requests.
			// In that case we will emit a redundant error here about
			// the request being cancelled, but we'll still detect it
			// as a cancellation after the installer returns and do the
			// normal cancellation handling.

			*diags = diags.Append(tfdiags.Sourceless(
				tfdiags.Error,
				"Failed to install provider",
				fmt.Sprintf("Error while installing %s v%s: %s", provider.ForDisplay(), version, err),
			))
		}
	}
}

// Returns a reused callback function for the FetchPackageSuccess event in a providercache.InstallerEvents struct.
func fetchPackageSuccessCallback(view views.Init) func(provider addrs.Provider, version getproviders.Version, localDir string, authResult *getproviders.PackageAuthenticationResult) {
	return func(provider addrs.Provider, version getproviders.Version, localDir string, authResult *getproviders.PackageAuthenticationResult) {
		var keyID string
		if authResult != nil && authResult.ThirdPartySigned() {
			keyID = authResult.KeyID
		}
		if keyID != "" {
			keyID = view.PrepareMessage(views.KeyID, keyID)
		}

		view.LogInitMessage(views.InstalledProviderVersionInfo, provider.ForDisplay(), version, authResult, keyID)
	}
}

// Returns a reused callback function for the ProvidersLockUpdated event in a providercache.InstallerEvents struct.
func providersLockUpdatedCallback(incompleteProviders *[]string) func(provider addrs.Provider, version getproviders.Version, localHashes []getproviders.Hash, signedHashes []getproviders.Hash, priorHashes []getproviders.Hash) {
	return func(provider addrs.Provider, version getproviders.Version, localHashes []getproviders.Hash, signedHashes []getproviders.Hash, priorHashes []getproviders.Hash) {
		// We're going to use this opportunity to track if we have any
		// "incomplete" installs of providers. An incomplete install is
		// when we are only going to write the local hashes into our lock
		// file which means a `terraform init` command will fail in future
		// when used on machines of a different architecture.
		//
		// We want to print a warning about this.

		if len(signedHashes) > 0 {
			// If we have any signedHashes hashes then we don't worry - as
			// we know we retrieved all available hashes for this version
			// anyway.
			return
		}

		// If local hashes and prior hashes are exactly the same then
		// it means we didn't record any signed hashes previously, and
		// we know we're not adding any extra in now (because we already
		// checked the signedHashes), so that's a problem.
		//
		// In the actual check here, if we have any priorHashes and those
		// hashes are not the same as the local hashes then we're going to
		// accept that this provider has been configured correctly.
		if len(priorHashes) > 0 && !reflect.DeepEqual(localHashes, priorHashes) {
			return
		}

		// Now, either signedHashes is empty, or priorHashes is exactly the
		// same as our localHashes which means we never retrieved the
		// signedHashes previously.
		//
		// Either way, this is bad. Let's complain/warn.
		*incompleteProviders = append(*incompleteProviders, provider.ForDisplay())
	}
}

// Returns a reused callback function for the ProvidersFetched event in a providercache.InstallerEvents struct.
func providersFetchedCallback(view views.Init) func(authResults map[addrs.Provider]*getproviders.PackageAuthenticationResult) {
	return func(authResults map[addrs.Provider]*getproviders.PackageAuthenticationResult) {
		thirdPartySigned := false
		for _, authResult := range authResults {
			if authResult.ThirdPartySigned() {
				thirdPartySigned = true
				break
			}
		}
		if thirdPartySigned {
			view.LogInitMessage(views.PartnerAndCommunityProvidersMessage)
		}
	}
}

const errInitCopyNotEmpty = `
The working directory already contains files. The -from-module option requires
an empty directory into which a copy of the referenced module will be placed.

To initialize the configuration already in this working directory, omit the
-from-module option.
`

// providerProtocolTooOld is a message sent to the CLI UI if the provider's
// supported protocol versions are too old for the user's version of terraform,
// but a newer version of the provider is compatible.
const providerProtocolTooOld = `Provider %q v%s is not compatible with Terraform %s.
Provider version %s is the latest compatible version. Select it with the following version constraint:
	version = %q

Terraform checked all of the plugin versions matching the given constraint:
	%s

Consult the documentation for this provider for more information on compatibility between provider and Terraform versions.
`

// providerProtocolTooNew is a message sent to the CLI UI if the provider's
// supported protocol versions are too new for the user's version of terraform,
// and the user could either upgrade terraform or choose an older version of the
// provider.
const providerProtocolTooNew = `Provider %q v%s is not compatible with Terraform %s.
You need to downgrade to v%s or earlier. Select it with the following constraint:
	version = %q

Terraform checked all of the plugin versions matching the given constraint:
	%s

Consult the documentation for this provider for more information on compatibility between provider and Terraform versions.
Alternatively, upgrade to the latest version of Terraform for compatibility with newer provider releases.
`

// No version of the provider is compatible.
const errProviderVersionIncompatible = `No compatible versions of provider %s were found.`

// incompleteLockFileInformationHeader is the summary displayed to users when
// the lock file has only recorded local hashes.
const incompleteLockFileInformationHeader = `Incomplete lock file information for providers`

// incompleteLockFileInformationBody is the body of text displayed to users when
// the lock file has only recorded local hashes.
const incompleteLockFileInformationBody = `Due to your customized provider installation methods, Terraform was forced to calculate lock file checksums locally for the following providers:
  - %s

The current .terraform.lock.hcl file only includes checksums for %s, so Terraform running on another platform will fail to install these providers.

To calculate additional checksums for another platform, run:
  terraform providers lock -platform=linux_amd64
(where linux_amd64 is the platform to generate)`
