package main

import (
	"fmt"
	"html/template"
	"os"
	"reflect"
	"strings"
	"text/tabwriter"
	"time"

	tm "github.com/buger/goterm"
	"github.com/containers/buildah/pkg/formats"
	"github.com/containers/libpod/cmd/podman/cliconfig"
	"github.com/containers/libpod/libpod"
	"github.com/containers/libpod/libpod/define"
	"github.com/containers/libpod/pkg/adapter"
	"github.com/containers/libpod/pkg/cgroups"
	"github.com/containers/libpod/pkg/rootless"
	"github.com/pkg/errors"
	"github.com/spf13/cobra"
)

var (
	podStatsCommand     cliconfig.PodStatsValues
	podStatsDescription = `For each specified pod this command will display percentage of CPU, memory, network I/O, block I/O and PIDs for containers in one the pods.`

	_podStatsCommand = &cobra.Command{
		Use:   "stats [flags] [POD...]",
		Short: "Display a live stream of resource usage statistics for the containers in one or more pods",
		Long:  podStatsDescription,
		RunE: func(cmd *cobra.Command, args []string) error {
			podStatsCommand.InputArgs = args
			podStatsCommand.GlobalFlags = MainGlobalOpts
			podStatsCommand.Remote = remoteclient
			return podStatsCmd(&podStatsCommand)
		},
		Example: `podman stats -a --no-stream
  podman stats --no-reset ctrID
  podman stats --no-stream --format "table {{.ID}} {{.Name}} {{.MemUsage}}" ctrID`,
	}
)

func init() {
	podStatsCommand.Command = _podStatsCommand
	podStatsCommand.SetHelpTemplate(HelpTemplate())
	podStatsCommand.SetUsageTemplate(UsageTemplate())
	flags := podStatsCommand.Flags()
	flags.BoolVarP(&podStatsCommand.All, "all", "a", false, "Provide stats for all running pods")
	flags.StringVar(&podStatsCommand.Format, "format", "", "Pretty-print container statistics to JSON or using a Go template")
	flags.BoolVarP(&podStatsCommand.Latest, "latest", "l", false, "Provide stats on the latest pod podman is aware of")
	flags.BoolVar(&podStatsCommand.NoStream, "no-stream", false, "Disable streaming stats and only pull the first result, default setting is false")
	flags.BoolVar(&podStatsCommand.NoReset, "no-reset", false, "Disable resetting the screen between intervals")
	markFlagHiddenForRemoteClient("latest", flags)
}

func podStatsCmd(c *cliconfig.PodStatsValues) error {
	if rootless.IsRootless() {
		unified, err := cgroups.IsCgroup2UnifiedMode()
		if err != nil {
			return err
		}
		if !unified {
			return errors.New("stats is not supported in rootless mode without cgroups v2")
		}
	}

	format := c.Format
	all := c.All
	latest := c.Latest
	ctr := 0
	if all {
		ctr += 1
	}
	if latest {
		ctr += 1
	}
	if len(c.InputArgs) > 0 {
		ctr += 1
	}

	if ctr > 1 {
		return errors.Errorf("--all, --latest and containers cannot be used together")
	}

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

	times := -1
	if c.NoStream {
		times = 1
	}

	pods, err := runtime.GetStatPods(c)
	if err != nil {
		return errors.Wrapf(err, "unable to get a list of pods")
	}

	// Create empty container stat results for our first pass
	var previousPodStats []*adapter.PodContainerStats
	for _, p := range pods {
		cs := make(map[string]*libpod.ContainerStats)
		pcs := adapter.PodContainerStats{
			Pod:            p,
			ContainerStats: cs,
		}
		previousPodStats = append(previousPodStats, &pcs)
	}

	step := 1
	if times == -1 {
		times = 1
		step = 0
	}

	headerNames := make(map[string]string)
	if c.Format != "" {
		// Make a map of the field names for the headers
		v := reflect.ValueOf(podStatOut{})
		t := v.Type()
		for i := 0; i < t.NumField(); i++ {
			value := strings.ToUpper(splitCamelCase(t.Field(i).Name))
			switch value {
			case "CPU":
				value = value + " %"
			case "MEM":
				value = value + " %"
			case "MEM USAGE":
				value = "MEM USAGE / LIMIT"
			}
			headerNames[t.Field(i).Name] = value
		}
	}

	for i := 0; i < times; i += step {
		var newStats []*adapter.PodContainerStats
		for _, p := range pods {
			prevStat := getPreviousPodContainerStats(p.ID(), previousPodStats)
			newPodStats, err := p.GetPodStats(prevStat)
			if errors.Cause(err) == define.ErrNoSuchPod {
				continue
			}
			if err != nil {
				return err
			}
			newPod := adapter.PodContainerStats{
				Pod:            p,
				ContainerStats: newPodStats,
			}
			newStats = append(newStats, &newPod)
		}
		//Output
		if strings.ToLower(format) != formats.JSONString && !c.NoReset {
			tm.Clear()
			tm.MoveCursor(1, 1)
			tm.Flush()
		}
		if strings.ToLower(format) == formats.JSONString {
			if err := outputJson(newStats); err != nil {
				return err
			}

		} else {
			results := podContainerStatsToPodStatOut(newStats)
			if len(format) == 0 {
				outputToStdOut(results)
			} else {
				if err := printPSFormat(c.Format, results, headerNames); err != nil {
					return err
				}
			}
		}
		time.Sleep(time.Second)
		previousPodStats := new([]*libpod.PodContainerStats)
		if err := libpod.JSONDeepCopy(newStats, previousPodStats); err != nil {
			return err
		}
		pods, err = runtime.GetStatPods(c)
		if err != nil {
			return err
		}
	}

	return nil
}

func podContainerStatsToPodStatOut(stats []*adapter.PodContainerStats) []*podStatOut {
	var out []*podStatOut
	for _, p := range stats {
		for _, c := range p.ContainerStats {
			o := podStatOut{
				CPU:      floatToPercentString(c.CPU),
				MemUsage: combineHumanValues(c.MemUsage, c.MemLimit),
				Mem:      floatToPercentString(c.MemPerc),
				NetIO:    combineHumanValues(c.NetInput, c.NetOutput),
				BlockIO:  combineHumanValues(c.BlockInput, c.BlockOutput),
				PIDS:     pidsToString(c.PIDs),
				CID:      c.ContainerID[:12],
				Name:     c.Name,
				Pod:      p.Pod.ID()[:12],
			}
			out = append(out, &o)
		}
	}
	return out
}

type podStatOut struct {
	CPU      string
	MemUsage string
	Mem      string
	NetIO    string
	BlockIO  string
	PIDS     string
	Pod      string
	CID      string
	Name     string
}

func printPSFormat(format string, stats []*podStatOut, headerNames map[string]string) error {
	if len(stats) == 0 {
		return nil
	}

	// Use a tabwriter to align column format
	w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
	// Spit out the header if "table" is present in the format
	if strings.HasPrefix(format, "table") {
		hformat := strings.Replace(strings.TrimSpace(format[5:]), " ", "\t", -1)
		format = hformat
		headerTmpl, err := template.New("header").Parse(hformat)
		if err != nil {
			return err
		}
		if err := headerTmpl.Execute(w, headerNames); err != nil {
			return err
		}
		fmt.Fprintln(w, "")
	}

	// Spit out the data rows now
	dataTmpl, err := template.New("data").Parse(format)
	if err != nil {
		return err
	}
	for _, container := range stats {
		if err := dataTmpl.Execute(w, container); err != nil {
			return err
		}
		fmt.Fprintln(w, "")
	}
	// Flush the writer
	return w.Flush()

}

func outputToStdOut(stats []*podStatOut) {
	w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
	outFormat := "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n"
	fmt.Fprintf(w, outFormat, "POD", "CID", "NAME", "CPU %", "MEM USAGE/ LIMIT", "MEM %", "NET IO", "BLOCK IO", "PIDS")
	for _, i := range stats {
		if len(stats) == 0 {
			fmt.Fprintf(w, outFormat, i.Pod, "--", "--", "--", "--", "--", "--", "--", "--")
		} else {
			fmt.Fprintf(w, outFormat, i.Pod, i.CID, i.Name, i.CPU, i.MemUsage, i.Mem, i.NetIO, i.BlockIO, i.PIDS)
		}
	}
	w.Flush()
}

func getPreviousPodContainerStats(podID string, prev []*adapter.PodContainerStats) map[string]*libpod.ContainerStats {
	for _, p := range prev {
		if podID == p.Pod.ID() {
			return p.ContainerStats
		}
	}
	return map[string]*libpod.ContainerStats{}
}

func outputJson(stats []*adapter.PodContainerStats) error {
	b, err := json.MarshalIndent(&stats, "", "     ")
	if err != nil {
		return err
	}
	fmt.Println(string(b))
	return nil
}