package main

import (
	"fmt"
	"reflect"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/containers/buildah/pkg/formats"
	"github.com/containers/libpod/cmd/podman/cliconfig"
	"github.com/containers/libpod/cmd/podman/shared"
	"github.com/containers/libpod/libpod/define"
	"github.com/containers/libpod/pkg/adapter"
	"github.com/containers/libpod/pkg/util"
	"github.com/docker/go-units"
	"github.com/pkg/errors"
	"github.com/spf13/cobra"
)

const (
	STOPPED      = "Stopped" //nolint
	RUNNING      = "Running"
	PAUSED       = "Paused"
	EXITED       = "Exited"
	ERROR        = "Error"
	CREATED      = "Created"
	NUM_CTR_INFO = 10
)

type PodFilter func(*adapter.Pod) bool

var (
	bc_opts shared.PsOptions
)

type podPsCtrInfo struct {
	Name   string `json:"name,omitempty"`
	Id     string `json:"id,omitempty"`
	Status string `json:"status,omitempty"`
}

type podPsOptions struct {
	NoTrunc            bool
	Format             string
	Sort               string
	Quiet              bool
	NumberOfContainers bool
	Cgroup             bool
	NamesOfContainers  bool
	IdsOfContainers    bool
	StatusOfContainers bool
}

type podPsTemplateParams struct {
	Created            string
	ID                 string
	Name               string
	NumberOfContainers int
	Status             string
	Cgroup             string
	ContainerInfo      string
	InfraID            string
	Namespaces         string
}

// podPsJSONParams is used as a base structure for the psParams
// If template output is requested, podPsJSONParams will be converted to
// podPsTemplateParams.
// podPsJSONParams will be populated by data from libpod.Container,
// the members of the struct are the sama data types as their sources.
type podPsJSONParams struct {
	CreatedAt          time.Time      `json:"createdAt"`
	ID                 string         `json:"id"`
	Name               string         `json:"name"`
	NumberOfContainers int            `json:"numberOfContainers"`
	Status             string         `json:"status"`
	CtrsInfo           []podPsCtrInfo `json:"containerInfo,omitempty"`
	Cgroup             string         `json:"cgroup,omitempty"`
	InfraID            string         `json:"infraContainerId,omitempty"`
	Namespaces         []string       `json:"namespaces,omitempty"`
}

// Type declaration and functions for sorting the pod PS output
type podPsSorted []podPsJSONParams

func (a podPsSorted) Len() int      { return len(a) }
func (a podPsSorted) Swap(i, j int) { a[i], a[j] = a[j], a[i] }

type podPsSortedCreated struct{ podPsSorted }

func (a podPsSortedCreated) Less(i, j int) bool {
	return a.podPsSorted[i].CreatedAt.After(a.podPsSorted[j].CreatedAt)
}

type podPsSortedId struct{ podPsSorted }

func (a podPsSortedId) Less(i, j int) bool { return a.podPsSorted[i].ID < a.podPsSorted[j].ID }

type podPsSortedNumber struct{ podPsSorted }

func (a podPsSortedNumber) Less(i, j int) bool {
	return len(a.podPsSorted[i].CtrsInfo) < len(a.podPsSorted[j].CtrsInfo)
}

type podPsSortedName struct{ podPsSorted }

func (a podPsSortedName) Less(i, j int) bool { return a.podPsSorted[i].Name < a.podPsSorted[j].Name }

type podPsSortedStatus struct{ podPsSorted }

func (a podPsSortedStatus) Less(i, j int) bool {
	return a.podPsSorted[i].Status < a.podPsSorted[j].Status
}

var (
	podPsCommand cliconfig.PodPsValues

	podPsDescription = "List all pods on system including their names, ids and current state."
	_podPsCommand    = &cobra.Command{
		Use:     "ps",
		Aliases: []string{"ls", "list"},
		Args:    noSubArgs,
		Short:   "List pods",
		Long:    podPsDescription,
		RunE: func(cmd *cobra.Command, args []string) error {
			podPsCommand.InputArgs = args
			podPsCommand.GlobalFlags = MainGlobalOpts
			podPsCommand.Remote = remoteclient
			return podPsCmd(&podPsCommand)
		},
	}
)

func init() {
	podPsCommand.Command = _podPsCommand
	podPsCommand.SetHelpTemplate(HelpTemplate())
	podPsCommand.SetUsageTemplate(UsageTemplate())
	flags := podPsCommand.Flags()
	flags.BoolVar(&podPsCommand.CtrNames, "ctr-names", false, "Display the container names")
	flags.BoolVar(&podPsCommand.CtrIDs, "ctr-ids", false, "Display the container UUIDs. If no-trunc is not set they will be truncated")
	flags.BoolVar(&podPsCommand.CtrStatus, "ctr-status", false, "Display the container status")
	flags.StringVarP(&podPsCommand.Filter, "filter", "f", "", "Filter output based on conditions given")
	flags.StringVar(&podPsCommand.Format, "format", "", "Pretty-print pods to JSON or using a Go template")
	flags.BoolVarP(&podPsCommand.Latest, "latest", "l", false, "Act on the latest pod podman is aware of")
	flags.BoolVar(&podPsCommand.Namespace, "namespace", false, "Display namespace information of the pod")
	flags.BoolVar(&podPsCommand.Namespace, "ns", false, "Display namespace information of the pod")
	flags.BoolVar(&podPsCommand.NoTrunc, "no-trunc", false, "Do not truncate pod and container IDs")
	flags.BoolVarP(&podPsCommand.Quiet, "quiet", "q", false, "Print the numeric IDs of the pods only")
	flags.StringVar(&podPsCommand.Sort, "sort", "created", "Sort output by created, id, name, or number")
	markFlagHiddenForRemoteClient("latest", flags)
}

func podPsCmd(c *cliconfig.PodPsValues) error {
	if err := podPsCheckFlagsPassed(c); err != nil {
		return errors.Wrapf(err, "error with flags passed")
	}

	runtime, err := adapter.GetRuntime(getContext(), &c.PodmanCommand)
	if err != nil {
		return errors.Wrapf(err, "error creating libpod runtime")
	}
	defer runtime.DeferredShutdown(false)

	opts := podPsOptions{
		NoTrunc:            c.NoTrunc,
		Quiet:              c.Quiet,
		Sort:               c.Sort,
		IdsOfContainers:    c.CtrIDs,
		NamesOfContainers:  c.CtrNames,
		StatusOfContainers: c.CtrStatus,
	}

	opts.Format = genPodPsFormat(c)

	var filterFuncs []PodFilter
	if c.Filter != "" {
		filters := strings.Split(c.Filter, ",")
		for _, f := range filters {
			filterSplit := strings.Split(f, "=")
			if len(filterSplit) < 2 {
				return errors.Errorf("filter input must be in the form of filter=value: %s is invalid", f)
			}
			generatedFunc, err := generatePodFilterFuncs(filterSplit[0], filterSplit[1])
			if err != nil {
				return errors.Wrapf(err, "invalid filter")
			}
			filterFuncs = append(filterFuncs, generatedFunc)
		}
	}

	var pods []*adapter.Pod
	if c.Latest {
		pod, err := runtime.GetLatestPod()
		if err != nil {
			return err
		}
		pods = append(pods, pod)
	} else {
		pods, err = runtime.GetAllPods()
		if err != nil {
			return err
		}
	}

	podsFiltered := make([]*adapter.Pod, 0, len(pods))
	for _, pod := range pods {
		include := true
		for _, filter := range filterFuncs {
			include = include && filter(pod)
		}

		if include {
			podsFiltered = append(podsFiltered, pod)
		}
	}

	return generatePodPsOutput(podsFiltered, opts)
}

// podPsCheckFlagsPassed checks if mutually exclusive flags are passed together
func podPsCheckFlagsPassed(c *cliconfig.PodPsValues) error {
	// quiet, and format with Go template are mutually exclusive
	flags := 0
	if c.Quiet {
		flags++
	}
	if c.Flag("format").Changed && c.Format != formats.JSONString {
		flags++
	}
	if flags > 1 {
		return errors.Errorf("quiet and format with Go template are mutually exclusive")
	}
	return nil
}

func generatePodFilterFuncs(filter, filterValue string) (func(pod *adapter.Pod) bool, error) {
	switch filter {
	case "ctr-ids":
		return func(p *adapter.Pod) bool {
			ctrIds, err := p.AllContainersByID()
			if err != nil {
				return false
			}
			return util.StringInSlice(filterValue, ctrIds)
		}, nil
	case "ctr-names":
		return func(p *adapter.Pod) bool {
			ctrs, err := p.AllContainers()
			if err != nil {
				return false
			}
			for _, ctr := range ctrs {
				if filterValue == ctr.Name() {
					return true
				}
			}
			return false
		}, nil
	case "ctr-number":
		return func(p *adapter.Pod) bool {
			ctrIds, err := p.AllContainersByID()
			if err != nil {
				return false
			}

			fVint, err2 := strconv.Atoi(filterValue)
			if err2 != nil {
				return false
			}
			return len(ctrIds) == fVint
		}, nil
	case "ctr-status":
		if !util.StringInSlice(filterValue, []string{"created", "restarting", "running", "paused", "exited", "unknown"}) {
			return nil, errors.Errorf("%s is not a valid status", filterValue)
		}
		return func(p *adapter.Pod) bool {
			ctr_statuses, err := p.Status()
			if err != nil {
				return false
			}
			for _, ctr_status := range ctr_statuses {
				state := ctr_status.String()
				if ctr_status == define.ContainerStateConfigured {
					state = "created"
				}
				if state == filterValue {
					return true
				}
			}
			return false
		}, nil
	case "id":
		return func(p *adapter.Pod) bool {
			return strings.Contains(p.ID(), filterValue)
		}, nil
	case "name":
		return func(p *adapter.Pod) bool {
			return strings.Contains(p.Name(), filterValue)
		}, nil
	case "status":
		if !util.StringInSlice(filterValue, []string{"stopped", "running", "paused", "exited", "dead", "created"}) {
			return nil, errors.Errorf("%s is not a valid pod status", filterValue)
		}
		return func(p *adapter.Pod) bool {
			status, err := p.GetPodStatus()
			if err != nil {
				return false
			}
			if strings.ToLower(status) == filterValue {
				return true
			}
			return false
		}, nil
	}
	return nil, errors.Errorf("%s is an invalid filter", filter)
}

// generate the template based on conditions given
func genPodPsFormat(c *cliconfig.PodPsValues) string {
	format := ""
	switch {
	case c.Format != "":
		// "\t" from the command line is not being recognized as a tab
		// replacing the string "\t" to a tab character if the user passes in "\t"
		format = strings.Replace(c.Format, `\t`, "\t", -1)
	case c.Quiet:
		format = formats.IDString
	default:
		format = "table {{.ID}}\t{{.Name}}\t{{.Status}}\t{{.Created}}"
		if c.Bool("namespace") {
			format += "\t{{.Cgroup}}\t{{.Namespaces}}"
		}
		if c.CtrNames || c.CtrIDs || c.CtrStatus {
			format += "\t{{.ContainerInfo}}"
		} else {
			format += "\t{{.NumberOfContainers}}"
		}
		format += "\t{{.InfraID}}"
	}
	return format
}

func podPsToGeneric(templParams []podPsTemplateParams, jsonParams []podPsJSONParams) (genericParams []interface{}) {
	if len(templParams) > 0 {
		for _, v := range templParams {
			genericParams = append(genericParams, interface{}(v))
		}
		return
	}
	for _, v := range jsonParams {
		genericParams = append(genericParams, interface{}(v))
	}
	return
}

// generate the accurate header based on template given
func (p *podPsTemplateParams) podHeaderMap() map[string]string {
	v := reflect.Indirect(reflect.ValueOf(p))
	values := make(map[string]string)

	for i := 0; i < v.NumField(); i++ {
		key := v.Type().Field(i).Name
		value := key
		if value == "ID" {
			value = "Pod" + value
		}
		if value == "NumberOfContainers" {
			value = "#OfContainers"
		}
		values[key] = strings.ToUpper(splitCamelCase(value))
	}
	return values
}

func sortPodPsOutput(sortBy string, psOutput podPsSorted) (podPsSorted, error) {
	switch sortBy {
	case "created":
		sort.Sort(podPsSortedCreated{psOutput})
	case "id":
		sort.Sort(podPsSortedId{psOutput})
	case "name":
		sort.Sort(podPsSortedName{psOutput})
	case "number":
		sort.Sort(podPsSortedNumber{psOutput})
	case "status":
		sort.Sort(podPsSortedStatus{psOutput})
	default:
		return nil, errors.Errorf("invalid option for --sort, options are: id, names, or number")
	}
	return psOutput, nil
}

// getPodTemplateOutput returns the modified container information
func getPodTemplateOutput(psParams []podPsJSONParams, opts podPsOptions) ([]podPsTemplateParams, error) {
	var (
		psOutput []podPsTemplateParams
	)

	for _, psParam := range psParams {
		podID := psParam.ID
		infraID := psParam.InfraID
		var ctrStr string

		truncated := ""
		if !opts.NoTrunc {
			podID = shortID(podID)
			if len(psParam.CtrsInfo) > NUM_CTR_INFO {
				psParam.CtrsInfo = psParam.CtrsInfo[:NUM_CTR_INFO]
				truncated = "..."
			}
			infraID = shortID(infraID)
		}
		for _, ctrInfo := range psParam.CtrsInfo {
			infoSlice := make([]string, 0)
			if opts.IdsOfContainers {
				if opts.NoTrunc {
					infoSlice = append(infoSlice, ctrInfo.Id)
				} else {
					infoSlice = append(infoSlice, shortID(ctrInfo.Id))
				}
			}
			if opts.NamesOfContainers {
				infoSlice = append(infoSlice, ctrInfo.Name)
			}
			if opts.StatusOfContainers {
				infoSlice = append(infoSlice, ctrInfo.Status)
			}
			if len(infoSlice) != 0 {
				ctrStr += fmt.Sprintf("[%s] ", strings.Join(infoSlice, ","))
			}
		}
		ctrStr += truncated
		params := podPsTemplateParams{
			Created:            units.HumanDuration(time.Since(psParam.CreatedAt)) + " ago",
			ID:                 podID,
			Name:               psParam.Name,
			Status:             psParam.Status,
			NumberOfContainers: psParam.NumberOfContainers,
			Cgroup:             psParam.Cgroup,
			ContainerInfo:      ctrStr,
			InfraID:            infraID,
			Namespaces:         strings.Join(psParam.Namespaces, ","),
		}

		psOutput = append(psOutput, params)
	}

	return psOutput, nil
}

func getNamespaces(pod *adapter.Pod) []string {
	var shared []string
	if pod.SharesPID() {
		shared = append(shared, "pid")
	}
	if pod.SharesNet() {
		shared = append(shared, "net")
	}
	if pod.SharesMount() {
		shared = append(shared, "mnt")
	}
	if pod.SharesIPC() {
		shared = append(shared, "ipc")
	}
	if pod.SharesUser() {
		shared = append(shared, "user")
	}
	if pod.SharesCgroup() {
		shared = append(shared, "cgroup")
	}
	if pod.SharesUTS() {
		shared = append(shared, "uts")
	}
	return shared
}

// getAndSortPodJSONOutput returns the container info in its raw, sorted form
func getAndSortPodJSONParams(pods []*adapter.Pod, opts podPsOptions) ([]podPsJSONParams, error) {
	var (
		psOutput []podPsJSONParams
	)

	for _, pod := range pods {
		ctrs, err := pod.AllContainers()
		ctrsInfo := make([]podPsCtrInfo, 0)
		if err != nil {
			return nil, err
		}
		ctrNum := len(ctrs)
		status, err := pod.GetPodStatus()
		if err != nil {
			return nil, err
		}

		infraID, err := pod.InfraContainerID()
		if err != nil {
			return nil, err
		}
		for _, ctr := range ctrs {
			batchInfo, err := adapter.BatchContainerOp(ctr, bc_opts)
			if err != nil {
				return nil, err
			}
			var status string
			switch batchInfo.ConState {
			case define.ContainerStateExited:
				fallthrough
			case define.ContainerStateStopped:
				status = EXITED
			case define.ContainerStateRunning:
				status = RUNNING
			case define.ContainerStatePaused:
				status = PAUSED
			case define.ContainerStateCreated, define.ContainerStateConfigured:
				status = CREATED
			default:
				status = ERROR
			}
			ctrsInfo = append(ctrsInfo, podPsCtrInfo{
				Name:   batchInfo.ConConfig.Name,
				Id:     ctr.ID(),
				Status: status,
			})
		}
		params := podPsJSONParams{
			CreatedAt:          pod.CreatedTime(),
			ID:                 pod.ID(),
			Name:               pod.Name(),
			Status:             status,
			Cgroup:             pod.CgroupParent(),
			NumberOfContainers: ctrNum,
			CtrsInfo:           ctrsInfo,
			Namespaces:         getNamespaces(pod),
			InfraID:            infraID,
		}

		psOutput = append(psOutput, params)
	}
	return sortPodPsOutput(opts.Sort, psOutput)
}

func generatePodPsOutput(pods []*adapter.Pod, opts podPsOptions) error {
	if len(pods) == 0 && opts.Format != formats.JSONString {
		return nil
	}
	psOutput, err := getAndSortPodJSONParams(pods, opts)
	if err != nil {
		return err
	}
	var out formats.Writer

	switch opts.Format {
	case formats.JSONString:
		out = formats.JSONStructArray{Output: podPsToGeneric([]podPsTemplateParams{}, psOutput)}
	default:
		psOutput, err := getPodTemplateOutput(psOutput, opts)
		if err != nil {
			return errors.Wrapf(err, "unable to create output")
		}
		out = formats.StdoutTemplateArray{Output: podPsToGeneric(psOutput, []podPsJSONParams{}), Template: opts.Format, Fields: psOutput[0].podHeaderMap()}
	}

	return out.Out()
}
