package main

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/containers/buildah"
	"github.com/containers/libpod/cmd/podman/cliconfig"
	"github.com/containers/libpod/libpod"
	"github.com/containers/libpod/pkg/rootless"
	"github.com/containers/storage"
	"github.com/fatih/camelcase"
	jsoniter "github.com/json-iterator/go"
	"github.com/pkg/errors"
	"github.com/spf13/cobra"
)

var (
	stores = make(map[storage.Store]struct{})
	json   = jsoniter.ConfigCompatibleWithStandardLibrary
)

const (
	idTruncLength = 12
)

func splitCamelCase(src string) string {
	entries := camelcase.Split(src)
	return strings.Join(entries, " ")
}

func shortID(id string) string {
	if len(id) > idTruncLength {
		return id[:idTruncLength]
	}
	return id
}

// checkAllAndLatest checks that --all and --latest are used correctly
func checkAllAndLatest(c *cobra.Command, args []string, ignoreArgLen bool) error {
	argLen := len(args)
	if c.Flags().Lookup("all") == nil || c.Flags().Lookup("latest") == nil {
		return errors.New("unable to lookup values for 'latest' or 'all'")
	}
	all, _ := c.Flags().GetBool("all")
	latest, _ := c.Flags().GetBool("latest")
	if all && latest {
		return errors.Errorf("--all and --latest cannot be used together")
	}
	if ignoreArgLen {
		return nil
	}
	if (all || latest) && argLen > 0 {
		return errors.Errorf("no arguments are needed with --all or --latest")
	}
	if argLen < 1 && !all && !latest {
		return errors.Errorf("you must provide at least one name or id")
	}
	return nil
}

// noSubArgs checks that there are no further positional parameters
func noSubArgs(c *cobra.Command, args []string) error {
	if len(args) > 0 {
		return errors.Errorf("`%s` takes no arguments", c.CommandPath())
	}
	return nil
}

func commandRunE() func(*cobra.Command, []string) error {
	return func(cmd *cobra.Command, args []string) error {
		if len(args) > 0 {
			return errors.Errorf("unrecognized command `%s %s`\nTry '%s --help' for more information.", cmd.CommandPath(), args[0], cmd.CommandPath())
		} else {
			return errors.Errorf("missing command '%s COMMAND'\nTry '%s --help' for more information.", cmd.CommandPath(), cmd.CommandPath())
		}
	}
}

// getAllOrLatestContainers tries to return the correct list of containers
// depending if --all, --latest or <container-id> is used.
// It requires the Context (c) and the Runtime (runtime). As different
// commands are using different container state for the --all option
// the desired state has to be specified in filterState. If no filter
// is desired a -1 can be used to get all containers. For a better
// error message, if the filter fails, a corresponding verb can be
// specified which will then appear in the error message.
func getAllOrLatestContainers(c *cliconfig.PodmanCommand, runtime *libpod.Runtime, filterState libpod.ContainerStatus, verb string) ([]*libpod.Container, error) {
	var containers []*libpod.Container
	var lastError error
	var err error
	if c.Bool("all") {
		if filterState != -1 {
			var filterFuncs []libpod.ContainerFilter
			filterFuncs = append(filterFuncs, func(c *libpod.Container) bool {
				state, _ := c.State()
				return state == filterState
			})
			containers, err = runtime.GetContainers(filterFuncs...)
		} else {
			containers, err = runtime.GetContainers()
		}
		if err != nil {
			return nil, errors.Wrapf(err, "unable to get %s containers", verb)
		}
	} else if c.Bool("latest") {
		lastCtr, err := runtime.GetLatestContainer()
		if err != nil {
			return nil, errors.Wrapf(err, "unable to get latest container")
		}
		containers = append(containers, lastCtr)
	} else {
		args := c.InputArgs
		for _, i := range args {
			container, err := runtime.LookupContainer(i)
			if err != nil {
				if lastError != nil {
					fmt.Fprintln(os.Stderr, lastError)
				}
				lastError = errors.Wrapf(err, "unable to find container %s", i)
			}
			if container != nil {
				// This is here to make sure this does not return [<nil>] but only nil
				containers = append(containers, container)
			}
		}
	}

	return containers, lastError
}

// getContext returns a non-nil, empty context
func getContext() context.Context {
	if Ctx != nil {
		return Ctx
	}
	return context.TODO()
}

func getDefaultNetwork() string {
	if rootless.IsRootless() {
		return "slirp4netns"
	}
	return "bridge"
}

func getCreateFlags(c *cliconfig.PodmanCommand) {

	createFlags := c.Flags()

	createFlags.StringSlice(
		"add-host", []string{},
		"Add a custom host-to-IP mapping (host:ip) (default [])",
	)
	createFlags.StringSlice(
		"annotation", []string{},
		"Add annotations to container (key:value) (default [])",
	)
	createFlags.StringSliceP(
		"attach", "a", []string{},
		"Attach to STDIN, STDOUT or STDERR (default [])",
	)
	createFlags.String(
		"authfile", getAuthFile(""),
		"Path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override",
	)
	createFlags.String(
		"blkio-weight", "",
		"Block IO weight (relative weight) accepts a weight value between 10 and 1000.",
	)
	createFlags.StringSlice(
		"blkio-weight-device", []string{},
		"Block IO weight (relative device weight, format: `DEVICE_NAME:WEIGHT`)",
	)
	createFlags.StringSlice(
		"cap-add", []string{},
		"Add capabilities to the container",
	)
	createFlags.StringSlice(
		"cap-drop", []string{},
		"Drop capabilities from the container",
	)
	createFlags.String(
		"cgroup-parent", "",
		"Optional parent cgroup for the container",
	)
	createFlags.String(
		"cidfile", "",
		"Write the container ID to the file",
	)
	createFlags.String(
		"conmon-pidfile", "",
		"Path to the file that will receive the PID of conmon",
	)
	createFlags.Uint64(
		"cpu-period", 0,
		"Limit the CPU CFS (Completely Fair Scheduler) period",
	)
	createFlags.Int64(
		"cpu-quota", 0,
		"Limit the CPU CFS (Completely Fair Scheduler) quota",
	)
	createFlags.Uint64(
		"cpu-rt-period", 0,
		"Limit the CPU real-time period in microseconds",
	)
	createFlags.Int64(
		"cpu-rt-runtime", 0,
		"Limit the CPU real-time runtime in microseconds",
	)
	createFlags.Uint64(
		"cpu-shares", 0,
		"CPU shares (relative weight)",
	)
	createFlags.Float64(
		"cpus", 0,
		"Number of CPUs. The default is 0.000 which means no limit",
	)
	createFlags.String(
		"cpuset-cpus", "",
		"CPUs in which to allow execution (0-3, 0,1)",
	)
	createFlags.String(
		"cpuset-mems", "",
		"Memory nodes (MEMs) in which to allow execution (0-3, 0,1). Only effective on NUMA systems.",
	)
	createFlags.BoolP(
		"detach", "d", false,
		"Run container in background and print container ID",
	)
	createFlags.String(
		"detach-keys", "",
		"Override the key sequence for detaching a container. Format is a single character `[a-Z]` or `ctrl-<value>` where `<value>` is one of: `a-z`, `@`, `^`, `[`, `\\`, `]`, `^` or `_`",
	)
	createFlags.StringSlice(
		"device", []string{},
		"Add a host device to the container (default [])",
	)
	createFlags.StringSlice(
		"device-read-bps", []string{},
		"Limit read rate (bytes per second) from a device (e.g. --device-read-bps=/dev/sda:1mb)",
	)
	createFlags.StringSlice(
		"device-read-iops", []string{},
		"Limit read rate (IO per second) from a device (e.g. --device-read-iops=/dev/sda:1000)",
	)
	createFlags.StringSlice(
		"device-write-bps", []string{},
		"Limit write rate (bytes per second) to a device (e.g. --device-write-bps=/dev/sda:1mb)",
	)
	createFlags.StringSlice(
		"device-write-iops", []string{},
		"Limit write rate (IO per second) to a device (e.g. --device-write-iops=/dev/sda:1000)",
	)
	createFlags.StringSlice(
		"dns", []string{},
		"Set custom DNS servers",
	)
	createFlags.StringSlice(
		"dns-opt", []string{},
		"Set custom DNS options",
	)
	createFlags.StringSlice(
		"dns-search", []string{},
		"Set custom DNS search domains",
	)
	createFlags.String(
		"entrypoint", "",
		"Overwrite the default ENTRYPOINT of the image",
	)
	createFlags.StringArrayP(
		"env", "e", []string{},
		"Set environment variables in container",
	)
	createFlags.StringSlice(
		"env-file", []string{},
		"Read in a file of environment variables",
	)
	createFlags.StringSlice(
		"expose", []string{},
		"Expose a port or a range of ports (default [])",
	)
	createFlags.StringSlice(
		"gidmap", []string{},
		"GID map to use for the user namespace",
	)
	createFlags.StringSlice(
		"group-add", []string{},
		"Add additional groups to join (default [])",
	)
	createFlags.Bool(
		"help", false, "",
	)
	createFlags.String(
		"healthcheck-command", "",
		"set a healthcheck command for the container ('none' disables the existing healthcheck)",
	)
	createFlags.String(
		"healthcheck-interval", cliconfig.DefaultHealthCheckInterval,
		"set an interval for the healthchecks (a value of disable results in no automatic timer setup)",
	)
	createFlags.Uint(
		"healthcheck-retries", cliconfig.DefaultHealthCheckRetries,
		"the number of retries allowed before a healthcheck is considered to be unhealthy",
	)
	createFlags.String(
		"healthcheck-start-period", cliconfig.DefaultHealthCheckStartPeriod,
		"the initialization time needed for a container to bootstrap",
	)
	createFlags.String(
		"healthcheck-timeout", cliconfig.DefaultHealthCheckTimeout,
		"the maximum time allowed to complete the healthcheck before an interval is considered failed",
	)
	createFlags.StringP(
		"hostname", "h", "",
		"Set container hostname",
	)
	createFlags.Bool(
		"http-proxy", true,
		"Set proxy environment variables in the container based on the host proxy vars",
	)
	createFlags.String(
		"image-volume", cliconfig.DefaultImageVolume,
		"Tells podman how to handle the builtin image volumes. The options are: 'bind', 'tmpfs', or 'ignore'",
	)
	createFlags.Bool(
		"init", false,
		"Run an init binary inside the container that forwards signals and reaps processes",
	)
	createFlags.String(
		"init-path", "",
		// Do not use  the Value field for setting the default value to determine user input (i.e., non-empty string)
		fmt.Sprintf("Path to the container-init binary (default: %q)", libpod.DefaultInitPath),
	)
	createFlags.BoolP(
		"interactive", "i", false,
		"Keep STDIN open even if not attached",
	)
	createFlags.String(
		"ip", "",
		"Specify a static IPv4 address for the container",
	)
	createFlags.String(
		"ipc", "",
		"IPC namespace to use",
	)
	createFlags.String(
		"kernel-memory", "",
		"Kernel memory limit (format: `<number>[<unit>]`, where unit = b, k, m or g)",
	)
	createFlags.StringArrayP(
		"label", "l", []string{},
		"Set metadata on container (default [])",
	)
	createFlags.StringSlice(
		"label-file", []string{},
		"Read in a line delimited file of labels (default [])",
	)
	createFlags.String(
		"log-driver", "",
		"Logging driver for the container",
	)
	createFlags.StringSlice(
		"log-opt", []string{},
		"Logging driver options (default [])",
	)
	createFlags.String(
		"mac-address", "",
		"Container MAC address (e.g. 92:d0:c6:0a:29:33), not currently supported",
	)
	createFlags.StringP(
		"memory", "m", "",
		"Memory limit (format: <number>[<unit>], where unit = b, k, m or g)",
	)
	createFlags.String(
		"memory-reservation", "",
		"Memory soft limit (format: <number>[<unit>], where unit = b, k, m or g)",
	)
	createFlags.String(
		"memory-swap", "",
		"Swap limit equal to memory plus swap: '-1' to enable unlimited swap",
	)
	createFlags.Int64(
		"memory-swappiness", -1,
		"Tune container memory swappiness (0 to 100, or -1 for system default)",
	)
	createFlags.String(
		"name", "",
		"Assign a name to the container",
	)
	createFlags.String(
		"net", getDefaultNetwork(),
		"Connect a container to a network",
	)
	createFlags.String(
		"network", getDefaultNetwork(),
		"Connect a container to a network",
	)
	createFlags.Bool(
		"no-hosts", false,
		"Do not create /etc/hosts within the container, instead use the version from the image",
	)
	createFlags.Bool(
		"oom-kill-disable", false,
		"Disable OOM Killer",
	)
	createFlags.Int(
		"oom-score-adj", 0,
		"Tune the host's OOM preferences (-1000 to 1000)",
	)
	createFlags.String(
		"pid", "",
		"PID namespace to use",
	)
	createFlags.Int64(
		"pids-limit", 0,
		"Tune container pids limit (set -1 for unlimited)",
	)
	createFlags.String(
		"pod", "",
		"Run container in an existing pod",
	)
	createFlags.Bool(
		"privileged", false,
		"Give extended privileges to container",
	)
	createFlags.StringSliceP(
		"publish", "p", []string{},
		"Publish a container's port, or a range of ports, to the host (default [])",
	)
	createFlags.BoolP(
		"publish-all", "P", false,
		"Publish all exposed ports to random ports on the host interface",
	)
	createFlags.BoolP(
		"quiet", "q", false,
		"Suppress output information when pulling images",
	)
	createFlags.Bool(
		"read-only", false,
		"Make containers root filesystem read-only",
	)
	createFlags.Bool(
		"read-only-tmpfs", true,
		"When running containers in read-only mode mount a read-write tmpfs on /run, /tmp and /var/tmp",
	)
	createFlags.String(
		"restart", "",
		"Restart policy to apply when a container exits",
	)
	createFlags.Bool(
		"rm", false,
		"Remove container (and pod if created) after exit",
	)
	createFlags.Bool(
		"rootfs", false,
		"The first argument is not an image but the rootfs to the exploded container",
	)
	createFlags.StringArray(
		"security-opt", []string{},
		"Security Options (default [])",
	)
	createFlags.String(
		"shm-size", cliconfig.DefaultShmSize,
		"Size of `/dev/shm`. The format is `<number><unit>`",
	)
	createFlags.String(
		"stop-signal", "",
		"Signal to stop a container. Default is SIGTERM",
	)
	createFlags.Uint(
		"stop-timeout", libpod.CtrRemoveTimeout,
		"Timeout (in seconds) to stop a container. Default is 10",
	)
	createFlags.StringSlice(
		"storage-opt", []string{},
		"Storage driver options per container (default [])",
	)
	createFlags.String(
		"subgidname", "",
		"Name of range listed in /etc/subgid for use in user namespace",
	)
	createFlags.String(
		"subuidname", "",
		"Name of range listed in /etc/subuid for use in user namespace",
	)

	createFlags.StringSlice(
		"sysctl", []string{},
		"Sysctl options (default [])",
	)
	createFlags.Bool(
		"systemd", cliconfig.DefaultSystemD,
		"Run container in systemd mode if the command executable is systemd or init",
	)
	createFlags.StringArray(
		"tmpfs", []string{},
		"Mount a temporary filesystem (`tmpfs`) into a container (default [])",
	)
	createFlags.BoolP(
		"tty", "t", false,
		"Allocate a pseudo-TTY for container",
	)
	createFlags.StringSlice(
		"uidmap", []string{},
		"UID map to use for the user namespace",
	)
	createFlags.StringSlice(
		"ulimit", []string{},
		"Ulimit options (default [])",
	)
	createFlags.StringP(
		"user", "u", "",
		"Username or UID (format: <name|uid>[:<group|gid>])",
	)
	createFlags.String(
		"userns", os.Getenv("PODMAN_USERNS"),
		"User namespace to use",
	)
	createFlags.String(
		"uts", "",
		"UTS namespace to use",
	)
	createFlags.StringArray(
		"mount", []string{},
		"Attach a filesystem mount to the container (default [])",
	)
	createFlags.StringArrayP(
		"volume", "v", []string{},
		"Bind mount a volume into the container (default [])",
	)
	createFlags.StringSlice(
		"volumes-from", []string{},
		"Mount volumes from the specified container(s) (default [])",
	)
	createFlags.StringP(
		"workdir", "w", "",
		"Working directory inside the container",
	)
}

func getFormat(c *cliconfig.PodmanCommand) (string, error) {
	format := strings.ToLower(c.String("format"))
	if strings.HasPrefix(format, buildah.OCI) {
		return buildah.OCIv1ImageManifest, nil
	}

	if strings.HasPrefix(format, buildah.DOCKER) {
		return buildah.Dockerv2ImageManifest, nil
	}
	return "", errors.Errorf("unrecognized image type %q", format)
}

func getAuthFile(authfile string) string {
	if authfile != "" {
		return authfile
	}
	if remote {
		return ""
	}
	authfile = os.Getenv("REGISTRY_AUTH_FILE")
	if authfile != "" {
		return authfile
	}
	runtimeDir := os.Getenv("XDG_RUNTIME_DIR")
	if runtimeDir != "" {
		return filepath.Join(runtimeDir, "containers/auth.json")
	}
	return ""
}

// scrubServer removes 'http://' or 'https://' from the front of the
// server/registry string if either is there.  This will be mostly used
// for user input from 'podman login' and 'podman logout'.
func scrubServer(server string) string {
	server = strings.TrimPrefix(server, "https://")
	return strings.TrimPrefix(server, "http://")
}

// HelpTemplate returns the help template for podman commands
// This uses the short and long options.
// command should not use this.
func HelpTemplate() string {
	return `{{.Short}}

Description:
  {{.Long}}

{{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}`
}

// UsageTemplate returns the usage template for podman commands
// This blocks the desplaying of the global options. The main podman
// command should not use this.
func UsageTemplate() string {
	return `Usage:{{if (and .Runnable (not .HasAvailableSubCommands))}}
  {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}
  {{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}}

Aliases:
  {{.NameAndAliases}}{{end}}{{if .HasExample}}

Examples:
  {{.Example}}{{end}}{{if .HasAvailableSubCommands}}

Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
  {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}

Flags:
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
{{end}}
`
}
