package types

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"github.com/BurntSushi/toml"
	cfg "github.com/containers/storage/pkg/config"
	"github.com/containers/storage/pkg/idtools"
	"github.com/sirupsen/logrus"
)

// TOML-friendly explicit tables used for conversions.
type TomlConfig struct {
	Storage struct {
		Driver              string            `toml:"driver,omitempty"`
		RunRoot             string            `toml:"runroot,omitempty"`
		GraphRoot           string            `toml:"graphroot,omitempty"`
		RootlessStoragePath string            `toml:"rootless_storage_path,omitempty"`
		Options             cfg.OptionsConfig `toml:"options,omitempty"`
	} `toml:"storage"`
}

const (
	overlayDriver  = "overlay"
	overlay2       = "overlay2"
	storageConfEnv = "CONTAINERS_STORAGE_CONF"
)

var (
	defaultStoreOptionsOnce    sync.Once
	loadDefaultStoreOptionsErr error
)

func loadDefaultStoreOptions() {
	defaultStoreOptions.RunRoot = defaultRunRoot
	defaultStoreOptions.GraphRoot = defaultGraphRoot
	defaultStoreOptions.GraphDriverName = ""

	if path, ok := os.LookupEnv(storageConfEnv); ok {
		defaultOverrideConfigFile = path
		if err := ReloadConfigurationFileIfNeeded(path, &defaultStoreOptions); err != nil {
			loadDefaultStoreOptionsErr = err
			return
		}
	} else if _, err := os.Stat(defaultOverrideConfigFile); err == nil {
		// The DefaultConfigFile(rootless) function returns the path
		// of the used storage.conf file, by returning defaultConfigFile
		// If override exists containers/storage uses it by default.
		defaultConfigFile = defaultOverrideConfigFile
		if err := ReloadConfigurationFileIfNeeded(defaultOverrideConfigFile, &defaultStoreOptions); err != nil {
			loadDefaultStoreOptionsErr = err
			return
		}
	} else {
		if !os.IsNotExist(err) {
			logrus.Warningf("Attempting to use %s, %v", defaultConfigFile, err)
		}
		if err := ReloadConfigurationFileIfNeeded(defaultConfigFile, &defaultStoreOptions); err != nil && !errors.Is(err, os.ErrNotExist) {
			loadDefaultStoreOptionsErr = err
			return
		}
	}
	// reload could set values to empty for run and graph root if config does not contains anything
	if defaultStoreOptions.RunRoot == "" {
		defaultStoreOptions.RunRoot = defaultRunRoot
	}
	if defaultStoreOptions.GraphRoot == "" {
		defaultStoreOptions.GraphRoot = defaultGraphRoot
	}
}

// defaultStoreOptionsIsolated is an internal implementation detail of DefaultStoreOptions to allow testing.
// Everyone but the tests this is intended for should only call DefaultStoreOptions, never this function.
func defaultStoreOptionsIsolated(rootless bool, rootlessUID int, storageConf string) (StoreOptions, error) {
	var (
		defaultRootlessRunRoot   string
		defaultRootlessGraphRoot string
		err                      error
	)
	defaultStoreOptionsOnce.Do(loadDefaultStoreOptions)
	if loadDefaultStoreOptionsErr != nil {
		return StoreOptions{}, loadDefaultStoreOptionsErr
	}
	storageOpts := defaultStoreOptions
	if rootless && rootlessUID != 0 {
		storageOpts, err = getRootlessStorageOpts(rootlessUID, storageOpts)
		if err != nil {
			return storageOpts, err
		}
	}
	_, err = os.Stat(storageConf)
	if err != nil && !os.IsNotExist(err) {
		return storageOpts, err
	}
	if err == nil && !defaultConfigFileSet {
		defaultRootlessRunRoot = storageOpts.RunRoot
		defaultRootlessGraphRoot = storageOpts.GraphRoot
		storageOpts = StoreOptions{}
		reloadConfigurationFileIfNeeded(storageConf, &storageOpts)
		if rootless && rootlessUID != 0 {
			// If the file did not specify a graphroot or runroot,
			// set sane defaults so we don't try and use root-owned
			// directories
			if storageOpts.RunRoot == "" {
				storageOpts.RunRoot = defaultRootlessRunRoot
			}
			if storageOpts.GraphRoot == "" {
				if storageOpts.RootlessStoragePath != "" {
					storageOpts.GraphRoot = storageOpts.RootlessStoragePath
				} else {
					storageOpts.GraphRoot = defaultRootlessGraphRoot
				}
			}
		}
	}
	if storageOpts.RunRoot != "" {
		runRoot, err := expandEnvPath(storageOpts.RunRoot, rootlessUID)
		if err != nil {
			return storageOpts, err
		}
		storageOpts.RunRoot = runRoot
	}
	if storageOpts.GraphRoot != "" {
		graphRoot, err := expandEnvPath(storageOpts.GraphRoot, rootlessUID)
		if err != nil {
			return storageOpts, err
		}
		storageOpts.GraphRoot = graphRoot
	}
	if storageOpts.RootlessStoragePath != "" {
		storagePath, err := expandEnvPath(storageOpts.RootlessStoragePath, rootlessUID)
		if err != nil {
			return storageOpts, err
		}
		storageOpts.RootlessStoragePath = storagePath
	}

	return storageOpts, nil
}

// DefaultStoreOptions returns the default storage ops for containers
func DefaultStoreOptions(rootless bool, rootlessUID int) (StoreOptions, error) {
	storageConf, err := DefaultConfigFile(rootless && rootlessUID != 0)
	if err != nil {
		return defaultStoreOptions, err
	}
	return defaultStoreOptionsIsolated(rootless, rootlessUID, storageConf)
}

// StoreOptions is used for passing initialization options to GetStore(), for
// initializing a Store object and the underlying storage that it controls.
type StoreOptions struct {
	// RunRoot is the filesystem path under which we can store run-time
	// information, such as the locations of active mount points, that we
	// want to lose if the host is rebooted.
	RunRoot string `json:"runroot,omitempty"`
	// GraphRoot is the filesystem path under which we will store the
	// contents of layers, images, and containers.
	GraphRoot string `json:"root,omitempty"`
	// RootlessStoragePath is the storage path for rootless users
	// default $HOME/.local/share/containers/storage
	RootlessStoragePath string `toml:"rootless_storage_path"`
	// GraphDriverName is the underlying storage driver that we'll be
	// using.  It only needs to be specified the first time a Store is
	// initialized for a given RunRoot and GraphRoot.
	GraphDriverName string `json:"driver,omitempty"`
	// GraphDriverOptions are driver-specific options.
	GraphDriverOptions []string `json:"driver-options,omitempty"`
	// UIDMap and GIDMap are used for setting up a container's root filesystem
	// for use inside of a user namespace where UID mapping is being used.
	UIDMap []idtools.IDMap `json:"uidmap,omitempty"`
	GIDMap []idtools.IDMap `json:"gidmap,omitempty"`
	// RootAutoNsUser is the user used to pick a subrange when automatically setting
	// a user namespace for the root user.
	RootAutoNsUser string `json:"root_auto_ns_user,omitempty"`
	// AutoNsMinSize is the minimum size for an automatic user namespace.
	AutoNsMinSize uint32 `json:"auto_userns_min_size,omitempty"`
	// AutoNsMaxSize is the maximum size for an automatic user namespace.
	AutoNsMaxSize uint32 `json:"auto_userns_max_size,omitempty"`
	// PullOptions specifies options to be handed to pull managers
	// This API is experimental and can be changed without bumping the major version number.
	PullOptions map[string]string `toml:"pull_options"`
	// DisableVolatile doesn't allow volatile mounts when it is set.
	DisableVolatile bool `json:"disable-volatile,omitempty"`
}

// isRootlessDriver returns true if the given storage driver is valid for containers running as non root
func isRootlessDriver(driver string) bool {
	validDrivers := map[string]bool{
		"btrfs":    true,
		"overlay":  true,
		"overlay2": true,
		"vfs":      true,
	}
	return validDrivers[driver]
}

// getRootlessStorageOpts returns the storage opts for containers running as non root
func getRootlessStorageOpts(rootlessUID int, systemOpts StoreOptions) (StoreOptions, error) {
	var opts StoreOptions

	dataDir, rootlessRuntime, err := getRootlessDirInfo(rootlessUID)
	if err != nil {
		return opts, err
	}
	opts.RunRoot = rootlessRuntime
	opts.PullOptions = systemOpts.PullOptions
	if systemOpts.RootlessStoragePath != "" {
		opts.GraphRoot, err = expandEnvPath(systemOpts.RootlessStoragePath, rootlessUID)
		if err != nil {
			return opts, err
		}
	} else {
		opts.GraphRoot = filepath.Join(dataDir, "containers", "storage")
	}

	if driver := systemOpts.GraphDriverName; isRootlessDriver(driver) {
		opts.GraphDriverName = driver
	}
	if driver := os.Getenv("STORAGE_DRIVER"); driver != "" {
		opts.GraphDriverName = driver
	}
	if opts.GraphDriverName == overlay2 {
		logrus.Warnf("Switching default driver from overlay2 to the equivalent overlay driver")
		opts.GraphDriverName = overlayDriver
	}

	// If the configuration file was explicitly set, then copy all the options
	// present.
	if defaultConfigFileSet {
		opts.GraphDriverOptions = systemOpts.GraphDriverOptions
	} else if opts.GraphDriverName == overlayDriver {
		for _, o := range systemOpts.GraphDriverOptions {
			if strings.Contains(o, "ignore_chown_errors") {
				opts.GraphDriverOptions = append(opts.GraphDriverOptions, o)
				break
			}
		}
	}
	if opts.GraphDriverName == "" {
		opts.GraphDriverName = "vfs"
	}

	if os.Getenv("STORAGE_OPTS") != "" {
		opts.GraphDriverOptions = append(opts.GraphDriverOptions, strings.Split(os.Getenv("STORAGE_OPTS"), ",")...)
	}

	return opts, nil
}

// DefaultStoreOptionsAutoDetectUID returns the default storage ops for containers
func DefaultStoreOptionsAutoDetectUID() (StoreOptions, error) {
	uid := getRootlessUID()
	return DefaultStoreOptions(uid != 0, uid)
}

var prevReloadConfig = struct {
	storeOptions *StoreOptions
	mod          time.Time
	mutex        sync.Mutex
	configFile   string
}{}

// SetDefaultConfigFilePath sets the default configuration to the specified path
func SetDefaultConfigFilePath(path string) error {
	defaultConfigFile = path
	defaultConfigFileSet = true
	return ReloadConfigurationFileIfNeeded(defaultConfigFile, &defaultStoreOptions)
}

func ReloadConfigurationFileIfNeeded(configFile string, storeOptions *StoreOptions) error {
	prevReloadConfig.mutex.Lock()
	defer prevReloadConfig.mutex.Unlock()

	fi, err := os.Stat(configFile)
	if err != nil {
		return err
	}

	mtime := fi.ModTime()
	if prevReloadConfig.storeOptions != nil && prevReloadConfig.mod == mtime && prevReloadConfig.configFile == configFile {
		*storeOptions = *prevReloadConfig.storeOptions
		return nil
	}

	if err := ReloadConfigurationFile(configFile, storeOptions); err != nil {
		return err
	}

	cOptions := *storeOptions
	prevReloadConfig.storeOptions = &cOptions
	prevReloadConfig.mod = mtime
	prevReloadConfig.configFile = configFile
	return nil
}

// ReloadConfigurationFile parses the specified configuration file and overrides
// the configuration in storeOptions.
func ReloadConfigurationFile(configFile string, storeOptions *StoreOptions) error {
	config := new(TomlConfig)

	meta, err := toml.DecodeFile(configFile, &config)
	if err == nil {
		keys := meta.Undecoded()
		if len(keys) > 0 {
			logrus.Warningf("Failed to decode the keys %q from %q", keys, configFile)
		}
	} else {
		if !os.IsNotExist(err) {
			fmt.Printf("Failed to read %s %v\n", configFile, err.Error())
			return err
		}
	}

	// Clear storeOptions of previous settings
	*storeOptions = StoreOptions{}
	if config.Storage.Driver != "" {
		storeOptions.GraphDriverName = config.Storage.Driver
	}
	if os.Getenv("STORAGE_DRIVER") != "" {
		config.Storage.Driver = os.Getenv("STORAGE_DRIVER")
		storeOptions.GraphDriverName = config.Storage.Driver
	}
	if storeOptions.GraphDriverName == overlay2 {
		logrus.Warnf("Switching default driver from overlay2 to the equivalent overlay driver")
		storeOptions.GraphDriverName = overlayDriver
	}
	if storeOptions.GraphDriverName == "" {
		logrus.Errorf("The storage 'driver' option must be set in %s to guarantee proper operation", configFile)
	}
	if config.Storage.RunRoot != "" {
		storeOptions.RunRoot = config.Storage.RunRoot
	}
	if config.Storage.GraphRoot != "" {
		storeOptions.GraphRoot = config.Storage.GraphRoot
	}
	if config.Storage.RootlessStoragePath != "" {
		storeOptions.RootlessStoragePath = config.Storage.RootlessStoragePath
	}
	for _, s := range config.Storage.Options.AdditionalImageStores {
		storeOptions.GraphDriverOptions = append(storeOptions.GraphDriverOptions, fmt.Sprintf("%s.imagestore=%s", config.Storage.Driver, s))
	}
	for _, s := range config.Storage.Options.AdditionalLayerStores {
		storeOptions.GraphDriverOptions = append(storeOptions.GraphDriverOptions, fmt.Sprintf("%s.additionallayerstore=%s", config.Storage.Driver, s))
	}
	if config.Storage.Options.Size != "" {
		storeOptions.GraphDriverOptions = append(storeOptions.GraphDriverOptions, fmt.Sprintf("%s.size=%s", config.Storage.Driver, config.Storage.Options.Size))
	}
	if config.Storage.Options.MountProgram != "" {
		storeOptions.GraphDriverOptions = append(storeOptions.GraphDriverOptions, fmt.Sprintf("%s.mount_program=%s", config.Storage.Driver, config.Storage.Options.MountProgram))
	}
	if config.Storage.Options.SkipMountHome != "" {
		storeOptions.GraphDriverOptions = append(storeOptions.GraphDriverOptions, fmt.Sprintf("%s.skip_mount_home=%s", config.Storage.Driver, config.Storage.Options.SkipMountHome))
	}
	if config.Storage.Options.IgnoreChownErrors != "" {
		storeOptions.GraphDriverOptions = append(storeOptions.GraphDriverOptions, fmt.Sprintf("%s.ignore_chown_errors=%s", config.Storage.Driver, config.Storage.Options.IgnoreChownErrors))
	}
	if config.Storage.Options.ForceMask != 0 {
		storeOptions.GraphDriverOptions = append(storeOptions.GraphDriverOptions, fmt.Sprintf("%s.force_mask=%o", config.Storage.Driver, config.Storage.Options.ForceMask))
	}
	if config.Storage.Options.MountOpt != "" {
		storeOptions.GraphDriverOptions = append(storeOptions.GraphDriverOptions, fmt.Sprintf("%s.mountopt=%s", config.Storage.Driver, config.Storage.Options.MountOpt))
	}
	if config.Storage.Options.RemapUser != "" && config.Storage.Options.RemapGroup == "" {
		config.Storage.Options.RemapGroup = config.Storage.Options.RemapUser
	}
	if config.Storage.Options.RemapGroup != "" && config.Storage.Options.RemapUser == "" {
		config.Storage.Options.RemapUser = config.Storage.Options.RemapGroup
	}
	if config.Storage.Options.RemapUser != "" && config.Storage.Options.RemapGroup != "" {
		mappings, err := idtools.NewIDMappings(config.Storage.Options.RemapUser, config.Storage.Options.RemapGroup)
		if err != nil {
			fmt.Printf("Error initializing ID mappings for %s:%s %v\n", config.Storage.Options.RemapUser, config.Storage.Options.RemapGroup, err)
			return err
		}
		storeOptions.UIDMap = mappings.UIDs()
		storeOptions.GIDMap = mappings.GIDs()
	}

	uidmap, err := idtools.ParseIDMap([]string{config.Storage.Options.RemapUIDs}, "remap-uids")
	if err != nil {
		return err
	}
	gidmap, err := idtools.ParseIDMap([]string{config.Storage.Options.RemapGIDs}, "remap-gids")
	if err != nil {
		return err
	}

	storeOptions.UIDMap = uidmap
	storeOptions.GIDMap = gidmap
	storeOptions.RootAutoNsUser = config.Storage.Options.RootAutoUsernsUser
	if config.Storage.Options.AutoUsernsMinSize > 0 {
		storeOptions.AutoNsMinSize = config.Storage.Options.AutoUsernsMinSize
	}
	if config.Storage.Options.AutoUsernsMaxSize > 0 {
		storeOptions.AutoNsMaxSize = config.Storage.Options.AutoUsernsMaxSize
	}
	if config.Storage.Options.PullOptions != nil {
		storeOptions.PullOptions = config.Storage.Options.PullOptions
	}

	storeOptions.DisableVolatile = config.Storage.Options.DisableVolatile

	storeOptions.GraphDriverOptions = append(storeOptions.GraphDriverOptions, cfg.GetGraphDriverOptions(storeOptions.GraphDriverName, config.Storage.Options)...)

	if opts, ok := os.LookupEnv("STORAGE_OPTS"); ok {
		storeOptions.GraphDriverOptions = strings.Split(opts, ",")
	}
	if len(storeOptions.GraphDriverOptions) == 1 && storeOptions.GraphDriverOptions[0] == "" {
		storeOptions.GraphDriverOptions = nil
	}
	return nil
}

func Options() (StoreOptions, error) {
	defaultStoreOptionsOnce.Do(loadDefaultStoreOptions)
	return defaultStoreOptions, loadDefaultStoreOptionsErr
}

// Save overwrites the tomlConfig in storage.conf with the given conf
func Save(conf TomlConfig, rootless bool) error {
	configFile, err := DefaultConfigFile(rootless)
	if err != nil {
		return err
	}

	if err = os.Remove(configFile); !os.IsNotExist(err) && err != nil {
		return err
	}

	f, err := os.Create(configFile)
	if err != nil {
		return err
	}

	return toml.NewEncoder(f).Encode(conf)
}

// StorageConfig is used to retrieve the storage.conf toml in order to overwrite it
func StorageConfig(rootless bool) (*TomlConfig, error) {
	config := new(TomlConfig)

	configFile, err := DefaultConfigFile(rootless)
	if err != nil {
		return nil, err
	}

	_, err = toml.DecodeFile(configFile, &config)
	if err != nil {
		return nil, err
	}

	return config, nil
}
