//go:build amd64 || arm64

package machine

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

	"github.com/containers/podman/v5/cmd/podman/common"
	"github.com/containers/podman/v5/cmd/podman/registry"
	"github.com/containers/podman/v5/cmd/podman/validate"
	"github.com/containers/podman/v5/pkg/domain/entities"
	"github.com/containers/podman/v5/pkg/machine"
	provider2 "github.com/containers/podman/v5/pkg/machine/provider"
	"github.com/containers/podman/v5/pkg/machine/shim"
	"github.com/containers/podman/v5/pkg/machine/vmconfigs"
	"github.com/docker/go-units"
	"github.com/spf13/cobra"
	"go.podman.io/common/pkg/completion"
	"go.podman.io/common/pkg/config"
	"go.podman.io/common/pkg/report"
)

var (
	lsCmd = &cobra.Command{
		Use:     "list [options]",
		Aliases: []string{"ls"},
		Short:   "List machines",
		Long:    "List managed virtual machines.",
		// do not use machinePreRunE, as that pre-sets the provider
		PersistentPreRunE: rootlessOnly,
		RunE:              list,
		Args:              validate.NoArgs,
		ValidArgsFunction: completion.AutocompleteNone,
		Example: `podman machine list,
  podman machine list --format json
  podman machine ls`,
	}
	listFlag = listFlagType{}
)

type listFlagType struct {
	format       string
	noHeading    bool
	quiet        bool
	allProviders bool
}

func init() {
	registry.Commands = append(registry.Commands, registry.CliCommand{
		Command: lsCmd,
		Parent:  machineCmd,
	})

	flags := lsCmd.Flags()
	formatFlagName := "format"
	flags.StringVar(&listFlag.format, formatFlagName, "{{range .}}{{.Name}}\t{{.VMType}}\t{{.Created}}\t{{.LastUp}}\t{{.CPUs}}\t{{.Memory}}\t{{.DiskSize}}\n{{end -}}", "Format volume output using JSON or a Go template")
	_ = lsCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(&entities.ListReporter{}))
	flags.BoolVarP(&listFlag.noHeading, "noheading", "n", false, "Do not print headers")
	flags.BoolVarP(&listFlag.quiet, "quiet", "q", false, "Show only machine names")
	flags.BoolVar(&listFlag.allProviders, "all-providers", false, "Show machines from all providers")
}

func list(cmd *cobra.Command, _ []string) error {
	var (
		opts machine.ListOptions
		err  error
	)
	var providers []vmconfigs.VMProvider
	if listFlag.allProviders {
		providers = provider2.GetAll()
	} else {
		provider, err = provider2.Get()
		if err != nil {
			return err
		}
		providers = []vmconfigs.VMProvider{provider}
	}

	listResponse, err := shim.List(providers, opts)
	if err != nil {
		return err
	}

	// Sort by last run
	sort.Slice(listResponse, func(i, j int) bool {
		return listResponse[i].LastUp.After(listResponse[j].LastUp)
	})
	// Bring currently running machines to top
	sort.Slice(listResponse, func(i, _ int) bool {
		return listResponse[i].Running
	})

	// ignore the error here we only want to know if we have a default connection to show it in list
	defaultCon, _ := registry.PodmanConfig().ContainersConfDefaultsRO.GetConnection("", true)

	if report.IsJSON(listFlag.format) {
		machineReporter := toMachineFormat(listResponse, defaultCon)
		b, err := json.MarshalIndent(machineReporter, "", "    ")
		if err != nil {
			return err
		}
		os.Stdout.Write(b)
		return nil
	}

	machineReporter := toHumanFormat(listResponse, defaultCon)
	return outputTemplate(cmd, machineReporter)
}

func outputTemplate(cmd *cobra.Command, responses []*entities.ListReporter) error {
	headers := report.Headers(entities.ListReporter{}, map[string]string{
		"LastUp":   "LAST UP",
		"VmType":   "VM TYPE",
		"CPUs":     "CPUS",
		"Memory":   "MEMORY",
		"DiskSize": "DISK SIZE",
		"Swap":     "SWAP",
	})

	rpt := report.New(os.Stdout, cmd.Name())
	defer rpt.Flush()

	var err error
	switch {
	case cmd.Flag("format").Changed:
		rpt, err = rpt.Parse(report.OriginUser, listFlag.format)
	case listFlag.quiet:
		rpt, err = rpt.Parse(report.OriginUser, "{{.Name}}\n")
	default:
		rpt, err = rpt.Parse(report.OriginPodman, listFlag.format)
	}
	if err != nil {
		return err
	}

	if rpt.RenderHeaders && !listFlag.noHeading {
		if err := rpt.Execute(headers); err != nil {
			return fmt.Errorf("failed to write report column headers: %w", err)
		}
	}
	return rpt.Execute(responses)
}

func strTime(t time.Time) string {
	iso, err := t.MarshalText()
	if err != nil {
		return ""
	}
	return string(iso)
}

func strUint(u uint64) string {
	return strconv.FormatUint(u, 10)
}

func streamName(imageStream string) string {
	if imageStream == "" {
		return "default"
	}
	return imageStream
}

func toMachineFormat(vms []*machine.ListResponse, defaultCon *config.Connection) []*entities.ListReporter {
	machineResponses := make([]*entities.ListReporter, 0, len(vms))
	for _, vm := range vms {
		isDefault := false
		// check port, in case we somehow have machines with the same name in different providers
		if defaultCon != nil {
			isDefault = vm.Name == defaultCon.Name && strings.Contains(defaultCon.URI, strconv.Itoa((vm.Port)))
		}
		response := new(entities.ListReporter)
		response.Default = isDefault
		response.Name = vm.Name
		response.Running = vm.Running
		response.LastUp = strTime(vm.LastUp)
		response.Created = strTime(vm.CreatedAt)
		response.Stream = streamName(vm.Stream)
		response.VMType = vm.VMType
		response.CPUs = vm.CPUs
		response.Memory = strUint(uint64(vm.Memory.ToBytes()))
		response.Swap = strUint(uint64(vm.Swap.ToBytes()))
		response.DiskSize = strUint(uint64(vm.DiskSize.ToBytes()))
		response.Port = vm.Port
		response.RemoteUsername = vm.RemoteUsername
		response.IdentityPath = vm.IdentityPath
		response.Starting = vm.Starting
		response.UserModeNetworking = vm.UserModeNetworking

		machineResponses = append(machineResponses, response)
	}
	return machineResponses
}

func toHumanFormat(vms []*machine.ListResponse, defaultCon *config.Connection) []*entities.ListReporter {
	humanResponses := make([]*entities.ListReporter, 0, len(vms))
	for _, vm := range vms {
		response := new(entities.ListReporter)
		isDefault := false
		// check port, in case we somehow have machines with the same name in different providers
		if defaultCon != nil {
			isDefault = vm.Name == defaultCon.Name && strings.Contains(defaultCon.URI, strconv.Itoa((vm.Port)))
		}
		if isDefault {
			response.Name = vm.Name + "*"
			response.Default = true
		} else {
			response.Name = vm.Name
		}
		switch {
		case vm.Starting:
			response.LastUp = "Currently starting"
			response.Starting = true
		case vm.Running:
			response.LastUp = "Currently running"
			response.Running = true
		case vm.LastUp.IsZero():
			response.LastUp = "Never"
		default:
			response.LastUp = units.HumanDuration(time.Since(vm.LastUp)) + " ago"
		}
		response.Created = units.HumanDuration(time.Since(vm.CreatedAt)) + " ago"
		response.VMType = vm.VMType
		response.CPUs = vm.CPUs
		response.Memory = units.BytesSize(float64(vm.Memory.ToBytes()))
		response.Swap = units.BytesSize(float64(vm.Swap.ToBytes()))
		response.DiskSize = units.BytesSize(float64(vm.DiskSize.ToBytes()))

		humanResponses = append(humanResponses, response)
	}
	return humanResponses
}
