// Package migrate provides the ability to bootstrap remote state backend.
package migrate

import (
	"context"
	"fmt"

	"github.com/gruntwork-io/terragrunt/internal/configbridge"
	"github.com/gruntwork-io/terragrunt/internal/runner"
	"github.com/gruntwork-io/terragrunt/internal/runner/run"
	"github.com/gruntwork-io/terragrunt/internal/venv"

	"errors"

	"github.com/gruntwork-io/terragrunt/internal/remotestate/backend"
	"github.com/gruntwork-io/terragrunt/internal/util"
	"github.com/gruntwork-io/terragrunt/pkg/config"
	"github.com/gruntwork-io/terragrunt/pkg/log"
	"github.com/gruntwork-io/terragrunt/pkg/options"
)

// Run migrates Terraform/OpenTofu state from srcPath to dstPath. v is the
// virtualized environment used for the underlying stack runner build and
// the state pull/push terraform invocations.
func Run(
	ctx context.Context,
	l log.Logger,
	v venv.Venv,
	srcPath, dstPath string,
	opts *options.TerragruntOptions,
) error {
	var err error

	srcPath, err = util.CanonicalPath(srcPath, opts.WorkingDir)
	if err != nil {
		return err
	}

	l.Debugf("Source unit path %s", srcPath)

	dstPath, err = util.CanonicalPath(dstPath, opts.WorkingDir)
	if err != nil {
		return err
	}

	l.Debugf("Destination unit path %s", dstPath)

	rnr, err := runner.NewStackRunner(ctx, l, run.FromRoot(v), opts)
	if err != nil {
		return err
	}

	srcUnit := rnr.GetStack().FindUnitByPath(srcPath)
	if srcUnit == nil {
		return fmt.Errorf("src unit not found at %s", srcPath)
	}

	dstUnit := rnr.GetStack().FindUnitByPath(dstPath)
	if dstUnit == nil {
		return fmt.Errorf("dst unit not found at %s", dstPath)
	}

	srcOpts, _, err := runner.BuildUnitOpts(l, opts, srcUnit)
	if err != nil {
		return fmt.Errorf("failed to build opts for src unit %s: %w", srcPath, err)
	}

	dstOpts, _, err := runner.BuildUnitOpts(l, opts, dstUnit)
	if err != nil {
		return fmt.Errorf("failed to build opts for dst unit %s: %w", dstPath, err)
	}

	_, srcPctx := configbridge.NewParsingContext(ctx, l, srcOpts)
	srcPctx.Venv = v

	srcRemoteState, err := config.ParseRemoteState(ctx, l, srcPctx)
	if err != nil {
		return err
	}

	if srcRemoteState == nil {
		return fmt.Errorf("missing remote state configuration for source module: %s", srcPath)
	}

	// ParseRemoteState updates pctx.WorkingDir to point to the .terragrunt-cache
	// directory (where backend.tf and .terraform/ live) when a terraform source is
	// configured. Propagate that back so pullState runs in the correct directory.
	srcOpts.WorkingDir = srcPctx.WorkingDir

	_, dstPctx := configbridge.NewParsingContext(ctx, l, dstOpts)
	dstPctx.Venv = v

	dstRemoteState, err := config.ParseRemoteState(ctx, l, dstPctx)
	if err != nil {
		return err
	}

	if dstRemoteState == nil {
		return fmt.Errorf("missing remote state configuration for destination module: %s", dstPath)
	}

	// Same for the destination: pushState needs the cache directory.
	dstOpts.WorkingDir = dstPctx.WorkingDir

	if !opts.ForceBackendMigrate {
		enabled, err := srcRemoteState.IsVersionControlEnabled(ctx, l, configbridge.RemoteStateOptsFromOpts(srcOpts))
		if err != nil && !errors.As(err, new(backend.BucketDoesNotExistError)) {
			return err
		}

		if !enabled {
			return fmt.Errorf(
				"src bucket is not versioned, refusing to migrate backend state."+
					" If you are sure you want to migrate the backend state anyways, use the --%s flag",
				ForceBackendMigrateFlagName)
		}
	}

	return srcRemoteState.Migrate(
		ctx, l,
		v.Exec,
		configbridge.RemoteStateOptsFromOpts(srcOpts),
		configbridge.RemoteStateOptsFromOpts(dstOpts),
		dstRemoteState,
	)
}
