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

package command

import (
	"bytes"
	"fmt"
	"io"
	"os"
	"strings"

	"github.com/hashicorp/go-secure-stdlib/password"
	"github.com/hashicorp/vault/api"
	"github.com/hashicorp/vault/helper/pgpkeys"
	"github.com/hashicorp/vault/sdk/helper/roottoken"
	"github.com/mitchellh/cli"
	"github.com/posener/complete"
)

var (
	_ cli.Command             = (*OperatorGenerateRootCommand)(nil)
	_ cli.CommandAutocomplete = (*OperatorGenerateRootCommand)(nil)
)

type generateRootKind int

const (
	generateRootRegular generateRootKind = iota
	generateRootDR
	generateRootRecovery
)

type OperatorGenerateRootCommand struct {
	*BaseCommand

	flagInit          bool
	flagCancel        bool
	flagStatus        bool
	flagDecode        string
	flagOTP           string
	flagPGPKey        string
	flagNonce         string
	flagGenerateOTP   bool
	flagDRToken       bool
	flagRecoveryToken bool

	testStdin io.Reader // for tests
}

func (c *OperatorGenerateRootCommand) Synopsis() string {
	return "Generates a new root, DR operation, or recovery token"
}

func (c *OperatorGenerateRootCommand) Help() string {
	helpText := `
Usage: vault operator generate-root [options] -init [-otp=...] [-pgp-key=...]
       vault operator generate-root [options] [-nonce=... KEY]
       vault operator generate-root [options] -decode=... -otp=...
       vault operator generate-root [options] -generate-otp
       vault operator generate-root [options] -status
       vault operator generate-root [options] -cancel

  Generates a new root token by combining a quorum of share holders.

  This command is unusual, as it is effectively six separate subcommands,
  selected via the options -init, -decode, -generate-otp, -status, -cancel, 
  or the absence of any of the previous five options (which selects the 
  "provide a key share" form).

  With the -dr-token or -recovery-token options, a DR operation token or a
  recovery token is generated instead of a root token - the relevant option
  must be included in every form of the generate-root command.

  Form 1 (-init) - Start a token generation:

    When starting a root or privileged operation token generation, you must
    choose one of the following protection methods for how the token will be
    returned:

      - A base64-encoded one-time-password (OTP). The resulting token is XORed
        with this value when it is returned. Use the "-decode" form of this
        command to output the final value.

        The Vault server will generate a suitable OTP for you, and return it:

            $ vault operator generate-root -init

        Vault versions before 0.11.2, released in 2018, required you to
        generate your own OTP (see the "-generate-otp" form) and pass it in,
        but this is no longer necessary. The command is still supported for
        compatibility, though:

            $ vault operator generate-root -init -otp="..."

      - A PGP key. The resulting token is encrypted with this public key.
        The key may be specified as a path to a file, or a string of the
        form "keybase:<username>" to fetch the key from the keybase.io API.

            $ vault operator generate-root -init -pgp-key="..."

  Form 2 (no option) - Enter an unseal key to progress root token generation:

    In the sub-form intended for interactive use, the command will
    automatically look up the nonce of the currently active generation
    operation, and will prompt for the key to be entered:

        $ vault operator generate-root

    In the sub-form intended for automation, the operation nonce must be
    explicitly provided, and the key is provided directly on the command line

        $ vault operator generate-root -nonce=... KEY

    If key is specified as "-", the command will read from stdin.

  Form 3 (-decode) - Decode a generated token protected with an OTP:

        $ vault operator generate-root -decode=ENCODED_TOKEN -otp=OTP

    If encoded token is specified as "-", the command will read from stdin.

  Form 4 (-generate-otp) - Generate an OTP code for the final token:

        $ vault operator generate-root -generate-otp

    Since changes in Vault 0.11.2 in 2018, there is no longer any reason to
    use this form, as a suitable OTP will be returned as part of the "-init"
    command.

  Form 5 (-status) - Get the status of a token generation that is in progress:

        $ vault operator generate-root -status

    This form also returns the length of the a correct OTP, for the running
    version and configuration of Vault.

  Form 6 (-cancel) - Cancel a token generation that is in progress:

    This would be used to remove an in progress generation operation, so that
    a new one can be started with different parameters.

        $ vault operator generate-root -cancel

` + c.Flags().Help()
	return strings.TrimSpace(helpText)
}

func (c *OperatorGenerateRootCommand) Flags() *FlagSets {
	set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat)

	f := set.NewFlagSet("Command Options")

	f.BoolVar(&BoolVar{
		Name:       "init",
		Target:     &c.flagInit,
		Default:    false,
		EnvVar:     "",
		Completion: complete.PredictNothing,
		Usage: "Start a root token generation. This can only be done if " +
			"there is not currently one in progress.",
	})

	f.BoolVar(&BoolVar{
		Name:       "cancel",
		Target:     &c.flagCancel,
		Default:    false,
		EnvVar:     "",
		Completion: complete.PredictNothing,
		Usage: "Reset the root token generation progress. This will discard any " +
			"submitted unseal keys or configuration.",
	})

	f.BoolVar(&BoolVar{
		Name:       "status",
		Target:     &c.flagStatus,
		Default:    false,
		EnvVar:     "",
		Completion: complete.PredictNothing,
		Usage: "Print the status of the current attempt without providing an " +
			"unseal key.",
	})

	f.StringVar(&StringVar{
		Name:       "decode",
		Target:     &c.flagDecode,
		Default:    "",
		EnvVar:     "",
		Completion: complete.PredictAnything,
		Usage: "The value to decode; setting this triggers a decode operation. " +
			" If the value is \"-\" then read the encoded token from stdin.",
	})

	f.BoolVar(&BoolVar{
		Name:       "generate-otp",
		Target:     &c.flagGenerateOTP,
		Default:    false,
		EnvVar:     "",
		Completion: complete.PredictNothing,
		Usage: "Generate and print a high-entropy one-time-password (OTP) " +
			"suitable for use with the \"-init\" flag.",
	})

	f.BoolVar(&BoolVar{
		Name:       "dr-token",
		Target:     &c.flagDRToken,
		Default:    false,
		EnvVar:     "",
		Completion: complete.PredictNothing,
		Usage: "Set this flag to do generate root operations on DR operation " +
			"tokens.",
	})

	f.BoolVar(&BoolVar{
		Name:       "recovery-token",
		Target:     &c.flagRecoveryToken,
		Default:    false,
		EnvVar:     "",
		Completion: complete.PredictNothing,
		Usage: "Set this flag to do generate root operations on recovery " +
			"tokens.",
	})

	f.StringVar(&StringVar{
		Name:       "otp",
		Target:     &c.flagOTP,
		Default:    "",
		EnvVar:     "",
		Completion: complete.PredictAnything,
		Usage:      "OTP code to use with \"-decode\" or \"-init\".",
	})

	f.VarFlag(&VarFlag{
		Name:       "pgp-key",
		Value:      (*pgpkeys.PubKeyFileFlag)(&c.flagPGPKey),
		Default:    "",
		EnvVar:     "",
		Completion: complete.PredictAnything,
		Usage: "Path to a file on disk containing a binary or base64-encoded " +
			"public PGP key. This can also be specified as a Keybase username " +
			"using the format \"keybase:<username>\". When supplied, the generated " +
			"root token will be encrypted and base64-encoded with the given public " +
			"key. Must be used with \"-init\".",
	})

	f.StringVar(&StringVar{
		Name:       "nonce",
		Target:     &c.flagNonce,
		Default:    "",
		EnvVar:     "",
		Completion: complete.PredictAnything,
		Usage: "Nonce value returned at initialization. The same nonce value " +
			"must be provided with each unseal or recovery key. Only needed " +
			"when providing an unseal or recovery key.",
	})

	return set
}

func (c *OperatorGenerateRootCommand) AutocompleteArgs() complete.Predictor {
	return nil
}

func (c *OperatorGenerateRootCommand) AutocompleteFlags() complete.Flags {
	return c.Flags().Completions()
}

func (c *OperatorGenerateRootCommand) Run(args []string) int {
	f := c.Flags()

	if err := f.Parse(args); err != nil {
		c.UI.Error(err.Error())
		return 1
	}

	args = f.Args()
	if len(args) > 1 {
		c.UI.Error(fmt.Sprintf("Too many arguments (expected 0-1, got %d)", len(args)))
		return 1
	}

	if c.flagDRToken && c.flagRecoveryToken {
		c.UI.Error("Both -recovery-token and -dr-token flags are set")
		return 1
	}

	client, err := c.Client()
	if err != nil {
		c.UI.Error(err.Error())
		return 2
	}

	kind := generateRootRegular
	switch {
	case c.flagDRToken:
		kind = generateRootDR
	case c.flagRecoveryToken:
		kind = generateRootRecovery
	}

	switch {
	case c.flagGenerateOTP:
		otp, code := c.generateOTP(client, kind)
		if code == 0 {
			switch Format(c.UI) {
			case "", "table":
				return PrintRaw(c.UI, otp)
			default:
				status := map[string]interface{}{
					"otp":        otp,
					"otp_length": len(otp),
				}
				return OutputData(c.UI, status)
			}
		}
		return code
	case c.flagDecode != "":
		return c.decode(client, c.flagDecode, c.flagOTP, kind)
	case c.flagCancel:
		return c.cancel(client, kind)
	case c.flagInit:
		return c.init(client, c.flagOTP, c.flagPGPKey, kind)
	case c.flagStatus:
		return c.status(client, kind)
	default:
		// If there are no other flags, prompt for an unseal key.
		key := ""
		if len(args) > 0 {
			key = strings.TrimSpace(args[0])
		}
		return c.provide(client, key, kind)
	}
}

// generateOTP generates a suitable OTP code for generating a root token.
func (c *OperatorGenerateRootCommand) generateOTP(client *api.Client, kind generateRootKind) (string, int) {
	f := client.Sys().GenerateRootStatus
	switch kind {
	case generateRootDR:
		f = client.Sys().GenerateDROperationTokenStatus
	case generateRootRecovery:
		f = client.Sys().GenerateRecoveryOperationTokenStatus
	}

	status, err := f()
	if err != nil {
		c.UI.Error(fmt.Sprintf("Error getting root generation status: %s", err))
		return "", 2
	}

	otp, err := roottoken.GenerateOTP(status.OTPLength)
	var retCode int
	if err != nil {
		retCode = 2
		c.UI.Error(err.Error())
	} else {
		retCode = 0
	}
	return otp, retCode
}

// decode decodes the given value using the otp.
func (c *OperatorGenerateRootCommand) decode(client *api.Client, encoded, otp string, kind generateRootKind) int {
	if encoded == "" {
		c.UI.Error("Missing encoded value: use -decode=<string> to supply it")
		return 1
	}
	if otp == "" {
		c.UI.Error("Missing otp: use -otp to supply it")
		return 1
	}

	if encoded == "-" {
		// Pull our fake stdin if needed
		stdin := (io.Reader)(os.Stdin)
		if c.testStdin != nil {
			stdin = c.testStdin
		}

		var buf bytes.Buffer
		if _, err := io.Copy(&buf, stdin); err != nil {
			c.UI.Error(fmt.Sprintf("Failed to read from stdin: %s", err))
			return 1
		}

		encoded = buf.String()

		if encoded == "" {
			c.UI.Error("Missing encoded value. When using -decode=\"-\" value must be passed via stdin.")
			return 1
		}
	}

	f := client.Sys().GenerateRootStatus
	switch kind {
	case generateRootDR:
		f = client.Sys().GenerateDROperationTokenStatus
	case generateRootRecovery:
		f = client.Sys().GenerateRecoveryOperationTokenStatus
	}

	status, err := f()
	if err != nil {
		c.UI.Error(fmt.Sprintf("Error getting root generation status: %s", err))
		return 2
	}

	token, err := roottoken.DecodeToken(encoded, otp, status.OTPLength)
	if err != nil {
		c.UI.Error(fmt.Sprintf("Error decoding root token: %s", err))
		return 1
	}

	switch Format(c.UI) {
	case "", "table":
		return PrintRaw(c.UI, token)
	default:
		tokenJSON := map[string]interface{}{
			"token": token,
		}
		return OutputData(c.UI, tokenJSON)
	}
}

// init is used to start the generation process
func (c *OperatorGenerateRootCommand) init(client *api.Client, otp, pgpKey string, kind generateRootKind) int {
	// Validate incoming fields. Either OTP OR PGP keys must be supplied.
	if otp != "" && pgpKey != "" {
		c.UI.Error("Error initializing: cannot specify both -otp and -pgp-key")
		return 1
	}

	// Start the root generation
	f := client.Sys().GenerateRootInit
	switch kind {
	case generateRootDR:
		f = client.Sys().GenerateDROperationTokenInit
	case generateRootRecovery:
		f = client.Sys().GenerateRecoveryOperationTokenInit
	}
	status, err := f(otp, pgpKey)
	if err != nil {
		c.UI.Error(fmt.Sprintf("Error initializing root generation: %s", err))
		return 2
	}

	switch Format(c.UI) {
	case "table":
		return c.printStatus(status)
	default:
		return OutputData(c.UI, status)
	}
}

// provide prompts the user for the seal key and posts it to the update root
// endpoint. If this is the last unseal, this function outputs it.
func (c *OperatorGenerateRootCommand) provide(client *api.Client, key string, kind generateRootKind) int {
	f := client.Sys().GenerateRootStatus
	switch kind {
	case generateRootDR:
		f = client.Sys().GenerateDROperationTokenStatus
	case generateRootRecovery:
		f = client.Sys().GenerateRecoveryOperationTokenStatus
	}
	status, err := f()
	if err != nil {
		c.UI.Error(fmt.Sprintf("Error getting root generation status: %s", err))
		return 2
	}

	// Verify a root token generation is in progress. If there is not one in
	// progress, return an error instructing the user to start one.
	if !status.Started {
		c.UI.Error(wrapAtLength(
			"No root generation is in progress. Start a root generation by " +
				"running \"vault operator generate-root -init\"."))
		c.UI.Warn(wrapAtLength(fmt.Sprintf(
			"If starting root generation using the OTP method and generating "+
				"your own OTP, the length of the OTP string needs to be %d "+
				"characters in length.", status.OTPLength)))
		return 1
	}

	var nonce string

	switch key {
	case "-": // Read from stdin
		nonce = c.flagNonce

		// Pull our fake stdin if needed
		stdin := (io.Reader)(os.Stdin)
		if c.testStdin != nil {
			stdin = c.testStdin
		}

		var buf bytes.Buffer
		if _, err := io.Copy(&buf, stdin); err != nil {
			c.UI.Error(fmt.Sprintf("Failed to read from stdin: %s", err))
			return 1
		}

		key = buf.String()
	case "": // Prompt using the tty
		// Nonce value is not required if we are prompting via the terminal
		nonce = status.Nonce

		w := getWriterFromUI(c.UI)
		fmt.Fprintf(w, "Operation nonce: %s\n", nonce)
		fmt.Fprintf(w, "Unseal Key (will be hidden): ")
		key, err = password.Read(os.Stdin)
		fmt.Fprintf(w, "\n")
		if err != nil {
			if err == password.ErrInterrupted {
				c.UI.Error("user canceled")
				return 1
			}

			c.UI.Error(wrapAtLength(fmt.Sprintf("An error occurred attempting to "+
				"ask for the unseal key. The raw error message is shown below, but "+
				"usually this is because you attempted to pipe a value into the "+
				"command or you are executing outside of a terminal (tty). If you "+
				"want to pipe the value, pass \"-\" as the argument to read from "+
				"stdin. The raw error was: %s", err)))
			return 1
		}
	default: // Supplied directly as an arg
		nonce = c.flagNonce
	}

	// Trim any whitespace from they key, especially since we might have prompted
	// the user for it.
	key = strings.TrimSpace(key)

	// Verify we have a nonce value
	if nonce == "" {
		c.UI.Error("Missing nonce value: specify it via the -nonce flag")
		return 1
	}

	// Provide the key, this may potentially complete the update
	fUpd := client.Sys().GenerateRootUpdate
	switch kind {
	case generateRootDR:
		fUpd = client.Sys().GenerateDROperationTokenUpdate
	case generateRootRecovery:
		fUpd = client.Sys().GenerateRecoveryOperationTokenUpdate
	}
	status, err = fUpd(key, nonce)
	if err != nil {
		c.UI.Error(fmt.Sprintf("Error posting unseal key: %s", err))
		return 2
	}
	switch Format(c.UI) {
	case "table":
		return c.printStatus(status)
	default:
		return OutputData(c.UI, status)
	}
}

// cancel cancels the root token generation
func (c *OperatorGenerateRootCommand) cancel(client *api.Client, kind generateRootKind) int {
	f := client.Sys().GenerateRootCancel
	switch kind {
	case generateRootDR:
		f = client.Sys().GenerateDROperationTokenCancel
	case generateRootRecovery:
		f = client.Sys().GenerateRecoveryOperationTokenCancel
	}
	if err := f(); err != nil {
		c.UI.Error(fmt.Sprintf("Error canceling root token generation: %s", err))
		return 2
	}
	c.UI.Output("Success! Root token generation canceled (if it was started)")
	return 0
}

// status is used just to fetch and dump the status
func (c *OperatorGenerateRootCommand) status(client *api.Client, kind generateRootKind) int {
	f := client.Sys().GenerateRootStatus
	switch kind {
	case generateRootDR:
		f = client.Sys().GenerateDROperationTokenStatus
	case generateRootRecovery:
		f = client.Sys().GenerateRecoveryOperationTokenStatus
	}

	status, err := f()
	if err != nil {
		c.UI.Error(fmt.Sprintf("Error getting root generation status: %s", err))
		return 2
	}
	switch Format(c.UI) {
	case "table":
		return c.printStatus(status)
	default:
		return OutputData(c.UI, status)
	}
}

// printStatus dumps the status to output
func (c *OperatorGenerateRootCommand) printStatus(status *api.GenerateRootStatusResponse) int {
	out := []string{}
	out = append(out, fmt.Sprintf("Nonce | %s", status.Nonce))
	out = append(out, fmt.Sprintf("Started | %t", status.Started))
	out = append(out, fmt.Sprintf("Progress | %d/%d", status.Progress, status.Required))
	out = append(out, fmt.Sprintf("Complete | %t", status.Complete))
	if status.PGPFingerprint != "" {
		out = append(out, fmt.Sprintf("PGP Fingerprint | %s", status.PGPFingerprint))
	}
	switch {
	case status.EncodedToken != "":
		out = append(out, fmt.Sprintf("Encoded Token | %s", status.EncodedToken))
	case status.EncodedRootToken != "":
		out = append(out, fmt.Sprintf("Encoded Root Token | %s", status.EncodedRootToken))
	}
	if status.OTP != "" {
		c.UI.Warn(wrapAtLength("A One-Time-Password has been generated for you and is shown in the OTP field. You will need this value to decode the resulting root token, so keep it safe."))
		out = append(out, fmt.Sprintf("OTP | %s", status.OTP))
	}
	if status.OTPLength != 0 {
		out = append(out, fmt.Sprintf("OTP Length | %d", status.OTPLength))
	}

	output := columnOutput(out, nil)
	c.UI.Output(output)
	return 0
}
