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

package command

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

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

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

type OperatorUnsealCommand struct {
	*BaseCommand

	flagReset   bool
	flagMigrate bool

	testOutput io.Writer // for tests
}

func (c *OperatorUnsealCommand) Synopsis() string {
	return "Unseals the Vault server"
}

func (c *OperatorUnsealCommand) Help() string {
	helpText := `
Usage: vault operator unseal [options] [KEY]

  Provide a portion of the root key to unseal a Vault server. Vault starts
  in a sealed state. It cannot perform operations until it is unsealed. This
  command accepts a portion of the root key (an "unseal key").

  The unseal key can be supplied as an argument to the command, but this is
  not recommended as the unseal key will be available in your history:

      $ vault operator unseal IXyR0OJnSFobekZMMCKCoVEpT7wI6l+USMzE3IcyDyo=

  Instead, run the command with no arguments and it will prompt for the key:

      $ vault operator unseal
      Key (will be hidden): IXyR0OJnSFobekZMMCKCoVEpT7wI6l+USMzE3IcyDyo=

` + c.Flags().Help()

	return strings.TrimSpace(helpText)
}

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

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

	f.BoolVar(&BoolVar{
		Name:       "reset",
		Aliases:    []string{},
		Target:     &c.flagReset,
		Default:    false,
		EnvVar:     "",
		Completion: complete.PredictNothing,
		Usage:      "Discard any previously entered keys to the unseal process.",
	})

	f.BoolVar(&BoolVar{
		Name:       "migrate",
		Aliases:    []string{},
		Target:     &c.flagMigrate,
		Default:    false,
		EnvVar:     "",
		Completion: complete.PredictNothing,
		Usage:      "Indicate that this share is provided with the intent that it is part of a seal migration process.",
	})

	return set
}

func (c *OperatorUnsealCommand) AutocompleteArgs() complete.Predictor {
	return complete.PredictAnything
}

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

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

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

	unsealKey := ""

	args = f.Args()
	switch len(args) {
	case 0:
		// We will prompt for the unseal key later
	case 1:
		unsealKey = strings.TrimSpace(args[0])
	default:
		c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
		return 1
	}

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

	if c.flagReset {
		status, err := client.Sys().ResetUnsealProcess()
		if err != nil {
			c.UI.Error(fmt.Sprintf("Error resetting unseal process: %s", err))
			return 2
		}
		return OutputSealStatus(c.UI, client, status)
	}

	if unsealKey == "" {
		// Override the output
		writer := (io.Writer)(os.Stdout)
		if c.testOutput != nil {
			writer = c.testOutput
		}

		fmt.Fprintf(writer, "Unseal Key (will be hidden): ")
		value, err := password.Read(os.Stdin)
		fmt.Fprintf(writer, "\n")
		if err != nil {
			c.UI.Error(wrapAtLength(fmt.Sprintf("An error occurred attempting to "+
				"ask for an unseal key. The raw error message is shown below, but "+
				"usually this is because you attempted to pipe a value into the "+
				"unseal command or you are executing outside of a terminal (tty). "+
				"You should run the unseal command from a terminal for maximum "+
				"security. If this is not an option, the unseal key can be provided "+
				"as the first argument to the unseal command. The raw error "+
				"was:\n\n%s", err)))
			return 1
		}
		unsealKey = strings.TrimSpace(value)
	}

	status, err := client.Sys().UnsealWithOptions(&api.UnsealOpts{
		Key:     unsealKey,
		Migrate: c.flagMigrate,
	})
	if err != nil {
		c.UI.Error(fmt.Sprintf("Error unsealing: %s", err))
		return 2
	}

	return OutputSealStatus(c.UI, client, status)
}
