// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package get

import (
	"bytes"
	"encoding/base64"
	"flag"
	"fmt"
	"io"
	"text/tabwriter"

	"github.com/hashicorp/consul/api"
	"github.com/hashicorp/consul/command/flags"
	"github.com/mitchellh/cli"
)

func New(ui cli.Ui) *cmd {
	c := &cmd{UI: ui}
	c.init()
	return c
}

type cmd struct {
	UI           cli.Ui
	flags        *flag.FlagSet
	http         *flags.HTTPFlags
	help         string
	base64encode bool
	detailed     bool
	keys         bool
	recurse      bool
	separator    string
}

func (c *cmd) init() {
	c.flags = flag.NewFlagSet("", flag.ContinueOnError)
	c.flags.BoolVar(&c.base64encode, "base64", false,
		"Base64 encode the value. The default value is false.")
	c.flags.BoolVar(&c.detailed, "detailed", false,
		"Provide additional metadata about the key in addition to the value such "+
			"as the ModifyIndex and any flags that may have been set on the key. "+
			"The default value is false.")
	c.flags.BoolVar(&c.keys, "keys", false,
		"List keys which start with the given prefix, but not their values. "+
			"This is especially useful if you only need the key names themselves. "+
			"This option is commonly combined with the -separator option. The default "+
			"value is false.")
	c.flags.BoolVar(&c.recurse, "recurse", false,
		"Recursively look at all keys prefixed with the given path. The default "+
			"value is false.")
	c.flags.StringVar(&c.separator, "separator", "/",
		"String to use as a separator between keys. The default value is \"/\", "+
			"but this option is only taken into account when paired with the -keys flag.")

	c.http = &flags.HTTPFlags{}
	flags.Merge(c.flags, c.http.ClientFlags())
	flags.Merge(c.flags, c.http.ServerFlags())
	flags.Merge(c.flags, c.http.MultiTenancyFlags())
	c.help = flags.Usage(help, c.flags)
}

func (c *cmd) Run(args []string) int {
	if err := c.flags.Parse(args); err != nil {
		return 1
	}

	key := ""

	// Check for arg validation
	args = c.flags.Args()
	switch len(args) {
	case 0:
		key = ""
	case 1:
		key = args[0]
	default:
		c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
		return 1
	}

	// This is just a "nice" thing to do. Since pairs cannot start with a /, but
	// users will likely put "/" or "/foo", lets go ahead and strip that for them
	// here.
	if len(key) > 0 && key[0] == '/' {
		key = key[1:]
	}

	// If the key is empty and we are not doing a recursive or key-based lookup,
	// this is an error.
	if key == "" && !(c.recurse || c.keys) {
		c.UI.Error("Error! Missing KEY argument")
		return 1
	}

	// Create and test the HTTP client
	client, err := c.http.APIClient()
	if err != nil {
		c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
		return 1
	}

	switch {
	case c.keys && c.recurse:
		pairs, _, err := client.KV().List(key, &api.QueryOptions{
			AllowStale: c.http.Stale(),
		})
		if err != nil {
			c.UI.Error(fmt.Sprintf("Error querying Consul agent: %s", err))
			return 1
		}

		for i, pair := range pairs {
			if c.detailed {
				var b bytes.Buffer
				if err := prettyKVPair(&b, pair, false, true); err != nil {
					c.UI.Error(fmt.Sprintf("Error rendering KV key: %s", err))
					return 1
				}
				c.UI.Info(b.String())

				if i < len(pairs)-1 {
					c.UI.Info("")
				}
			} else {
				c.UI.Info(fmt.Sprintf("%s", pair.Key))
			}
		}
		return 0
	case c.keys:
		keys, _, err := client.KV().Keys(key, c.separator, &api.QueryOptions{
			AllowStale: c.http.Stale(),
		})
		if err != nil {
			c.UI.Error(fmt.Sprintf("Error querying Consul agent: %s", err))
			return 1
		}

		for _, k := range keys {
			c.UI.Info(k)
		}

		return 0
	case c.recurse:
		pairs, _, err := client.KV().List(key, &api.QueryOptions{
			AllowStale: c.http.Stale(),
		})
		if err != nil {
			c.UI.Error(fmt.Sprintf("Error querying Consul agent: %s", err))
			return 1
		}

		for i, pair := range pairs {
			if c.detailed {
				var b bytes.Buffer
				if err := prettyKVPair(&b, pair, c.base64encode, false); err != nil {
					c.UI.Error(fmt.Sprintf("Error rendering KV pair: %s", err))
					return 1
				}

				c.UI.Info(b.String())

				if i < len(pairs)-1 {
					c.UI.Info("")
				}
			} else {
				if c.base64encode {
					c.UI.Info(fmt.Sprintf("%s:%s", pair.Key, base64.StdEncoding.EncodeToString(pair.Value)))
				} else {
					c.UI.Info(fmt.Sprintf("%s:%s", pair.Key, pair.Value))
				}
			}
		}

		return 0
	default:
		pair, _, err := client.KV().Get(key, &api.QueryOptions{
			AllowStale: c.http.Stale(),
		})
		if err != nil {
			c.UI.Error(fmt.Sprintf("Error querying Consul agent: %s", err))
			return 1
		}

		if pair == nil {
			c.UI.Error(fmt.Sprintf("Error! No key exists at: %s", key))
			return 1
		}

		if c.detailed {
			var b bytes.Buffer
			if err := prettyKVPair(&b, pair, c.base64encode, false); err != nil {
				c.UI.Error(fmt.Sprintf("Error rendering KV pair: %s", err))
				return 1
			}

			c.UI.Info(b.String())
			return 0
		}

		if c.base64encode {
			c.UI.Info(base64.StdEncoding.EncodeToString(pair.Value))
		} else {
			c.UI.Info(string(pair.Value))
		}
		return 0
	}
}

func (c *cmd) Synopsis() string {
	return synopsis
}

func (c *cmd) Help() string {
	return c.help
}

func prettyKVPair(w io.Writer, pair *api.KVPair, base64EncodeValue bool, keysOnly bool) error {
	tw := tabwriter.NewWriter(w, 0, 2, 6, ' ', 0)
	fmt.Fprintf(tw, "CreateIndex\t%d\n", pair.CreateIndex)
	fmt.Fprintf(tw, "Flags\t%d\n", pair.Flags)
	fmt.Fprintf(tw, "Key\t%s\n", pair.Key)
	fmt.Fprintf(tw, "LockIndex\t%d\n", pair.LockIndex)
	fmt.Fprintf(tw, "ModifyIndex\t%d\n", pair.ModifyIndex)
	if pair.Session == "" {
		fmt.Fprint(tw, "Session\t-\n")
	} else {
		fmt.Fprintf(tw, "Session\t%s\n", pair.Session)
	}
	if pair.Partition != "" {
		fmt.Fprintf(tw, "Partition\t%s\n", pair.Partition)
	}
	if pair.Namespace != "" {
		fmt.Fprintf(tw, "Namespace\t%s\n", pair.Namespace)
	}
	if !keysOnly && base64EncodeValue {
		fmt.Fprintf(tw, "Value\t%s", base64.StdEncoding.EncodeToString(pair.Value))
	} else if !keysOnly {
		fmt.Fprintf(tw, "Value\t%s", pair.Value)
	}
	return tw.Flush()
}

const (
	synopsis = "Retrieves or lists data from the KV store"
	help     = `
Usage: consul kv get [options] [KEY_OR_PREFIX]

  Retrieves the value from Consul's key-value store at the given key name. If no
  key exists with that name, an error is returned. If a key exists with that
  name but has no data, nothing is returned. If the name or prefix is omitted,
  it defaults to "" which is the root of the key-value store.

  To retrieve the value for the key named "foo" in the key-value store:

      $ consul kv get foo

  This will return the original, raw value stored in Consul. To view detailed
  information about the key, specify the "-detailed" flag. This will output all
  known metadata about the key including ModifyIndex and any user-supplied
  flags:

      $ consul kv get -detailed foo

  To treat the path as a prefix and list all keys which start with the given
  prefix, specify the "-recurse" flag:

      $ consul kv get -recurse foo

  This will return all key-value pairs. To just list the keys which start with
  the specified prefix, use the "-keys" option instead:

      $ consul kv get -keys foo

  For a full list of options and examples, please see the Consul documentation.
`
)
