// Package main provides the "admin" tool, which can perform various
// administrative actions (such as revoking certificates) against a Boulder
// deployment.
//
// Run "admin -h" for a list of flags and subcommands.
//
// Note that the admin tool runs in "dry-run" mode *by default*. All commands
// which mutate the database (either directly or via gRPC requests) will refuse
// to do so, and instead print log lines representing the work they would do,
// unless the "-dry-run=false" flag is passed.
package main

import (
	"context"
	"flag"
	"fmt"
	"os"
	"strings"

	"github.com/letsencrypt/boulder/cmd"
	"github.com/letsencrypt/boulder/features"
)

type Config struct {
	Admin struct {
		// DB controls the admin tool's direct connection to the database.
		DB cmd.DBConfig
		// TLS controls the TLS client the admin tool uses for gRPC connections.
		TLS cmd.TLSConfig

		RAService *cmd.GRPCClientConfig
		SAService *cmd.GRPCClientConfig

		DebugAddr string

		Features features.Config
	}

	Syslog        cmd.SyslogConfig
	OpenTelemetry cmd.OpenTelemetryConfig
}

// subcommand specifies the set of methods that a struct must implement to be
// usable as an admin subcommand.
type subcommand interface {
	// Desc should return a short (one-sentence) description of the subcommand for
	// use in help/usage strings.
	Desc() string
	// Flags should register command line flags on the provided flagset. These
	// should use the "TypeVar" methods on the provided flagset, targeting fields
	// on the subcommand struct, so that the results of command line parsing can
	// be used by other methods on the struct.
	Flags(*flag.FlagSet)
	// Run should do all of the subcommand's heavy lifting, with behavior gated on
	// the subcommand struct's member fields which have been populated from the
	// command line. The provided admin object can be used for access to external
	// services like the RA, SA, and configured logger.
	Run(context.Context, *admin) error
}

// main is the entry-point for the admin tool. We do not include admin in the
// suite of tools which are subcommands of the "boulder" binary, since it
// should be small and portable and standalone.
func main() {
	// Do setup as similarly as possible to all other boulder services, including
	// config parsing and stats and logging setup. However, the one downside of
	// not being bundled with the boulder binary is that we don't get config
	// validation for free.
	defer cmd.AuditPanic()

	// This is the registry of all subcommands that the admin tool can run.
	subcommands := map[string]subcommand{
		"revoke-cert":  &subcommandRevokeCert{},
		"block-key":    &subcommandBlockKey{},
		"update-email": &subcommandUpdateEmail{},
	}

	defaultUsage := flag.Usage
	flag.Usage = func() {
		defaultUsage()
		fmt.Printf("\nSubcommands:\n")
		for name, command := range subcommands {
			fmt.Printf("  %s\n", name)
			fmt.Printf("\t%s\n", command.Desc())
		}
		fmt.Print("\nYou can run \"admin <subcommand> -help\" to get usage for that subcommand.\n")
	}

	// Start by parsing just the global flags before we get to the subcommand, if
	// they're present.
	configFile := flag.String("config", "", "Path to the configuration file for this service (required)")
	dryRun := flag.Bool("dry-run", true, "Print actions instead of mutating the database")
	flag.Parse()

	// Figure out which subcommand they want us to run.
	unparsedArgs := flag.Args()
	if len(unparsedArgs) == 0 {
		flag.Usage()
		os.Exit(1)
	}

	subcommand, ok := subcommands[unparsedArgs[0]]
	if !ok {
		flag.Usage()
		os.Exit(1)
	}

	// Then parse the rest of the args according to the selected subcommand's
	// flags, and allow the global flags to be placed after the subcommand name.
	subflags := flag.NewFlagSet(unparsedArgs[0], flag.ExitOnError)
	subcommand.Flags(subflags)
	flag.VisitAll(func(f *flag.Flag) {
		// For each flag registered at the global/package level, also register it on
		// the subflags FlagSet. The `f.Value` here is a pointer to the same var
		// that the original global flag would populate, so the same variable can
		// be set either way.
		subflags.Var(f.Value, f.Name, f.Usage)
	})
	_ = subflags.Parse(unparsedArgs[1:])

	// With the flags all parsed, now we can parse our config and set up our admin
	// object.
	if *configFile == "" {
		flag.Usage()
		os.Exit(1)
	}

	a, err := newAdmin(*configFile, *dryRun)
	cmd.FailOnError(err, "creating admin object")

	// Finally, run the selected subcommand.
	if a.dryRun {
		a.log.AuditInfof("admin tool executing a dry-run with the following arguments: %q", strings.Join(os.Args, " "))
	} else {
		a.log.AuditInfof("admin tool executing with the following arguments: %q", strings.Join(os.Args, " "))
	}

	err = subcommand.Run(context.Background(), a)
	cmd.FailOnError(err, "executing subcommand")

	if a.dryRun {
		a.log.AuditInfof("admin tool has successfully completed executing a dry-run with the following arguments: %q", strings.Join(os.Args, " "))
		a.log.Info("Dry run complete. Pass -dry-run=false to mutate the database.")
	} else {
		a.log.AuditInfof("admin tool has successfully completed executing with the following arguments: %q", strings.Join(os.Args, " "))
	}
}
