package system

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

	"github.com/containers/common/pkg/completion"
	"github.com/containers/common/pkg/report"
	"github.com/containers/podman/v3/cmd/podman/parse"
	"github.com/containers/podman/v3/cmd/podman/registry"
	"github.com/containers/podman/v3/cmd/podman/validate"
	"github.com/containers/podman/v3/pkg/domain/entities"
	"github.com/docker/go-units"
	"github.com/spf13/cobra"
)

var (
	dfSystemDescription = `
	podman system df

	Show podman disk usage
	`
	dfSystemCommand = &cobra.Command{
		Use:               "df [options]",
		Args:              validate.NoArgs,
		Short:             "Show podman disk usage",
		Long:              dfSystemDescription,
		RunE:              df,
		ValidArgsFunction: completion.AutocompleteNone,
	}
)

var (
	dfOptions entities.SystemDfOptions
)

func init() {
	registry.Commands = append(registry.Commands, registry.CliCommand{
		Mode:    []entities.EngineMode{entities.ABIMode, entities.TunnelMode},
		Command: dfSystemCommand,
		Parent:  systemCmd,
	})
	flags := dfSystemCommand.Flags()
	flags.BoolVarP(&dfOptions.Verbose, "verbose", "v", false, "Show detailed information on disk usage")

	formatFlagName := "format"
	flags.StringVar(&dfOptions.Format, formatFlagName, "", "Pretty-print images using a Go template")
	_ = dfSystemCommand.RegisterFlagCompletionFunc(formatFlagName, completion.AutocompleteNone)
}

func df(cmd *cobra.Command, args []string) error {
	reports, err := registry.ContainerEngine().SystemDf(registry.Context(), dfOptions)
	if err != nil {
		return err
	}

	w := tabwriter.NewWriter(os.Stdout, 8, 2, 2, ' ', 0)

	if dfOptions.Verbose {
		return printVerbose(w, cmd, reports)
	}
	return printSummary(w, cmd, reports)
}

func printSummary(w *tabwriter.Writer, cmd *cobra.Command, reports *entities.SystemDfReport) error {
	var (
		dfSummaries       []*dfSummary
		active            int
		size, reclaimable int64
	)
	for _, i := range reports.Images {
		if i.Containers > 0 {
			active++
		}
		size += i.Size
		if i.Containers < 1 {
			reclaimable += i.Size
		}
	}
	imageSummary := dfSummary{
		Type:        "Images",
		Total:       len(reports.Images),
		Active:      active,
		size:        size,
		reclaimable: reclaimable,
	}
	dfSummaries = append(dfSummaries, &imageSummary)

	// Containers
	var (
		conActive               int
		conSize, conReclaimable int64
	)
	for _, c := range reports.Containers {
		if c.Status == "running" {
			conActive++
		} else {
			conReclaimable += c.RWSize
		}
		conSize += c.RWSize
	}
	containerSummary := dfSummary{
		Type:        "Containers",
		Total:       len(reports.Containers),
		Active:      conActive,
		size:        conSize,
		reclaimable: conReclaimable,
	}
	dfSummaries = append(dfSummaries, &containerSummary)

	// Volumes
	var (
		activeVolumes                   int
		volumesSize, volumesReclaimable int64
	)

	for _, v := range reports.Volumes {
		activeVolumes += v.Links
		volumesSize += v.Size
		volumesReclaimable += v.ReclaimableSize
	}
	volumeSummary := dfSummary{
		Type:        "Local Volumes",
		Total:       len(reports.Volumes),
		Active:      activeVolumes,
		size:        volumesSize,
		reclaimable: volumesReclaimable,
	}
	dfSummaries = append(dfSummaries, &volumeSummary)

	// need to give un-exported fields
	hdrs := report.Headers(dfSummary{}, map[string]string{
		"Size":        "SIZE",
		"Reclaimable": "RECLAIMABLE",
	})
	row := "{{.Type}}\t{{.Total}}\t{{.Active}}\t{{.Size}}\t{{.Reclaimable}}\n"
	if cmd.Flags().Changed("format") {
		row = report.NormalizeFormat(dfOptions.Format)
	}
	return writeTemplate(w, cmd, hdrs, row, dfSummaries)
}

func printVerbose(w *tabwriter.Writer, cmd *cobra.Command, reports *entities.SystemDfReport) error {
	defer w.Flush()

	fmt.Fprint(w, "Images space usage:\n\n")
	// convert to dfImage for output
	dfImages := make([]*dfImage, 0, len(reports.Images))
	for _, d := range reports.Images {
		dfImages = append(dfImages, &dfImage{SystemDfImageReport: d})
	}
	hdrs := report.Headers(entities.SystemDfImageReport{}, map[string]string{
		"ImageID":    "IMAGE ID",
		"SharedSize": "SHARED SIZE",
		"UniqueSize": "UNIQUE SIZE",
	})
	imageRow := "{{.Repository}}\t{{.Tag}}\t{{.ImageID}}\t{{.Created}}\t{{.Size}}\t{{.SharedSize}}\t{{.UniqueSize}}\t{{.Containers}}\n"
	if err := writeTemplate(w, cmd, hdrs, imageRow, dfImages); err != nil {
		return nil
	}

	fmt.Fprint(w, "\nContainers space usage:\n\n")
	// convert to dfContainers for output
	dfContainers := make([]*dfContainer, 0, len(reports.Containers))
	for _, d := range reports.Containers {
		dfContainers = append(dfContainers, &dfContainer{SystemDfContainerReport: d})
	}
	hdrs = report.Headers(entities.SystemDfContainerReport{}, map[string]string{
		"ContainerID":  "CONTAINER ID",
		"LocalVolumes": "LOCAL VOLUMES",
		"RWSize":       "SIZE",
	})
	containerRow := "{{.ContainerID}}\t{{.Image}}\t{{.Command}}\t{{.LocalVolumes}}\t{{.RWSize}}\t{{.Created}}\t{{.Status}}\t{{.Names}}\n"
	if err := writeTemplate(w, cmd, hdrs, containerRow, dfContainers); err != nil {
		return nil
	}

	fmt.Fprint(w, "\nLocal Volumes space usage:\n\n")
	dfVolumes := make([]*dfVolume, 0, len(reports.Volumes))
	// convert to dfVolume for output
	for _, d := range reports.Volumes {
		dfVolumes = append(dfVolumes, &dfVolume{SystemDfVolumeReport: d})
	}
	hdrs = report.Headers(entities.SystemDfVolumeReport{}, map[string]string{
		"VolumeName": "VOLUME NAME",
	})
	volumeRow := "{{.VolumeName}}\t{{.Links}}\t{{.Size}}\n"
	return writeTemplate(w, cmd, hdrs, volumeRow, dfVolumes)
}

func writeTemplate(w *tabwriter.Writer, cmd *cobra.Command, hdrs []map[string]string, format string, output interface{}) error {
	defer w.Flush()

	format = parse.EnforceRange(format)
	tmpl, err := template.New("df").Parse(format)
	if err != nil {
		return err
	}

	if !cmd.Flags().Changed("format") {
		if err := tmpl.Execute(w, hdrs); err != nil {
			return err
		}
	}
	return tmpl.Execute(w, output)
}

type dfImage struct {
	*entities.SystemDfImageReport
}

func (d *dfImage) ImageID() string {
	return d.SystemDfImageReport.ImageID[0:12]
}

func (d *dfImage) Created() string {
	return units.HumanDuration(time.Since(d.SystemDfImageReport.Created))
}

func (d *dfImage) Size() string {
	return units.HumanSize(float64(d.SystemDfImageReport.Size))
}

func (d *dfImage) SharedSize() string {
	return units.HumanSize(float64(d.SystemDfImageReport.SharedSize))
}

func (d *dfImage) UniqueSize() string {
	return units.HumanSize(float64(d.SystemDfImageReport.UniqueSize))
}

type dfContainer struct {
	*entities.SystemDfContainerReport
}

func (d *dfContainer) ContainerID() string {
	return d.SystemDfContainerReport.ContainerID[0:12]
}

func (d *dfContainer) Image() string {
	return d.SystemDfContainerReport.Image[0:12]
}

func (d *dfContainer) Command() string {
	return strings.Join(d.SystemDfContainerReport.Command, " ")
}

func (d *dfContainer) RWSize() string {
	return units.HumanSize(float64(d.SystemDfContainerReport.RWSize))
}

func (d *dfContainer) Created() string {
	return units.HumanDuration(time.Since(d.SystemDfContainerReport.Created))
}

type dfVolume struct {
	*entities.SystemDfVolumeReport
}

func (d *dfVolume) Size() string {
	return units.HumanSize(float64(d.SystemDfVolumeReport.Size))
}

type dfSummary struct {
	Type        string
	Total       int
	Active      int
	size        int64
	reclaimable int64
}

func (d *dfSummary) Size() string {
	return units.HumanSize(float64(d.size))
}

func (d *dfSummary) Reclaimable() string {
	percent := int(float64(d.reclaimable)/float64(d.size)) * 100
	return fmt.Sprintf("%s (%d%%)", units.HumanSize(float64(d.reclaimable)), percent)
}
