package guardiand

import (
	"bufio"
	"context"
	"encoding/csv"
	"encoding/hex"
	"fmt"
	"io"
	"log"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/cosmos/cosmos-sdk/types"
	"github.com/davecgh/go-spew/spew"
	ethcommon "github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/crypto"
	"github.com/mr-tron/base58"
	"github.com/spf13/pflag"
	"golang.org/x/crypto/sha3"

	"github.com/certusone/wormhole/node/pkg/common"
	gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1"
	publicrpcv1 "github.com/certusone/wormhole/node/pkg/proto/publicrpc/v1"
	"github.com/wormhole-foundation/wormhole/sdk"
	"github.com/wormhole-foundation/wormhole/sdk/vaa"

	"github.com/spf13/cobra"
	"github.com/status-im/keycard-go/hexutils"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/protobuf/encoding/prototext"

	nodev1 "github.com/certusone/wormhole/node/pkg/proto/node/v1"
)

var (
	clientSocketPath *string
	shouldBackfill   *bool
	unsafeDevnetMode *bool
)

func init() {
	// Shared flags for all admin commands
	pf := pflag.NewFlagSet("commonAdminFlags", pflag.ContinueOnError)
	clientSocketPath = pf.String("socket", "", "gRPC admin server socket to connect to")
	err := cobra.MarkFlagRequired(pf, "socket")
	if err != nil {
		panic(err)
	}

	shouldBackfill = AdminClientFindMissingMessagesCmd.Flags().Bool(
		"backfill", false, "backfill missing VAAs from public RPC")

	AdminClientInjectGuardianSetUpdateCmd.Flags().AddFlagSet(pf)
	AdminClientFindMissingMessagesCmd.Flags().AddFlagSet(pf)
	AdminClientListNodes.Flags().AddFlagSet(pf)
	DumpVAAByMessageID.Flags().AddFlagSet(pf)
	DumpRPCs.Flags().AddFlagSet(pf)
	SendObservationRequest.Flags().AddFlagSet(pf)
	ClientChainGovernorStatusCmd.Flags().AddFlagSet(pf)
	ClientChainGovernorReloadCmd.Flags().AddFlagSet(pf)
	ClientChainGovernorDropPendingVAACmd.Flags().AddFlagSet(pf)
	ClientChainGovernorReleasePendingVAACmd.Flags().AddFlagSet(pf)
	ClientChainGovernorResetReleaseTimerCmd.Flags().AddFlagSet(pf)
	PurgePythNetVaasCmd.Flags().AddFlagSet(pf)
	SignExistingVaaCmd.Flags().AddFlagSet(pf)
	SignExistingVaasFromCSVCmd.Flags().AddFlagSet(pf)
	GetAndObserveMissingVAAs.Flags().AddFlagSet(pf)

	adminClientSignWormchainAddressFlags := pflag.NewFlagSet("adminClientSignWormchainAddressFlags", pflag.ContinueOnError)
	unsafeDevnetMode = adminClientSignWormchainAddressFlags.Bool("unsafeDevMode", false, "Run in unsafe devnet mode")
	AdminClientSignWormchainAddress.Flags().AddFlagSet(adminClientSignWormchainAddressFlags)

	AdminCmd.AddCommand(AdminClientInjectGuardianSetUpdateCmd)
	AdminCmd.AddCommand(AdminClientFindMissingMessagesCmd)
	AdminCmd.AddCommand(AdminClientGovernanceVAAVerifyCmd)
	AdminCmd.AddCommand(AdminClientListNodes)
	AdminCmd.AddCommand(AdminClientSignWormchainAddress)
	AdminCmd.AddCommand(DumpVAAByMessageID)
	AdminCmd.AddCommand(DumpRPCs)
	AdminCmd.AddCommand(SendObservationRequest)
	AdminCmd.AddCommand(ClientChainGovernorStatusCmd)
	AdminCmd.AddCommand(ClientChainGovernorReloadCmd)
	AdminCmd.AddCommand(ClientChainGovernorDropPendingVAACmd)
	AdminCmd.AddCommand(ClientChainGovernorReleasePendingVAACmd)
	AdminCmd.AddCommand(ClientChainGovernorResetReleaseTimerCmd)
	AdminCmd.AddCommand(PurgePythNetVaasCmd)
	AdminCmd.AddCommand(SignExistingVaaCmd)
	AdminCmd.AddCommand(SignExistingVaasFromCSVCmd)
	AdminCmd.AddCommand(Keccak256Hash)
	AdminCmd.AddCommand(GetAndObserveMissingVAAs)
}

var AdminCmd = &cobra.Command{
	Use:   "admin",
	Short: "Guardian node admin commands",
}

var AdminClientSignWormchainAddress = &cobra.Command{
	Use:   "sign-wormchain-address [/path/to/guardianKey] [wormchain-validator-address]",
	Short: "Sign a wormchain validator address.  Only sign the address that you control the key for and will be for your validator.",
	RunE:  runSignWormchainValidatorAddress,
	Args:  cobra.ExactArgs(2),
}

var AdminClientInjectGuardianSetUpdateCmd = &cobra.Command{
	Use:   "governance-vaa-inject [FILENAME]",
	Short: "Inject and sign a governance VAA from a prototxt file (see docs!)",
	Run:   runInjectGovernanceVAA,
	Args:  cobra.ExactArgs(1),
}

var AdminClientFindMissingMessagesCmd = &cobra.Command{
	Use:   "find-missing-messages [CHAIN_ID] [EMITTER_ADDRESS_HEX]",
	Short: "Find sequence number gaps for the given chain ID and emitter address",
	Run:   runFindMissingMessages,
	Args:  cobra.ExactArgs(2),
}

var DumpVAAByMessageID = &cobra.Command{
	Use:   "dump-vaa-by-message-id [MESSAGE_ID]",
	Short: "Retrieve a VAA by message ID (chain/emitter/seq) and decode and dump the VAA",
	Run:   runDumpVAAByMessageID,
	Args:  cobra.ExactArgs(1),
}

var SendObservationRequest = &cobra.Command{
	Use:   "send-observation-request [CHAIN_ID|CHAIN_NAME] [TX_HASH_HEX]",
	Short: "Broadcast an observation request for the given chain ID and chain-specific tx_hash",
	Run:   runSendObservationRequest,
	Args:  cobra.ExactArgs(2),
}

var ClientChainGovernorStatusCmd = &cobra.Command{
	Use:   "governor-status",
	Short: "Displays the status of the chain governor",
	Run:   runChainGovernorStatus,
	Args:  cobra.ExactArgs(0),
}

var ClientChainGovernorReloadCmd = &cobra.Command{
	Use:   "governor-reload",
	Short: "Clears the chain governor history and reloads it from the database",
	Run:   runChainGovernorReload,
	Args:  cobra.ExactArgs(0),
}

var ClientChainGovernorDropPendingVAACmd = &cobra.Command{
	Use:   "governor-drop-pending-vaa [VAA_ID]",
	Short: "Removes the specified VAA (chain/emitter/seq) from the chain governor pending list",
	Run:   runChainGovernorDropPendingVAA,
	Args:  cobra.ExactArgs(1),
}

var ClientChainGovernorReleasePendingVAACmd = &cobra.Command{
	Use:   "governor-release-pending-vaa [VAA_ID]",
	Short: "Releases the specified VAA (chain/emitter/seq) from the chain governor pending list, publishing it immediately",
	Run:   runChainGovernorReleasePendingVAA,
	Args:  cobra.ExactArgs(1),
}

var ClientChainGovernorResetReleaseTimerCmd = &cobra.Command{
	Use:   "governor-reset-release-timer [VAA_ID]",
	Short: "Resets the release timer for a chain governor pending VAA, extending it to the configured maximum",
	Run:   runChainGovernorResetReleaseTimer,
	Args:  cobra.ExactArgs(1),
}

var PurgePythNetVaasCmd = &cobra.Command{
	Use:   "purge-pythnet-vaas [DAYS_OLD] <logonly>",
	Short: "Deletes PythNet VAAs from the database that are more than [DAYS_OLD] days only (if logonly is specified, doesn't delete anything)",
	Run:   runPurgePythNetVaas,
	Args:  cobra.RangeArgs(1, 2),
}

var SignExistingVaaCmd = &cobra.Command{
	Use:   "sign-existing-vaa [VAA] [NEW_GUARDIANS] [NEW_GUARDIAN_SET_INDEX]",
	Short: "Signs an existing VAA for a new guardian set using the local guardian key. This only works if the new VAA would have quorum.",
	Run:   runSignExistingVaa,
	Args:  cobra.ExactArgs(3),
}

var SignExistingVaasFromCSVCmd = &cobra.Command{
	Use:   "sign-existing-vaas-csv [IN_FILE] [OUT_FILE] [NEW_GUARDIANS] [NEW_GUARDIAN_SET_INDEX]",
	Short: "Signs a CSV [VAA_ID,VAA_HEX] of existing VAAs for a new guardian set using the local guardian key and writes it to a new CSV. VAAs that don't have quorum on the new set will be dropped.",
	Run:   runSignExistingVaasFromCSV,
	Args:  cobra.ExactArgs(4),
}

var DumpRPCs = &cobra.Command{
	Use:   "dump-rpcs",
	Short: "Displays the RPCs in use by the guardian",
	Run:   runDumpRPCs,
	Args:  cobra.ExactArgs(0),
}

var GetAndObserveMissingVAAs = &cobra.Command{
	Use:   "get-and-observe-missing-vaas [URL] [API_KEY]",
	Short: "Get the list of missing VAAs from a cloud function and try to reobserve them.",
	Run:   runGetAndObserveMissingVAAs,
	Args:  cobra.ExactArgs(2),
}

var Keccak256Hash = &cobra.Command{
	Use:   "keccak256",
	Short: "Compute legacy keccak256 hash",
	Run:   runKeccak256Hash,
	Args:  cobra.ExactArgs(0),
}

func getAdminClient(ctx context.Context, addr string) (*grpc.ClientConn, nodev1.NodePrivilegedServiceClient, error) {
	conn, err := grpc.DialContext(ctx, fmt.Sprintf("unix:///%s", addr), grpc.WithTransportCredentials(insecure.NewCredentials()))

	if err != nil {
		log.Fatalf("failed to connect to %s: %v", addr, err)
	}

	c := nodev1.NewNodePrivilegedServiceClient(conn)
	return conn, c, err
}

func getPublicRPCServiceClient(ctx context.Context, addr string) (*grpc.ClientConn, publicrpcv1.PublicRPCServiceClient, error) {
	conn, err := grpc.DialContext(ctx, fmt.Sprintf("unix:///%s", addr), grpc.WithTransportCredentials(insecure.NewCredentials()))

	if err != nil {
		log.Fatalf("failed to connect to %s: %v", addr, err)
	}

	c := publicrpcv1.NewPublicRPCServiceClient(conn)
	return conn, c, err
}

func runSignWormchainValidatorAddress(cmd *cobra.Command, args []string) error {
	guardianKeyPath := args[0]
	wormchainAddress := args[1]
	if !strings.HasPrefix(wormchainAddress, "wormhole") || strings.HasPrefix(wormchainAddress, "wormholeval") {
		return fmt.Errorf("must provide a bech32 address that has 'wormhole' prefix")
	}
	gk, err := common.LoadGuardianKey(guardianKeyPath, *unsafeDevnetMode)
	if err != nil {
		return fmt.Errorf("failed to load guardian key: %w", err)
	}
	addr, err := types.GetFromBech32(wormchainAddress, "wormhole")
	if err != nil {
		return fmt.Errorf("failed to decode wormchain address: %w", err)
	}
	// Hash and sign address
	addrHash := crypto.Keccak256Hash(sdk.SignedWormchainAddressPrefix, addr)
	sig, err := crypto.Sign(addrHash[:], gk)
	if err != nil {
		return fmt.Errorf("failed to sign wormchain address: %w", err)
	}
	fmt.Println(hex.EncodeToString(sig))
	return nil
}

func runInjectGovernanceVAA(cmd *cobra.Command, args []string) {
	path := args[0]
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	conn, c, err := getAdminClient(ctx, *clientSocketPath)
	if err != nil {
		log.Fatalf("failed to get admin client: %v", err)
	}
	defer conn.Close()

	b, err := os.ReadFile(path)
	if err != nil {
		log.Fatalf("failed to read file: %v", err)
	}

	var msg nodev1.InjectGovernanceVAARequest
	err = prototext.Unmarshal(b, &msg)
	if err != nil {
		log.Fatalf("failed to deserialize: %v", err)
	}

	resp, err := c.InjectGovernanceVAA(ctx, &msg)
	if err != nil {
		log.Fatalf("failed to submit governance VAA: %v", err)
	}

	for _, digest := range resp.Digests {
		log.Printf("VAA successfully injected with digest %s", hexutils.BytesToHex(digest))
	}
}

func runFindMissingMessages(cmd *cobra.Command, args []string) {
	chainID, err := strconv.Atoi(args[0])
	if err != nil {
		log.Fatalf("invalid chain ID: %v", err)
	}
	emitterAddress := args[1]

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
	defer cancel()

	conn, c, err := getAdminClient(ctx, *clientSocketPath)
	if err != nil {
		log.Fatalf("failed to get admin client: %v", err)
	}
	defer conn.Close()

	msg := nodev1.FindMissingMessagesRequest{
		EmitterChain:   uint32(chainID),
		EmitterAddress: emitterAddress,
		RpcBackfill:    *shouldBackfill,
		BackfillNodes:  sdk.PublicRPCEndpoints,
	}
	resp, err := c.FindMissingMessages(ctx, &msg)
	if err != nil {
		log.Fatalf("failed to run find FindMissingMessages RPC: %v", err)
	}

	for _, id := range resp.MissingMessages {
		fmt.Println(id)
	}

	log.Printf("processed %s sequences %d to %d (%d gaps)",
		emitterAddress, resp.FirstSequence, resp.LastSequence, len(resp.MissingMessages))
}

// runDumpVAAByMessageID uses GetSignedVAA to request the given message,
// then decode and dump the VAA.
func runDumpVAAByMessageID(cmd *cobra.Command, args []string) {
	// Parse the {chain,emitter,seq} string.
	parts := strings.Split(args[0], "/")
	if len(parts) != 3 {
		log.Fatalf("invalid message ID: %s", args[0])
	}
	chainID, err := strconv.ParseUint(parts[0], 10, 32)
	if err != nil {
		log.Fatalf("invalid chain ID: %v", err)
	}
	emitterAddress := parts[1]
	seq, err := strconv.ParseUint(parts[2], 10, 64)
	if err != nil {
		log.Fatalf("invalid sequence number: %v", err)
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	conn, c, err := getPublicRPCServiceClient(ctx, *clientSocketPath)
	if err != nil {
		log.Fatalf("failed to get public RPC service client: %v", err)
	}
	defer conn.Close()

	msg := publicrpcv1.GetSignedVAARequest{
		MessageId: &publicrpcv1.MessageID{
			EmitterChain:   publicrpcv1.ChainID(chainID),
			EmitterAddress: emitterAddress,
			Sequence:       seq,
		},
	}
	resp, err := c.GetSignedVAA(ctx, &msg)
	if err != nil {
		log.Fatalf("failed to run GetSignedVAA RPC: %v", err)
	}

	v, err := vaa.Unmarshal(resp.VaaBytes)
	if err != nil {
		log.Fatalf("failed to decode VAA: %v", err)
	}

	log.Printf("VAA with digest %s: %+v\n", v.HexDigest(), spew.Sdump(v))
	fmt.Printf("Bytes:\n%s\n", hex.EncodeToString(resp.VaaBytes))
}

func runSendObservationRequest(cmd *cobra.Command, args []string) {
	chainID, err := parseChainID(args[0])
	if err != nil {
		log.Fatalf("invalid chain ID: %v", err)
	}

	// Support tx with or without leading 0x so copy / pasta
	// from monitoring tools is easier.
	txHash, err := hex.DecodeString(strings.TrimPrefix(args[1], "0x"))
	if err != nil {
		txHash, err = base58.Decode(args[1])
		if err != nil {
			log.Fatalf("invalid transaction hash (neither hex nor base58): %v", err)
		}
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	conn, c, err := getAdminClient(ctx, *clientSocketPath)
	if err != nil {
		log.Fatalf("failed to get admin client: %v", err)
	}
	defer conn.Close()

	_, err = c.SendObservationRequest(ctx, &nodev1.SendObservationRequestRequest{
		ObservationRequest: &gossipv1.ObservationRequest{
			ChainId: uint32(chainID),
			TxHash:  txHash,
		},
	})
	if err != nil {
		log.Fatalf("failed to send observation request: %v", err)
	}
}

func runDumpRPCs(cmd *cobra.Command, args []string) {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	conn, c, err := getAdminClient(ctx, *clientSocketPath)
	if err != nil {
		log.Fatalf("failed to get admin client: %v", err)
	}
	defer conn.Close()

	resp, err := c.DumpRPCs(ctx, &nodev1.DumpRPCsRequest{})
	if err != nil {
		log.Fatalf("failed to run dump-rpcs: %s", err)
	}

	for parm, rpc := range resp.Response {
		fmt.Println(parm, " = [", rpc, "]")
	}
}

func runGetAndObserveMissingVAAs(cmd *cobra.Command, args []string) {
	url := args[0]
	if !strings.HasPrefix(url, "https://") {
		log.Fatalf("invalid url: %s", url)
	}
	apiKey := args[1]
	if len(apiKey) == 0 {
		log.Fatalf("missing api key")
	}
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	conn, c, err := getAdminClient(ctx, *clientSocketPath)
	if err != nil {
		log.Fatalf("failed to get admin client: %v", err)
	}
	defer conn.Close()

	cmdInfo := nodev1.GetAndObserveMissingVAAsRequest{
		Url:    url,
		ApiKey: apiKey,
	}
	resp, err := c.GetAndObserveMissingVAAs(ctx, &cmdInfo)
	if err != nil {
		log.Fatalf("failed to run get-missing-vaas: %s", err)
	}

	fmt.Println(resp.GetResponse())
}

func runChainGovernorStatus(cmd *cobra.Command, args []string) {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	conn, c, err := getAdminClient(ctx, *clientSocketPath)
	if err != nil {
		log.Fatalf("failed to get admin client: %v", err)
	}
	defer conn.Close()

	msg := nodev1.ChainGovernorStatusRequest{}
	resp, err := c.ChainGovernorStatus(ctx, &msg)
	if err != nil {
		log.Fatalf("failed to run ChainGovernorStatus RPC: %s", err)
	}

	fmt.Println(resp.Response)
}

func runChainGovernorReload(cmd *cobra.Command, args []string) {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	conn, c, err := getAdminClient(ctx, *clientSocketPath)
	if err != nil {
		log.Fatalf("failed to get admin client: %v", err)
	}
	defer conn.Close()

	msg := nodev1.ChainGovernorReloadRequest{}
	resp, err := c.ChainGovernorReload(ctx, &msg)
	if err != nil {
		log.Fatalf("failed to run ChainGovernorReload RPC: %s", err)
	}

	fmt.Println(resp.Response)
}

func runChainGovernorDropPendingVAA(cmd *cobra.Command, args []string) {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	conn, c, err := getAdminClient(ctx, *clientSocketPath)
	if err != nil {
		log.Fatalf("failed to get admin client: %v", err)
	}
	defer conn.Close()

	msg := nodev1.ChainGovernorDropPendingVAARequest{
		VaaId: args[0],
	}
	resp, err := c.ChainGovernorDropPendingVAA(ctx, &msg)
	if err != nil {
		log.Fatalf("failed to run ChainGovernorDropPendingVAA RPC: %s", err)
	}

	fmt.Println(resp.Response)
}

func runChainGovernorReleasePendingVAA(cmd *cobra.Command, args []string) {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	conn, c, err := getAdminClient(ctx, *clientSocketPath)
	if err != nil {
		log.Fatalf("failed to get admin client: %v", err)
	}
	defer conn.Close()

	msg := nodev1.ChainGovernorReleasePendingVAARequest{
		VaaId: args[0],
	}
	resp, err := c.ChainGovernorReleasePendingVAA(ctx, &msg)
	if err != nil {
		log.Fatalf("failed to run ChainGovernorReleasePendingVAA RPC: %s", err)
	}

	fmt.Println(resp.Response)
}

func runChainGovernorResetReleaseTimer(cmd *cobra.Command, args []string) {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	conn, c, err := getAdminClient(ctx, *clientSocketPath)
	if err != nil {
		log.Fatalf("failed to get admin client: %v", err)
	}
	defer conn.Close()

	msg := nodev1.ChainGovernorResetReleaseTimerRequest{
		VaaId: args[0],
	}
	resp, err := c.ChainGovernorResetReleaseTimer(ctx, &msg)
	if err != nil {
		log.Fatalf("failed to run ChainGovernorResetReleaseTimer RPC: %s", err)
	}

	fmt.Println(resp.Response)
}

func runPurgePythNetVaas(cmd *cobra.Command, args []string) {
	daysOld, err := strconv.Atoi(args[0])
	if err != nil {
		log.Fatalf("invalid DAYS_OLD: %v", err)
	}

	if daysOld < 0 {
		log.Fatalf("DAYS_OLD may not be negative")
	}

	logOnly := false
	if len(args) > 1 {
		if args[1] != "logonly" {
			log.Fatalf("invalid option, only \"logonly\" is supported")
		}

		logOnly = true
	}

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	conn, c, err := getAdminClient(ctx, *clientSocketPath)
	if err != nil {
		log.Fatalf("failed to get admin client: %v", err)
	}
	defer conn.Close()

	msg := nodev1.PurgePythNetVaasRequest{
		DaysOld: uint64(daysOld),
		LogOnly: logOnly,
	}
	resp, err := c.PurgePythNetVaas(ctx, &msg)
	if err != nil {
		log.Fatalf("failed to run PurgePythNetVaas RPC: %s", err)
	}

	fmt.Println(resp.Response)
}

func runSignExistingVaa(cmd *cobra.Command, args []string) {
	existingVAA := ethcommon.Hex2Bytes(args[0])
	if len(existingVAA) == 0 {
		log.Fatalf("vaa hex invalid")
	}

	newGsStrings := strings.Split(args[1], ",")

	newGsIndex, err := strconv.ParseUint(args[2], 10, 32)
	if err != nil {
		log.Fatalf("invalid new guardian set index")
	}

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	conn, c, err := getAdminClient(ctx, *clientSocketPath)
	if err != nil {
		log.Fatalf("failed to get admin client: %v", err)
	}
	defer conn.Close()

	msg := nodev1.SignExistingVAARequest{
		Vaa:                 existingVAA,
		NewGuardianAddrs:    newGsStrings,
		NewGuardianSetIndex: uint32(newGsIndex),
	}
	resp, err := c.SignExistingVAA(ctx, &msg)
	if err != nil {
		log.Fatalf("failed to run SignExistingVAA RPC: %s", err)
	}

	fmt.Println(hex.EncodeToString(resp.Vaa))
}

func runSignExistingVaasFromCSV(cmd *cobra.Command, args []string) {
	oldVAAFile, err := os.Open(args[0])
	if err != nil {
		log.Fatalf("failed to read old VAA db: %v", err)
	}
	defer oldVAAFile.Close()

	newVAAFile, err := os.OpenFile(args[1], os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
	if err != nil {
		log.Fatalf("failed to create new VAA db: %v", err)
	}
	defer newVAAFile.Close()
	newVAAWriter := csv.NewWriter(newVAAFile)

	newGsStrings := strings.Split(args[2], ",")

	newGsIndex, err := strconv.ParseUint(args[3], 10, 32)
	if err != nil {
		log.Fatalf("invalid new guardian set index")
	}

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	conn, c, err := getAdminClient(ctx, *clientSocketPath)
	if err != nil {
		log.Fatalf("failed to get admin client: %v", err)
	}
	defer conn.Close()

	// Scan the CSV once to make sure it won't fail while reading unless raced
	oldVAAReader := csv.NewReader(oldVAAFile)
	numOldVAAs := 0
	for {
		row, err := oldVAAReader.Read()
		if err != nil {
			if err == io.EOF {
				break
			}
			log.Fatalf("failed to parse VAA CSV: %v", err)
		}
		if len(row) != 2 {
			log.Fatalf("row [%d] does not have 2 elements", numOldVAAs)
		}
		numOldVAAs++
	}

	// Reset reader
	_, err = oldVAAFile.Seek(0, io.SeekStart)
	if err != nil {
		log.Fatalf("failed to seek back in CSV file: %v", err)
	}
	oldVAAReader = csv.NewReader(oldVAAFile)

	counter, i := 0, 0
	for {
		row, err := oldVAAReader.Read()
		if err != nil {
			if err == io.EOF {
				break
			}
			log.Fatalf("failed to parse VAA CSV: %v", err)
		}
		if len(row) != 2 {
			log.Fatalf("row [%d] does not have 2 elements", i)
		}
		i++

		if i%10 == 0 {
			log.Printf("Processing VAA %d/%d", i, numOldVAAs)
		}

		vaaBytes := ethcommon.Hex2Bytes(row[1])
		msg := nodev1.SignExistingVAARequest{
			Vaa:                 vaaBytes,
			NewGuardianAddrs:    newGsStrings,
			NewGuardianSetIndex: uint32(newGsIndex),
		}
		resp, err := c.SignExistingVAA(ctx, &msg)
		if err != nil {
			log.Printf("signing VAA (%s)[%d] failed - skipping: %v", row[0], i, err)
			continue
		}
		err = newVAAWriter.Write([]string{row[0], hex.EncodeToString(resp.Vaa)})
		if err != nil {
			log.Fatalf("failed to write new VAA to out db: %v", err)
		}
		counter++
	}

	log.Printf("Successfully signed %d out of %d VAAs", counter, numOldVAAs)
	newVAAWriter.Flush()
}

// This exposes keccak256 as a command line utility, mostly for validating goverance messages
// that use this hash.  There isn't any common utility that computes this since this is nonstandard outside of evm.
// It is used similar to other hashing utilities, e.g. `cat <file> | guardiand admin keccak256`.
func runKeccak256Hash(cmd *cobra.Command, args []string) {
	reader := bufio.NewReader(os.Stdin)
	hash := sha3.NewLegacyKeccak256()
	// ~10 MB chunks
	buf := make([]byte, 10*1024*1024)
	for {
		count, err := reader.Read(buf)
		if err != nil && err != io.EOF {
			log.Fatalf("could not read: %v", err)
		}
		_, errHash := hash.Write(buf[:count])
		if errHash != nil {
			log.Fatalf("could not hash: %v", errHash)
		}
		if err == io.EOF {
			break
		}
	}
	digest := hash.Sum([]byte{})
	fmt.Printf("%s", hex.EncodeToString(digest))
}
