//go:build !remote

package libpod

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"

	"github.com/containers/podman/v5/libpod/define"
	"github.com/sirupsen/logrus"
	"go.podman.io/common/pkg/config"
	"go.podman.io/storage/pkg/fileutils"
)

// Migrate stops the rootless pause process and performs any necessary database
// migrations that are required. It can also migrate all containers to a new OCI
// runtime, if requested.
func (r *Runtime) Migrate(newRuntime string, migrateDB bool) error {
	// Acquire the alive lock and hold it.
	// Ensures that we don't let other Podman commands run while we are
	// rewriting things in the DB.
	aliveLock, err := r.getRuntimeAliveLock()
	if err != nil {
		return fmt.Errorf("retrieving alive lock: %w", err)
	}
	aliveLock.Lock()
	defer aliveLock.Unlock()

	if !r.valid {
		return define.ErrRuntimeStopped
	}

	runningContainers, err := r.GetRunningContainers()
	if err != nil {
		return err
	}

	allCtrs, err := r.state.AllContainers(false)
	if err != nil {
		return err
	}

	logrus.Infof("Stopping all containers")
	for _, ctr := range runningContainers {
		fmt.Printf("stopped %s\n", ctr.ID())
		if err := ctr.Stop(); err != nil {
			return fmt.Errorf("cannot stop container %s: %w", ctr.ID(), err)
		}
	}

	// Did the user request a new runtime?
	runtimeChangeRequested := newRuntime != ""
	var requestedRuntime OCIRuntime
	if runtimeChangeRequested {
		runtime, exists := r.ociRuntimes[newRuntime]
		if !exists {
			return fmt.Errorf("change to runtime %q requested but no such runtime is defined: %w", newRuntime, define.ErrInvalidArg)
		}
		requestedRuntime = runtime
	}

	for _, ctr := range allCtrs {
		needsWrite := false

		// Reset pause process location
		oldLocation := filepath.Join(ctr.state.RunDir, "conmon.pid")
		if ctr.config.ConmonPidFile == oldLocation {
			logrus.Infof("Changing conmon PID file for %s", ctr.ID())
			ctr.config.ConmonPidFile = filepath.Join(ctr.config.StaticDir, "conmon.pid")
			needsWrite = true
		}

		// Reset runtime
		if runtimeChangeRequested && ctr.config.OCIRuntime != newRuntime {
			logrus.Infof("Resetting container %s runtime to runtime %s", ctr.ID(), newRuntime)
			ctr.config.OCIRuntime = newRuntime
			ctr.ociRuntime = requestedRuntime

			needsWrite = true
		}

		if needsWrite {
			if err := r.state.RewriteContainerConfig(ctr, ctr.config); err != nil {
				return fmt.Errorf("rewriting config for container %s: %w", ctr.ID(), err)
			}
		}
	}

	if migrateDB {
		if err := r.checkCanMigrate(); err != nil {
			switch {
			case errors.Is(err, errCannotMigrateSqlite):
				fmt.Printf("No migration is necessary: %v", err)
				return r.stopPauseProcess()
			case errors.Is(err, errCannotMigrateHardcodedBolt):
				logrus.Errorf("In containers.conf, database_backend is manually set to \"boltdb\" - comment this line out and run `podman system migrate --migrate-db` to complete migration to SQLite")
				return fmt.Errorf("unable to migrate to SQLite database as database backend manually set")
			default:
				return err
			}
		}

		if err := r.migrateDB(); err != nil {
			return fmt.Errorf("migrating database from BoltDB to SQLite: %w", err)
		}
	}

	return r.stopPauseProcess()
}

var (
	errCannotMigrateSqlite        = errors.New("backing database is already SQLite")
	errCannotMigrateHardcodedBolt = errors.New("database_backend in containers.conf is manually set to \"boltdb\"")
)

func (r *Runtime) checkCanMigrate() error {
	if r.state.Type() == "sqlite" {
		return errCannotMigrateSqlite
	}

	// Necessary as database configuration is overwritten when the state is set up.
	// So we need a completely new state from disk to see what the user set.
	newCfg, err := config.New(nil)
	if err != nil {
		return fmt.Errorf("reloading configuration to check database backend in use")
	}
	backend, err := config.ParseDBBackend(newCfg.Engine.DBBackend)
	if err != nil {
		return fmt.Errorf("invalid DB backend configured - please change containers.conf database_backend to \"sqlite\"")
	}
	if backend == config.DBBackendBoltDB {
		return errCannotMigrateHardcodedBolt
	}

	return nil
}

func (r *Runtime) migrateDB() error {
	// Does the SQLite state already exist?
	dbPath := sqliteStatePath(r)
	if err := fileutils.Exists(dbPath); err == nil {
		return fmt.Errorf("a SQLite database already exists at %s, refusing to overwrite", dbPath)
	}

	sqlState, err := NewSqliteState(r)
	if err != nil {
		return fmt.Errorf("creating SQLite database: %w", err)
	}

	// Recreate the cached paths in the new database
	if err := sqlState.ValidateDBConfig(r); err != nil {
		return fmt.Errorf("recreating config table: %w", err)
	}

	// Migrate volumes, then pods, then containers.
	// Containers must be last as the pods they are part of and volumes they use must already exist.
	allVolumes, err := r.state.AllVolumes()
	if err != nil {
		return fmt.Errorf("retrieving volumes from boltdb: %w", err)
	}
	for _, vol := range allVolumes {
		if err := sqlState.AddVolume(vol); err != nil {
			return err
		}
		if err := vol.update(); err != nil {
			return err
		}
		if err := sqlState.SaveVolume(vol); err != nil {
			return err
		}
	}

	allPods, err := r.state.AllPods()
	if err != nil {
		return fmt.Errorf("retrieving pods from boltdb: %w", err)
	}
	for _, pod := range allPods {
		if err := sqlState.AddPod(pod); err != nil {
			return err
		}
		if err := pod.updatePod(); err != nil {
			return err
		}
		if err := sqlState.SavePod(pod); err != nil {
			return err
		}
	}

	// Containers must be done as a graph due to dependencies.
	// The state will error if we add a container before its dependencies.
	allCtrs, err := r.state.AllContainers(true)
	if err != nil {
		return fmt.Errorf("retrieving containers from boltdb: %w", err)
	}

	// BoltDB doesn't actually populate container networks on initial pull
	// from the database, that needs to be done separately.
	for _, ctr := range allCtrs {
		ctrNetworks, err := r.state.GetNetworks(ctr)
		if err != nil {
			return err
		}
		ctr.config.Networks = ctrNetworks
	}

	graph, err := BuildContainerGraph(allCtrs)
	if err != nil {
		return err
	}

	ctrErrors := make(map[string]error)
	ctrsVisited := make(map[string]bool)

	for _, node := range graph.noDepNodes {
		migrateNodeDatabase(node, false, ctrErrors, ctrsVisited, sqlState)
	}
	var ctrError error
	for id, err := range ctrErrors {
		if ctrError != nil {
			logrus.Errorf("Migrating containers to SQLite: %v", ctrError)
		}
		ctrError = fmt.Errorf("migrating container %s: %w", id, err)
	}
	if ctrError != nil {
		return ctrError
	}

	boltState := r.state
	r.state = sqlState

	// Move the Bolt database so it is not reused, but preserve so data is not lost.
	boltDBPath := getBoltDBPath(r)
	newBoltDBPath := fmt.Sprintf("%s-old", boltDBPath)
	if err := os.Rename(boltDBPath, newBoltDBPath); err != nil {
		// Restore the old state. Migration has failed.
		r.state = boltState

		return fmt.Errorf("renaming old database %s to %s: %w", boltDBPath, newBoltDBPath, err)
	}
	fmt.Printf("Old database has been renamed to %s and will no longer be used\n", newBoltDBPath)

	// Only after we handle all failure cases that could require a revert.
	boltState.Close()

	return nil
}
