// +build ABISupport

package infra

import (
	"context"
	"fmt"
	"os"

	"github.com/containers/libpod/libpod"
	"github.com/containers/libpod/pkg/cgroups"
	"github.com/containers/libpod/pkg/domain/entities"
	"github.com/containers/libpod/pkg/namespaces"
	"github.com/containers/libpod/pkg/rootless"
	"github.com/containers/storage"
	"github.com/containers/storage/pkg/idtools"
	"github.com/pkg/errors"
	flag "github.com/spf13/pflag"
)

type engineOpts struct {
	name     string
	renumber bool
	migrate  bool
	noStore  bool
	withFDS  bool
	config   *entities.PodmanConfig
}

// GetRuntimeMigrate gets a libpod runtime that will perform a migration of existing containers
func GetRuntimeMigrate(ctx context.Context, fs *flag.FlagSet, cfg *entities.PodmanConfig, newRuntime string) (*libpod.Runtime, error) {
	return getRuntime(ctx, fs, &engineOpts{
		name:     newRuntime,
		renumber: false,
		migrate:  true,
		noStore:  false,
		withFDS:  true,
		config:   cfg,
	})
}

// GetRuntimeDisableFDs gets a libpod runtime that will disable sd notify
func GetRuntimeDisableFDs(ctx context.Context, fs *flag.FlagSet, cfg *entities.PodmanConfig) (*libpod.Runtime, error) {
	return getRuntime(ctx, fs, &engineOpts{
		renumber: false,
		migrate:  false,
		noStore:  false,
		withFDS:  false,
		config:   cfg,
	})
}

// GetRuntimeRenumber gets a libpod runtime that will perform a lock renumber
func GetRuntimeRenumber(ctx context.Context, fs *flag.FlagSet, cfg *entities.PodmanConfig) (*libpod.Runtime, error) {
	return getRuntime(ctx, fs, &engineOpts{
		renumber: true,
		migrate:  false,
		noStore:  false,
		withFDS:  true,
		config:   cfg,
	})
}

// GetRuntime generates a new libpod runtime configured by command line options
func GetRuntime(ctx context.Context, flags *flag.FlagSet, cfg *entities.PodmanConfig) (*libpod.Runtime, error) {
	return getRuntime(ctx, flags, &engineOpts{
		renumber: false,
		migrate:  false,
		noStore:  false,
		withFDS:  true,
		config:   cfg,
	})
}

// GetRuntimeNoStore generates a new libpod runtime configured by command line options
func GetRuntimeNoStore(ctx context.Context, fs *flag.FlagSet, cfg *entities.PodmanConfig) (*libpod.Runtime, error) {
	return getRuntime(ctx, fs, &engineOpts{
		renumber: false,
		migrate:  false,
		noStore:  true,
		withFDS:  true,
		config:   cfg,
	})
}

func getRuntime(ctx context.Context, fs *flag.FlagSet, opts *engineOpts) (*libpod.Runtime, error) {
	options := []libpod.RuntimeOption{}
	storageOpts := storage.StoreOptions{}
	cfg := opts.config

	storageSet := false

	uidmapFlag := fs.Lookup("uidmap")
	gidmapFlag := fs.Lookup("gidmap")
	subuidname := fs.Lookup("subuidname")
	subgidname := fs.Lookup("subgidname")
	if (uidmapFlag != nil && gidmapFlag != nil && subuidname != nil && subgidname != nil) &&
		(uidmapFlag.Changed || gidmapFlag.Changed || subuidname.Changed || subgidname.Changed) {
		userns, _ := fs.GetString("userns")
		uidmapVal, _ := fs.GetStringSlice("uidmap")
		gidmapVal, _ := fs.GetStringSlice("gidmap")
		subuidVal, _ := fs.GetString("subuidname")
		subgidVal, _ := fs.GetString("subgidname")
		mappings, err := ParseIDMapping(namespaces.UsernsMode(userns), uidmapVal, gidmapVal, subuidVal, subgidVal)
		if err != nil {
			return nil, err
		}
		storageOpts.UIDMap = mappings.UIDMap
		storageOpts.GIDMap = mappings.GIDMap

		storageSet = true
	}

	if fs.Changed("root") {
		storageSet = true
		storageOpts.GraphRoot = cfg.Engine.StaticDir
	}
	if fs.Changed("runroot") {
		storageSet = true
		storageOpts.RunRoot = cfg.Runroot
	}
	if len(storageOpts.RunRoot) > 50 {
		return nil, errors.New("the specified runroot is longer than 50 characters")
	}
	if fs.Changed("storage-driver") {
		storageSet = true
		storageOpts.GraphDriverName = cfg.StorageDriver
		// Overriding the default storage driver caused GraphDriverOptions from storage.conf to be ignored
		storageOpts.GraphDriverOptions = []string{}
	}
	// This should always be checked after storage-driver is checked
	if len(cfg.StorageOpts) > 0 {
		storageSet = true
		storageOpts.GraphDriverOptions = cfg.StorageOpts
	}
	if opts.migrate {
		options = append(options, libpod.WithMigrate())
		if opts.name != "" {
			options = append(options, libpod.WithMigrateRuntime(opts.name))
		}
	}

	if opts.renumber {
		options = append(options, libpod.WithRenumber())
	}

	// Only set this if the user changes storage config on the command line
	if storageSet {
		options = append(options, libpod.WithStorageConfig(storageOpts))
	}

	if !storageSet && opts.noStore {
		options = append(options, libpod.WithNoStore())
	}
	// TODO CLI flags for image config?
	// TODO CLI flag for signature policy?

	if len(cfg.Engine.Namespace) > 0 {
		options = append(options, libpod.WithNamespace(cfg.Engine.Namespace))
	}

	if fs.Changed("runtime") {
		options = append(options, libpod.WithOCIRuntime(cfg.RuntimePath))
	}

	if fs.Changed("conmon") {
		options = append(options, libpod.WithConmonPath(cfg.ConmonPath))
	}
	if fs.Changed("tmpdir") {
		options = append(options, libpod.WithTmpDir(cfg.Engine.TmpDir))
	}
	if fs.Changed("network-cmd-path") {
		options = append(options, libpod.WithNetworkCmdPath(cfg.Engine.NetworkCmdPath))
	}

	if fs.Changed("events-backend") {
		options = append(options, libpod.WithEventsLogger(cfg.Engine.EventsLogger))
	}

	if fs.Changed("cgroup-manager") {
		options = append(options, libpod.WithCgroupManager(cfg.Engine.CgroupManager))
	} else {
		unified, err := cgroups.IsCgroup2UnifiedMode()
		if err != nil {
			return nil, err
		}
		if rootless.IsRootless() && !unified {
			options = append(options, libpod.WithCgroupManager("cgroupfs"))
		}
	}

	// TODO flag to set libpod static dir?
	// TODO flag to set libpod tmp dir?

	if fs.Changed("cni-config-dir") {
		options = append(options, libpod.WithCNIConfigDir(cfg.Network.NetworkConfigDir))
	}
	if fs.Changed("default-mounts-file") {
		options = append(options, libpod.WithDefaultMountsFile(cfg.Containers.DefaultMountsFile))
	}
	if fs.Changed("hooks-dir") {
		options = append(options, libpod.WithHooksDir(cfg.Engine.HooksDir...))
	}

	// TODO flag to set CNI plugins dir?

	// TODO I don't think these belong here?
	// Will follow up with a different PR to address
	//
	// Pod create options

	infraImageFlag := fs.Lookup("infra-image")
	if infraImageFlag != nil && infraImageFlag.Changed {
		infraImage, _ := fs.GetString("infra-image")
		options = append(options, libpod.WithDefaultInfraImage(infraImage))
	}

	infraCommandFlag := fs.Lookup("infra-command")
	if infraCommandFlag != nil && infraImageFlag.Changed {
		infraCommand, _ := fs.GetString("infra-command")
		options = append(options, libpod.WithDefaultInfraCommand(infraCommand))
	}

	if !opts.withFDS {
		options = append(options, libpod.WithEnableSDNotify())
	}
	return libpod.NewRuntime(ctx, options...)
}

// ParseIDMapping takes idmappings and subuid and subgid maps and returns a storage mapping
func ParseIDMapping(mode namespaces.UsernsMode, uidMapSlice, gidMapSlice []string, subUIDMap, subGIDMap string) (*storage.IDMappingOptions, error) {
	options := storage.IDMappingOptions{
		HostUIDMapping: true,
		HostGIDMapping: true,
	}

	if mode.IsAuto() {
		var err error
		options.HostUIDMapping = false
		options.HostGIDMapping = false
		options.AutoUserNs = true
		opts, err := mode.GetAutoOptions()
		if err != nil {
			return nil, err
		}
		options.AutoUserNsOpts = *opts
		return &options, nil
	}
	if mode.IsKeepID() {
		if len(uidMapSlice) > 0 || len(gidMapSlice) > 0 {
			return nil, errors.New("cannot specify custom mappings with --userns=keep-id")
		}
		if len(subUIDMap) > 0 || len(subGIDMap) > 0 {
			return nil, errors.New("cannot specify subuidmap or subgidmap with --userns=keep-id")
		}
		if rootless.IsRootless() {
			min := func(a, b int) int {
				if a < b {
					return a
				}
				return b
			}

			uid := rootless.GetRootlessUID()
			gid := rootless.GetRootlessGID()

			uids, gids, err := rootless.GetConfiguredMappings()
			if err != nil {
				return nil, errors.Wrapf(err, "cannot read mappings")
			}
			maxUID, maxGID := 0, 0
			for _, u := range uids {
				maxUID += u.Size
			}
			for _, g := range gids {
				maxGID += g.Size
			}

			options.UIDMap, options.GIDMap = nil, nil

			options.UIDMap = append(options.UIDMap, idtools.IDMap{ContainerID: 0, HostID: 1, Size: min(uid, maxUID)})
			options.UIDMap = append(options.UIDMap, idtools.IDMap{ContainerID: uid, HostID: 0, Size: 1})
			if maxUID > uid {
				options.UIDMap = append(options.UIDMap, idtools.IDMap{ContainerID: uid + 1, HostID: uid + 1, Size: maxUID - uid})
			}

			options.GIDMap = append(options.GIDMap, idtools.IDMap{ContainerID: 0, HostID: 1, Size: min(gid, maxGID)})
			options.GIDMap = append(options.GIDMap, idtools.IDMap{ContainerID: gid, HostID: 0, Size: 1})
			if maxGID > gid {
				options.GIDMap = append(options.GIDMap, idtools.IDMap{ContainerID: gid + 1, HostID: gid + 1, Size: maxGID - gid})
			}

			options.HostUIDMapping = false
			options.HostGIDMapping = false
		}
		// Simply ignore the setting and do not setup an inner namespace for root as it is a no-op
		return &options, nil
	}

	if subGIDMap == "" && subUIDMap != "" {
		subGIDMap = subUIDMap
	}
	if subUIDMap == "" && subGIDMap != "" {
		subUIDMap = subGIDMap
	}
	if len(gidMapSlice) == 0 && len(uidMapSlice) != 0 {
		gidMapSlice = uidMapSlice
	}
	if len(uidMapSlice) == 0 && len(gidMapSlice) != 0 {
		uidMapSlice = gidMapSlice
	}
	if len(uidMapSlice) == 0 && subUIDMap == "" && os.Getuid() != 0 {
		uidMapSlice = []string{fmt.Sprintf("0:%d:1", os.Getuid())}
	}
	if len(gidMapSlice) == 0 && subGIDMap == "" && os.Getuid() != 0 {
		gidMapSlice = []string{fmt.Sprintf("0:%d:1", os.Getgid())}
	}

	if subUIDMap != "" && subGIDMap != "" {
		mappings, err := idtools.NewIDMappings(subUIDMap, subGIDMap)
		if err != nil {
			return nil, err
		}
		options.UIDMap = mappings.UIDs()
		options.GIDMap = mappings.GIDs()
	}
	parsedUIDMap, err := idtools.ParseIDMap(uidMapSlice, "UID")
	if err != nil {
		return nil, err
	}
	parsedGIDMap, err := idtools.ParseIDMap(gidMapSlice, "GID")
	if err != nil {
		return nil, err
	}
	options.UIDMap = append(options.UIDMap, parsedUIDMap...)
	options.GIDMap = append(options.GIDMap, parsedGIDMap...)
	if len(options.UIDMap) > 0 {
		options.HostUIDMapping = false
	}
	if len(options.GIDMap) > 0 {
		options.HostGIDMapping = false
	}
	return &options, nil
}