// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package command

import (
	"errors"
	"fmt"
	"io"
	"os"
	"sort"
	"strings"

	"github.com/dustin/go-humanize"
	"github.com/hashicorp/nomad/api"
	"github.com/hashicorp/nomad/api/contexts"
	flaghelper "github.com/hashicorp/nomad/helper/flags"
	"github.com/posener/complete"
)

type VolumeSnapshotListCommand struct {
	Meta
}

func (c *VolumeSnapshotListCommand) Help() string {
	helpText := `
Usage: nomad volume snapshot list [-plugin plugin_id]

  Display a list of CSI volume snapshots for a plugin along
  with their source volume ID as known to the external
  storage provider.

  When ACLs are enabled, this command requires a token with the
  'csi-list-volumes' capability for the plugin's namespace.

General Options:

  ` + generalOptionsUsage(usageOptsDefault) + `

List Options:

  -page-token
    Where to start pagination.

  -per-page
    How many results to show per page. Defaults to 30.

  -plugin: Display only snapshots managed by a particular plugin. This
    parameter is required.

  -secret
    Secrets to pass to the plugin to list snapshots. Accepts multiple
    flags in the form -secret key=value

  -verbose
    Display full information for snapshots.
`

	return strings.TrimSpace(helpText)
}

func (c *VolumeSnapshotListCommand) Synopsis() string {
	return "Display a list of volume snapshots for plugin"
}

func (c *VolumeSnapshotListCommand) AutocompleteFlags() complete.Flags {
	return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient),
		complete.Flags{})
}

func (c *VolumeSnapshotListCommand) AutocompleteArgs() complete.Predictor {
	return complete.PredictFunc(func(a complete.Args) []string {
		client, err := c.Meta.Client()
		if err != nil {
			return nil
		}

		resp, _, err := client.Search().PrefixSearch(a.Last, contexts.Plugins, nil)
		if err != nil {
			return []string{}
		}
		return resp.Matches[contexts.Plugins]
	})
}

func (c *VolumeSnapshotListCommand) Name() string { return "volume snapshot list" }

func (c *VolumeSnapshotListCommand) Run(args []string) int {
	var pluginID string
	var verbose bool
	var secretsArgs flaghelper.StringFlag
	var perPage int
	var pageToken string

	flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
	flags.Usage = func() { c.Ui.Output(c.Help()) }
	flags.StringVar(&pluginID, "plugin", "", "")
	flags.BoolVar(&verbose, "verbose", false, "")
	flags.Var(&secretsArgs, "secret", "secrets for snapshot, ex. -secret key=value")
	flags.IntVar(&perPage, "per-page", 30, "")
	flags.StringVar(&pageToken, "page-token", "", "")

	if err := flags.Parse(args); err != nil {
		c.Ui.Error(fmt.Sprintf("Error parsing arguments %s", err))
		return 1
	}

	args = flags.Args()
	if len(args) > 0 {
		c.Ui.Error("This command takes no arguments")
		c.Ui.Error(commandErrorText(c))
		return 1
	}

	// Get the HTTP client
	client, err := c.Meta.Client()
	if err != nil {
		c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
		return 1
	}

	plugs, _, err := client.CSIPlugins().List(&api.QueryOptions{Prefix: pluginID})
	if err != nil {
		c.Ui.Error(fmt.Sprintf("Error querying CSI plugins: %s", err))
		return 1
	}
	if len(plugs) == 0 {
		c.Ui.Error(fmt.Sprintf("No plugins(s) with prefix or ID %q found", pluginID))
		return 1
	}
	if len(plugs) > 1 {
		if pluginID != plugs[0].ID {
			out, err := c.csiFormatPlugins(plugs)
			if err != nil {
				c.Ui.Error(fmt.Sprintf("Error formatting: %s", err))
				return 1
			}
			c.Ui.Error(fmt.Sprintf("Prefix matched multiple plugins\n\n%s", out))
			return 1
		}
	}
	pluginID = plugs[0].ID

	secrets := api.CSISecrets{}
	for _, kv := range secretsArgs {
		if key, value, found := strings.Cut(kv, "="); found {
			secrets[key] = value
		} else {
			c.Ui.Error("Secret must be in the format: -secret key=value")
			return 1
		}
	}

	req := &api.CSISnapshotListRequest{
		PluginID: pluginID,
		Secrets:  secrets,
		QueryOptions: api.QueryOptions{
			PerPage:   int32(perPage),
			NextToken: pageToken,
			Params:    map[string]string{},
		},
	}

	resp, _, err := client.CSIVolumes().ListSnapshotsOpts(req)
	if err != nil && !errors.Is(err, io.EOF) {
		c.Ui.Error(fmt.Sprintf(
			"Error querying CSI external snapshots for plugin %q: %s", pluginID, err))
		return 1
	}
	if resp == nil || len(resp.Snapshots) == 0 {
		// several plugins return EOF once you hit the end of the page,
		// rather than an empty list
		return 0
	}

	c.Ui.Output(csiFormatSnapshots(resp.Snapshots, verbose))

	if resp.NextToken != "" {
		c.Ui.Output(fmt.Sprintf(`
Results have been paginated. To get the next page run:
%s -page-token %s`, argsWithoutPageToken(os.Args), resp.NextToken))
	}

	return 0
}

func csiFormatSnapshots(snapshots []*api.CSISnapshot, verbose bool) string {
	rows := []string{"Snapshot ID|Volume ID|Size|Create Time|Ready?"}
	length := 12
	if verbose {
		length = 30
	}
	for _, v := range snapshots {
		rows = append(rows, fmt.Sprintf("%s|%s|%s|%s|%v",
			v.ID,
			limit(v.ExternalSourceVolumeID, length),
			humanize.IBytes(uint64(v.SizeBytes)),
			formatUnixNanoTime(v.CreateTime*1e9), // seconds to nanoseconds
			v.IsReady,
		))
	}
	return formatList(rows)
}

func (c *VolumeSnapshotListCommand) csiFormatPlugins(plugs []*api.CSIPluginListStub) (string, error) {
	// TODO: this has a lot of overlap with 'nomad plugin status', so we
	// should factor out some shared formatting helpers.
	sort.Slice(plugs, func(i, j int) bool { return plugs[i].ID < plugs[j].ID })
	length := 30
	rows := make([]string, len(plugs)+1)
	rows[0] = "ID|Provider|Controllers Healthy/Expected|Nodes Healthy/Expected"
	for i, p := range plugs {
		rows[i+1] = fmt.Sprintf("%s|%s|%d/%d|%d/%d",
			limit(p.ID, length),
			p.Provider,
			p.ControllersHealthy,
			p.ControllersExpected,
			p.NodesHealthy,
			p.NodesExpected,
		)
	}
	return formatList(rows), nil
}
