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

package command

import (
	"context"
	"fmt"
	"path/filepath"
	"strings"

	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/terraform/internal/addrs"
	"github.com/hashicorp/terraform/internal/command/arguments"
	"github.com/hashicorp/terraform/internal/command/clistate"
	"github.com/hashicorp/terraform/internal/command/views"
	"github.com/hashicorp/terraform/internal/command/workdir"
	"github.com/hashicorp/terraform/internal/configs"
	"github.com/hashicorp/terraform/internal/depsfile"
	"github.com/hashicorp/terraform/internal/getproviders"
	"github.com/hashicorp/terraform/internal/getproviders/providerreqs"
	"github.com/hashicorp/terraform/internal/providercache"
	"github.com/hashicorp/terraform/internal/tfdiags"
)

// StateMigrateCommand is a Command implementation that migrates
// the state file from one location to another
type StateMigrateCommand struct {
	Meta

	// incompleteProviders is necessary here to coordinate separate
	// provider installation and lock file update processes.
	incompleteProviders []string
}

func (c *StateMigrateCommand) Run(rawArgs []string) int {
	// Parse and apply global view arguments
	common, rawArgs := arguments.ParseView(rawArgs)
	c.Meta.View.Configure(common)

	args, diags := arguments.ParseStateMigrate(rawArgs)

	view := views.NewStateMigrate(args.ViewType, c.View)

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

	// FIXME: the -input flag value is needed but there is no clear path to pass
	// this value down, so we continue to mutate the Meta object state for now.
	c.Meta.input = args.InputEnabled

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

	// return validation errors early if there are any
	if diags.HasErrors() {
		view.Diagnostics(diags)
		return 1
	}

	c.Meta.includeStateMigrateFiles = true
	dir := c.Meta.WorkingDir.RootModuleDir()
	cfg, mDiags := c.Meta.loadConfig(dir)
	if mDiags.HasErrors() {
		diags = diags.Append(mDiags)
		view.Diagnostics(diags)
		return 1
	}
	if cfg.Module.StateStore != nil {
		cfg.Module.StateStore.ProviderSupplyMode = c.getProviderSupplyModeForStateStore(cfg.Module)
	}

	smi := cfg.Module.StateMigrationInstructions
	if smi == nil {
		diags = diags.Append(tfdiags.Sourceless(
			tfdiags.Error,
			"No state migration instructions found",
			"No instructions were found in the configuration files. Please ensure that a file with a .tfmigrate.hcl extension is present and contains valid state migration instructions.",
		))
		view.Diagnostics(diags)
		return 1
	}

	// When configuring the source and destination backends,
	// we prepare options for the migration action.
	//
	// If input is disabled, we "force" the migration. This stops the migration logic shared
	// with the init command failing when it discovers that input is disabled.
	// In "terraform init" the force field is controlled by the -force-copy flag.
	// In contrast, "terraform state migrate" assumes that if input is disabled the user wants to force the migration.
	migrateOpts := &backendMigrateOpts{
		ViewType: args.ViewType,
		force:    args.ForceCopy,
	}
	c.Meta.forceInitCopy = args.ForceCopy // backendMigrateOpts.force is overwritten with this value, so we also need to set it.

	// Load the source backend
	var source string
	var sourceLock *depsfile.Locks // This should only contain a single lock, if non nil. Used to avoid re-download if destination provider is the same.
	if smi.Backend != nil {
		source = fmt.Sprintf("backend %q", smi.Backend.Type)

		srcB, _, srcDiags := c.Meta.backendInitFromConfig(smi.Backend)
		diags = diags.Append(srcDiags)
		if !diags.HasErrors() {
			migrateOpts.SourceType = smi.Backend.Type
			migrateOpts.Source = srcB
		}
	} else if smi.StateStore != nil {
		source = fmt.Sprintf("state store %q (%s)", smi.StateStore.Type,
			smi.StateStore.ProviderAddr.ForDisplay())

		// Load any pre-existing source provider lock file.
		var lockfilePath string
		if args.SourceLockFilePath != "" {
			lockfilePath = args.SourceLockFilePath
		} else {
			lockfilePath = dependencyLockFilename // default
		}
		srcLocks, srcLockDiags := c.readLockedDependenciesFromPath(lockfilePath)
		diags = diags.Append(srcLockDiags)
		if srcLockDiags.HasErrors() {
			view.Diagnostics(diags)
			return 1
		}

		upgrade := false // The first provider download step will never be an upgrade. Either it's constrained by a preexisting lock or there is no lock.
		var srcProviderDiags tfdiags.Diagnostics
		var output bool
		output, sourceLock, srcProviderDiags = c.getSingleProvider(ctx, smi.StateStore.Type, smi.StateStoreProvider.Requirement, srcLocks, upgrade, MigrationSource, view)
		diags = diags.Append(srcProviderDiags)
		if srcProviderDiags.HasErrors() {
			view.Diagnostics(diags)
			return 1
		}
		if output {
			// Space out provider download output from the migration output below.
			view.Spacer()
		}

		srcB, _, _, srcDiags := c.Meta.stateStoreInitFromConfig(smi.StateStore, sourceLock)
		diags = diags.Append(srcDiags)
		if !diags.HasErrors() {
			migrateOpts.SourceType = smi.StateStore.Type
			migrateOpts.Source = srcB
		}
	}

	// Load the destination backend
	rootMod := cfg.Module
	var destination string
	var destinationLock *depsfile.Locks                               // This should only contain a single lock, if non nil. Used to update the dependency lock file on disk.
	var bsf *workdir.BackendStateFile = workdir.NewBackendStateFile() // Collect data below for updating the backend state file.
	if rootMod.Backend != nil {
		destination = fmt.Sprintf("backend %q", rootMod.Backend.Type)

		dstB, dstConfig, dstDiags := c.Meta.backendInitFromConfig(rootMod.Backend)
		diags = diags.Append(dstDiags)
		if !diags.HasErrors() {
			// Assign migration options for the migration action later in the command
			migrateOpts.DestinationType = rootMod.Backend.Type
			migrateOpts.Destination = dstB

			// Capture details of the destination backend for updating the backend state file after a successful migration.
			_, cHash, bcDiags := c.backendConfig(&BackendOpts{
				BackendConfig: rootMod.Backend,
			})
			diags = diags.Append(bcDiags)
			if bcDiags.HasErrors() {
				view.Diagnostics(diags)
				return 1
			}
			bsf.Backend = &workdir.BackendConfigState{
				Type: rootMod.Backend.Type,
				Hash: uint64(cHash),
			}
			err := bsf.Backend.SetConfig(dstConfig, dstB.ConfigSchema())
			if err != nil {
				diags = diags.Append(fmt.Errorf("Can't serialize backend configuration as JSON: %s", err))
				view.Diagnostics(diags)
				return 1
			}
		}
	} else if rootMod.StateStore != nil {
		destination = fmt.Sprintf("state store %q (%s)", rootMod.StateStore.Type,
			rootMod.StateStore.ProviderAddr.ForDisplay())

		// Get single required_providers entry for state store provider.
		dstReq, dstReqDiags := c.getDestinationStateStoreProviderRequirements(rootMod.StateStore.ProviderAddr, rootMod.ProviderRequirements)
		diags = diags.Append(dstReqDiags)
		if dstReqDiags.HasErrors() {
			view.Diagnostics(diags)
			return 1
		}

		// Load any pre-existing destination provider lock file.
		var lockfilePath string
		if args.DestinationLockFilePath != "" {
			lockfilePath = args.DestinationLockFilePath
		} else {
			lockfilePath = dependencyLockFilename // default
		}
		dstLocks, dstLockDiags := c.readLockedDependenciesFromPath(lockfilePath)
		diags = diags.Append(dstLockDiags)
		if dstLockDiags.HasErrors() {
			view.Diagnostics(diags)
			return 1
		}

		// The source provider download step may have introduced a new lock that can be re-used here.
		// Else, this download step could re-download the same provider if the migration is between stores
		// in the same provider.
		//
		// TODO: Make this conditional based on whether we're doing an upgrade or not?
		//       Or is use of upgrade flag in second download sufficient?
		var mergedLocks *depsfile.Locks
		if sourceLock != nil {
			mergedLocks = c.mergeLockedDependencies(dstLocks, sourceLock)
		} else {
			mergedLocks = dstLocks
		}

		// Perform download of the destination provider.
		// This may be controlled by a pre-existing lock from above or not, therefore the returned
		// lock for the destination state store may not already be in the lock file.
		//
		// We only pass in a single required provider, so we expect a single lock to be
		// returned. This will be added the dependency lock file after a successful migration.
		upgrade := false // TODO - control this by -upgrade flag
		var dstProviderDiags tfdiags.Diagnostics
		var output bool
		output, destinationLock, dstProviderDiags = c.getSingleProvider(ctx, rootMod.StateStore.Type, dstReq, mergedLocks, upgrade, MigrationDestination, view)
		diags = diags.Append(dstProviderDiags)
		if dstProviderDiags.HasErrors() {
			view.Diagnostics(diags)
			return 1
		}
		if output {
			// Space out provider download output from the migration output below.
			view.Spacer()
		}

		dstB, stateStoreConfigVal, providerConfigVal, dstDiags := c.Meta.stateStoreInitFromConfig(rootMod.StateStore, destinationLock)
		diags = diags.Append(dstDiags)
		if !diags.HasErrors() {
			migrateOpts.DestinationType = rootMod.StateStore.Type
			migrateOpts.Destination = dstB
		}

		// Capture details of the destination state store for updating the backend state file after a successful migration.
		_, cHash, sscDiags := c.stateStoreConfig(&BackendOpts{
			StateStoreConfig: rootMod.StateStore,
			Locks:            destinationLock,
		})
		diags = diags.Append(sscDiags)
		if sscDiags.HasErrors() {
			view.Diagnostics(diags)
			return 1
		}
		v := destinationLock.Provider(rootMod.StateStore.ProviderAddr).Version() // We just downloaded this provider, so the lock wil be present.
		version, err := providerreqs.GoVersionFromVersion(v)
		if err != nil {
			diags = diags.Append(fmt.Errorf("Failed to convert provider version to Go version: %s", err))
			view.Diagnostics(diags)
			return 1
		}

		bsf.StateStore = &workdir.StateStoreConfigState{
			Type: rootMod.StateStore.Type,
			Hash: uint64(cHash),
			Provider: &workdir.ProviderConfigState{
				Source:  &rootMod.StateStore.ProviderAddr,
				Version: version,
			},
			ProviderSupplyMode: rootMod.StateStore.ProviderSupplyMode,
		}
		err = bsf.StateStore.SetConfig(stateStoreConfigVal, dstB.ConfigSchema())
		if err != nil {
			diags = diags.Append(fmt.Errorf("Failed to set state store configuration: %w", err))
			view.Diagnostics(diags)
			return 1
		}

		err = bsf.StateStore.Provider.SetConfig(providerConfigVal, dstB.ProviderSchema())
		if err != nil {
			diags = diags.Append(fmt.Errorf("Failed to set state store provider configuration: %w", err))
			view.Diagnostics(diags)
			return 1
		}

	} else {
		diags = diags.Append(tfdiags.Sourceless(
			tfdiags.Error,
			"Unknown migration destination",
			"No configuration was provided for where to migrate the state to. Please ensure that a file with a .tf extension is present and contains valid state_store or backend configuration inside the terraform block.",
		))
	}

	// present all errors from above together so user can fix them all at once
	if diags.HasErrors() {
		view.Diagnostics(diags)
		return 1
	}

	view.Log(views.StateMigrationStartMessage, source, destination)

	// Perform the migration from source to destination
	err := c.Meta.backendMigrateState(migrateOpts)
	if err != nil {
		diags = diags.Append(fmt.Errorf("State migration failed: %w", err))
		view.Diagnostics(diags)
		view.Log(views.StateMigrationFailureMessage, source, destination)
		return 1
	}

	// After a successful migration to a state store, we must make sure the dependency lock file contains the
	// details of the destination state store provider.
	if rootMod.StateStore != nil {
		originalLocks, originalLockDiags := c.lockedDependencies()
		diags = diags.Append(originalLockDiags)
		if originalLockDiags.HasErrors() {
			view.Diagnostics(diags)
			return 1
		}

		// Get the combination of locks
		//
		// Take the lock from the destination provider download and add in the original locks from the dependency lock file.
		// This means the lock created from the destination provider download is authoritative (e.g. any hashes from the
		// installation process are preserved)
		newLocks := c.mergeLockedDependencies(destinationLock, originalLocks)

		output, depLockFileDiags := c.saveDependencyLockFile(originalLocks, newLocks, view)
		diags = diags.Append(depLockFileDiags)
		if depLockFileDiags.HasErrors() {
			view.Diagnostics(diags)
			view.Log(views.StateMigrationPostStepsInterruptedMessage, source, destination)
			return 1
		}
		if output {
			// Space out lock file creation/update output from the final outputs below.
			view.Spacer()
		}
	}

	// Success, so update backend state file to reflect the new location state is stored in.
	bsfDiags := c.updateBackendStateFile(bsf)
	diags = diags.Append(bsfDiags)
	if bsfDiags.HasErrors() {
		view.Diagnostics(diags)
		view.Log(views.StateMigrationPostStepsInterruptedMessage, source, destination)
		return 1
	}

	view.Diagnostics(diags) // Log any warnings

	view.Log(views.StateMigrationCompletedMessage, source, destination)

	return 0
}

func (c *StateMigrateCommand) Help() string {
	helpText := `
Usage: terraform [global options] state migrate [options]

  Migrate state from source declared in the migration configuration (*.tfmigrate.hcl)
  to the destination declared in the root module (*.tf).

  An error will be returned if the migration fails, e.g. if the state
  is inaccessible or the migration configuration is invalid.

Options:

  -source-provider-lock-file       Path to a provider lock file for the source provider (requires -input=false).
                                   Defaults to using the working directory's .terraform.lock.hcl file.

  -destination-provider-lock-file  Path to a provider lock file for the destination provider (requires -input=false).
                                   Defaults to using the working directory's .terraform.lock.hcl file.

  -upgrade                         Trigger upgrade of the provider used for state storage.

  -input=true                      Enable input for interactive prompts (defaults to true, set to false in automation).
`
	return strings.TrimSpace(helpText)
}

func (c *StateMigrateCommand) Synopsis() string {
	return "Migrate the state from one location to another"
}

const (
	MigrationSource      = "source"
	MigrationDestination = "destination"
)

func (c *StateMigrateCommand) updateBackendStateFile(s *workdir.BackendStateFile) tfdiags.Diagnostics {
	var diags tfdiags.Diagnostics

	statePath := filepath.Join(c.DataDir(), DefaultStateFilename)
	sMgr := &clistate.LocalState{Path: statePath}
	if err := sMgr.RefreshState(); err != nil {
		diags = diags.Append(fmt.Errorf("Failed to load the backend state file when preparing to update it: %s", err))
		return diags
	}

	if err := sMgr.WriteState(s); err != nil {
		diags = diags.Append(errBackendWriteSavedDiag(err))
		return diags
	}
	if err := sMgr.PersistState(); err != nil {
		diags = diags.Append(errBackendWriteSavedDiag(err))
		return diags
	}

	return diags
}

func (c *StateMigrateCommand) getDestinationStateStoreProviderRequirements(provider addrs.Provider, configReqs *configs.RequiredProviders) (providerreqs.Requirements, tfdiags.Diagnostics) {
	var diags tfdiags.Diagnostics
	req := make(providerreqs.Requirements, 1)

	if configReqs == nil {
		panic(fmt.Sprintf("expected one provider requirement for the destination state store provider %q, but received empty data about required providers.", provider))
	}

	for _, providerReq := range configReqs.RequiredProviders {
		if providerReq.Type.Equals(provider) {
			con, err := providerreqs.ParseVersionConstraints(providerReq.Requirement.Required.String())
			if err != nil {
				diags = diags.Append(&hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Invalid version constraint syntax for state store provider",
					// The errors returned by ParseVersionConstraint already include
					// the section of input that was incorrect, so we don't need to
					// include that here.
					Detail:  fmt.Sprintf("Incorrect version constraint syntax: %s.", err.Error()),
					Subject: providerReq.Requirement.DeclRange.Ptr(),
				})
			}
			req[providerReq.Type] = con
		}
	}
	if len(req) != 1 {
		panic(fmt.Sprintf("expected exactly one provider requirement for the destination state store provider %q, got %d", provider, len(req)))
	}

	return req, diags
}

// saveDependencyLockFile overwrites the contents of the dependency lock file.
func (c *StateMigrateCommand) saveDependencyLockFile(previousLocks, newLocks *depsfile.Locks, view views.StateMigrate) (output bool, diags tfdiags.Diagnostics) {
	// The state migrate command does not support the -lockfile=readonly flag
	// This flag is specific to the init command, and can only take "" or "readonly" as values.
	// As state migrate doesn't take this flag, we can safely set it to "" here.
	flagLockfile := ""

	return c.Meta.saveDependencyLockFile(previousLocks, newLocks, c.incompleteProviders, flagLockfile, view)
}

// getSingleProvider is used to download the source and/or destination state store providers during a state migration.
// Download of the up to 2 providers is kept separate due to:
// - Potential for downloading different versions of the same provider
// - Need to keep the locks separate for source and destination providers; destination providers are added to the dependency lock file.
func (c *StateMigrateCommand) getSingleProvider(ctx context.Context, storeName string, reqs providerreqs.Requirements, locks *depsfile.Locks, upgrade bool, location string, view views.StateMigrate) (output bool, resultingLock *depsfile.Locks, diags tfdiags.Diagnostics) {
	ctx, span := tracer.Start(ctx, "install state migration "+location+" provider")
	defer span.End()

	// We expect to download only one provider
	if len(reqs) != 1 {
		panic(fmt.Sprintf("expected exactly one provider requirement for the destination state store provider, got %d", len(reqs)))
	}

	// Check for legacy provider addresses.
	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
	}

	// Use a source that looks for providers in all of the standard locations,
	// possibly customized by the user in CLI config.
	inst := c.providerInstaller()

	// 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.LogInitializingStateStoreProviderPlugin(storeName)
		},
		ProviderAlreadyInstalled: providerAlreadyInstalledCallback(view),
		BuiltInProviderAvailable: builtInProviderAvailableCallback(view),
		BuiltInProviderFailure:   builtInProviderFailureCallback(&diags),
		QueryPackagesBegin: func(provider addrs.Provider, versionConstraints getproviders.VersionConstraints, locked bool) {
			if locked {
				view.LogReusingPreviousProviderVersion(provider)
			} else {
				if len(versionConstraints) > 0 {
					view.LogFindingMatchingVersion(provider, versionConstraints)
				} else {
					view.LogFindingLatestVersion(provider)
				}
			}
		},
		LinkFromCacheBegin:   linkFromCacheBeginCallback(view),
		FetchPackageBegin:    fetchPackageBeginCallback(view),
		QueryPackagesFailure: queryPackagesFailureCallback(&diags, ctx, inst.ProviderSource(), reqs, nil),
		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
	}

	newLocks, err := inst.EnsureProviderVersions(ctx, locks, reqs, mode)
	if ctx.Err() == context.Canceled {
		diags = diags.Append(fmt.Errorf("Provider installation was canceled by an interrupt signal."))
		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
}
