package createconfig

import (
	"os"
	"sort"
	"strconv"
	"strings"
	"syscall"

	"github.com/containers/image/v5/manifest"
	"github.com/containers/libpod/libpod"
	"github.com/containers/libpod/libpod/define"
	"github.com/containers/libpod/pkg/namespaces"
	"github.com/containers/storage"
	"github.com/docker/go-connections/nat"
	spec "github.com/opencontainers/runtime-spec/specs-go"
	"github.com/opencontainers/runtime-tools/generate"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
)

// Type constants
const (
	bps = iota
	iops
)

// CreateResourceConfig represents resource elements in CreateConfig
// structures
type CreateResourceConfig struct {
	BlkioWeight       uint16   // blkio-weight
	BlkioWeightDevice []string // blkio-weight-device
	CPUPeriod         uint64   // cpu-period
	CPUQuota          int64    // cpu-quota
	CPURtPeriod       uint64   // cpu-rt-period
	CPURtRuntime      int64    // cpu-rt-runtime
	CPUShares         uint64   // cpu-shares
	CPUs              float64  // cpus
	CPUsetCPUs        string
	CPUsetMems        string   // cpuset-mems
	DeviceReadBps     []string // device-read-bps
	DeviceReadIOps    []string // device-read-iops
	DeviceWriteBps    []string // device-write-bps
	DeviceWriteIOps   []string // device-write-iops
	DisableOomKiller  bool     // oom-kill-disable
	KernelMemory      int64    // kernel-memory
	Memory            int64    //memory
	MemoryReservation int64    // memory-reservation
	MemorySwap        int64    //memory-swap
	MemorySwappiness  int      // memory-swappiness
	OomScoreAdj       int      //oom-score-adj
	PidsLimit         int64    // pids-limit
	ShmSize           int64
	Ulimit            []string //ulimit
}

// PidConfig configures the pid namespace for the container
type PidConfig struct {
	PidMode namespaces.PidMode //pid
}

// IpcConfig configures the ipc namespace for the container
type IpcConfig struct {
	IpcMode namespaces.IpcMode //ipc
}

// CgroupConfig configures the cgroup namespace for the container
type CgroupConfig struct {
	Cgroups      string
	Cgroupns     string
	CgroupParent string                // cgroup-parent
	CgroupMode   namespaces.CgroupMode //cgroup
}

// UserConfig configures the user namespace for the container
type UserConfig struct {
	GroupAdd   []string // group-add
	IDMappings *storage.IDMappingOptions
	UsernsMode namespaces.UsernsMode //userns
	User       string                //user
}

// UtsConfig configures the uts namespace for the container
type UtsConfig struct {
	UtsMode  namespaces.UTSMode //uts
	NoHosts  bool
	HostAdd  []string //add-host
	Hostname string
}

// NetworkConfig configures the network namespace for the container
type NetworkConfig struct {
	DNSOpt       []string //dns-opt
	DNSSearch    []string //dns-search
	DNSServers   []string //dns
	ExposedPorts map[nat.Port]struct{}
	HTTPProxy    bool
	IP6Address   string                 //ipv6
	IPAddress    string                 //ip
	LinkLocalIP  []string               // link-local-ip
	MacAddress   string                 //mac-address
	NetMode      namespaces.NetworkMode //net
	Network      string                 //network
	NetworkAlias []string               //network-alias
	PortBindings nat.PortMap
	Publish      []string //publish
	PublishAll   bool     //publish-all
}

// SeccompPolicy determines which seccomp profile gets applied to the container.
type SeccompPolicy int

const (
	// SeccompPolicyDefault - if set use SecurityConfig.SeccompProfilePath,
	// otherwise use the default profile.  The SeccompProfilePath might be
	// explicitly set by the user.
	SeccompPolicyDefault SeccompPolicy = iota
	// SeccompPolicyImage - if set use SecurityConfig.SeccompProfileFromImage,
	// otherwise follow SeccompPolicyDefault.
	SeccompPolicyImage
)

// Map for easy lookups of supported policies.
var supportedSeccompPolicies = map[string]SeccompPolicy{
	"":        SeccompPolicyDefault,
	"default": SeccompPolicyDefault,
	"image":   SeccompPolicyImage,
}

// LookupSeccompPolicy looksup the corresponding SeccompPolicy for the specified
// string. If none is found, an errors is returned including the list of
// supported policies.
// Note that an empty string resolved to SeccompPolicyDefault.
func LookupSeccompPolicy(s string) (SeccompPolicy, error) {
	policy, exists := supportedSeccompPolicies[s]
	if exists {
		return policy, nil
	}

	// Sort the keys first as maps are non-deterministic.
	keys := []string{}
	for k := range supportedSeccompPolicies {
		if k != "" {
			keys = append(keys, k)
		}
	}
	sort.Strings(keys)

	return -1, errors.Errorf("invalid seccomp policy %q: valid policies are %+q", s, keys)
}

// SecurityConfig configures the security features for the container
type SecurityConfig struct {
	CapAdd                  []string // cap-add
	CapDrop                 []string // cap-drop
	LabelOpts               []string //SecurityOpts
	NoNewPrivs              bool     //SecurityOpts
	ApparmorProfile         string   //SecurityOpts
	SeccompProfilePath      string   //SecurityOpts
	SeccompProfileFromImage string   // seccomp profile from the container image
	SeccompPolicy           SeccompPolicy
	SecurityOpts            []string
	Privileged              bool              //privileged
	ReadOnlyRootfs          bool              //read-only
	ReadOnlyTmpfs           bool              //read-only-tmpfs
	Sysctl                  map[string]string //sysctl
}

// CreateConfig is a pre OCI spec structure.  It represents user input from varlink or the CLI
type CreateConfig struct {
	Annotations       map[string]string
	Args              []string
	CidFile           string
	ConmonPidFile     string
	Command           []string          // Full command that will be used
	UserCommand       []string          // User-entered command (or image CMD)
	Detach            bool              // detach
	Devices           []string          // device
	Entrypoint        []string          //entrypoint
	Env               map[string]string //env
	HealthCheck       *manifest.Schema2HealthConfig
	Init              bool   // init
	InitPath          string //init-path
	Image             string
	ImageID           string
	BuiltinImgVolumes map[string]struct{} // volumes defined in the image config
	ImageVolumeType   string              // how to handle the image volume, either bind, tmpfs, or ignore
	Interactive       bool                //interactive
	Labels            map[string]string   //label
	LogDriver         string              // log-driver
	LogDriverOpt      []string            // log-opt
	Name              string              //name
	PodmanPath        string
	Pod               string //pod
	Quiet             bool   //quiet
	Resources         CreateResourceConfig
	RestartPolicy     string
	Rm                bool           //rm
	StopSignal        syscall.Signal // stop-signal
	StopTimeout       uint           // stop-timeout
	Systemd           bool
	Tmpfs             []string // tmpfs
	Tty               bool     //tty
	Mounts            []spec.Mount
	MountsFlag        []string // mounts
	NamedVolumes      []*libpod.ContainerNamedVolume
	Volumes           []string //volume
	VolumesFrom       []string
	WorkDir           string //workdir
	Rootfs            string
	Security          SecurityConfig
	Syslog            bool // Whether to enable syslog on exit commands

	// Namespaces
	Pid     PidConfig
	Ipc     IpcConfig
	Cgroup  CgroupConfig
	User    UserConfig
	Uts     UtsConfig
	Network NetworkConfig
}

func u32Ptr(i int64) *uint32     { u := uint32(i); return &u }
func fmPtr(i int64) *os.FileMode { fm := os.FileMode(i); return &fm }

// CreateBlockIO returns a LinuxBlockIO struct from a CreateConfig
func (c *CreateConfig) CreateBlockIO() (*spec.LinuxBlockIO, error) {
	return c.createBlockIO()
}

func (c *CreateConfig) createExitCommand(runtime *libpod.Runtime) ([]string, error) {
	config, err := runtime.GetConfig()
	if err != nil {
		return nil, err
	}

	// We need a cleanup process for containers in the current model.
	// But we can't assume that the caller is Podman - it could be another
	// user of the API.
	// As such, provide a way to specify a path to Podman, so we can
	// still invoke a cleanup process.
	cmd := c.PodmanPath
	if cmd == "" {
		cmd, _ = os.Executable()
	}

	command := []string{cmd,
		"--root", config.StorageConfig.GraphRoot,
		"--runroot", config.StorageConfig.RunRoot,
		"--log-level", logrus.GetLevel().String(),
		"--cgroup-manager", config.CgroupManager,
		"--tmpdir", config.TmpDir,
	}
	if config.OCIRuntime != "" {
		command = append(command, []string{"--runtime", config.OCIRuntime}...)
	}
	if config.StorageConfig.GraphDriverName != "" {
		command = append(command, []string{"--storage-driver", config.StorageConfig.GraphDriverName}...)
	}
	for _, opt := range config.StorageConfig.GraphDriverOptions {
		command = append(command, []string{"--storage-opt", opt}...)
	}
	if config.EventsLogger != "" {
		command = append(command, []string{"--events-backend", config.EventsLogger}...)
	}

	if c.Syslog {
		command = append(command, "--syslog", "true")
	}
	command = append(command, []string{"container", "cleanup"}...)

	if c.Rm {
		command = append(command, "--rm")
	}

	return command, nil
}

// GetContainerCreateOptions takes a CreateConfig and returns a slice of CtrCreateOptions
func (c *CreateConfig) getContainerCreateOptions(runtime *libpod.Runtime, pod *libpod.Pod, mounts []spec.Mount, namedVolumes []*libpod.ContainerNamedVolume) ([]libpod.CtrCreateOption, error) {
	var options []libpod.CtrCreateOption
	var err error

	if c.Interactive {
		options = append(options, libpod.WithStdin())
	}
	if c.Systemd {
		options = append(options, libpod.WithSystemd())
	}
	if c.Name != "" {
		logrus.Debugf("setting container name %s", c.Name)
		options = append(options, libpod.WithName(c.Name))
	}
	if c.Pod != "" {
		logrus.Debugf("adding container to pod %s", c.Pod)
		options = append(options, runtime.WithPod(pod))
	}

	if len(mounts) != 0 || len(namedVolumes) != 0 {
		destinations := []string{}

		// Take all mount and named volume destinations.
		for _, mount := range mounts {
			destinations = append(destinations, mount.Destination)
		}
		for _, volume := range namedVolumes {
			destinations = append(destinations, volume.Dest)
		}

		options = append(options, libpod.WithUserVolumes(destinations))
	}

	if len(namedVolumes) != 0 {
		options = append(options, libpod.WithNamedVolumes(namedVolumes))
	}

	if len(c.UserCommand) != 0 {
		options = append(options, libpod.WithCommand(c.UserCommand))
	}

	// Add entrypoint unconditionally
	// If it's empty it's because it was explicitly set to "" or the image
	// does not have one
	options = append(options, libpod.WithEntrypoint(c.Entrypoint))

	// TODO: MNT, USER, CGROUP
	options = append(options, libpod.WithStopSignal(c.StopSignal))
	options = append(options, libpod.WithStopTimeout(c.StopTimeout))

	logPath, logTag := getLoggingOpts(c.LogDriverOpt)
	if logPath != "" {
		options = append(options, libpod.WithLogPath(logPath))
	}
	if logTag != "" {
		options = append(options, libpod.WithLogTag(logTag))
	}

	if c.LogDriver != "" {
		options = append(options, libpod.WithLogDriver(c.LogDriver))
	}

	secOpts, err := c.Security.ToCreateOptions()
	if err != nil {
		return nil, err
	}
	options = append(options, secOpts...)

	nsOpts, err := c.Cgroup.ToCreateOptions(runtime)
	if err != nil {
		return nil, err
	}
	options = append(options, nsOpts...)

	nsOpts, err = c.Ipc.ToCreateOptions(runtime)
	if err != nil {
		return nil, err
	}
	options = append(options, nsOpts...)

	nsOpts, err = c.Pid.ToCreateOptions(runtime)
	if err != nil {
		return nil, err
	}
	options = append(options, nsOpts...)

	nsOpts, err = c.Network.ToCreateOptions(runtime, &c.User)
	if err != nil {
		return nil, err
	}
	options = append(options, nsOpts...)

	nsOpts, err = c.Uts.ToCreateOptions(runtime, pod)
	if err != nil {
		return nil, err
	}
	options = append(options, nsOpts...)

	nsOpts, err = c.User.ToCreateOptions(runtime)
	if err != nil {
		return nil, err
	}
	options = append(options, nsOpts...)

	useImageVolumes := c.ImageVolumeType == TypeBind
	// Gather up the options for NewContainer which consist of With... funcs
	options = append(options, libpod.WithRootFSFromImage(c.ImageID, c.Image, useImageVolumes))
	options = append(options, libpod.WithConmonPidFile(c.ConmonPidFile))
	options = append(options, libpod.WithLabels(c.Labels))
	options = append(options, libpod.WithShmSize(c.Resources.ShmSize))
	if c.Rootfs != "" {
		options = append(options, libpod.WithRootFS(c.Rootfs))
	}
	// Default used if not overridden on command line

	if c.RestartPolicy != "" {
		if c.RestartPolicy == "unless-stopped" {
			return nil, errors.Wrapf(define.ErrInvalidArg, "the unless-stopped restart policy is not supported")
		}

		split := strings.Split(c.RestartPolicy, ":")
		if len(split) > 1 {
			numTries, err := strconv.Atoi(split[1])
			if err != nil {
				return nil, errors.Wrapf(err, "%s is not a valid number of retries for restart policy", split[1])
			}
			if numTries < 0 {
				return nil, errors.Wrapf(define.ErrInvalidArg, "restart policy requires a positive number of retries")
			}
			options = append(options, libpod.WithRestartRetries(uint(numTries)))
		}
		options = append(options, libpod.WithRestartPolicy(split[0]))
	}

	// Always use a cleanup process to clean up Podman after termination
	exitCmd, err := c.createExitCommand(runtime)
	if err != nil {
		return nil, err
	}
	options = append(options, libpod.WithExitCommand(exitCmd))

	if c.HealthCheck != nil {
		options = append(options, libpod.WithHealthCheck(c.HealthCheck))
		logrus.Debugf("New container has a health check")
	}
	return options, nil
}

// AddPrivilegedDevices iterates through host devices and adds all
// host devices to the spec
func (c *CreateConfig) AddPrivilegedDevices(g *generate.Generator) error {
	return c.addPrivilegedDevices(g)
}
