// Copyright 2022 Molecula Corp. (DBA FeatureBase).
// SPDX-License-Identifier: Apache-2.0
package cmd

import (
	"context"
	"errors"
	"fmt"
	"io"
	"strings"

	pilosa "github.com/featurebasedb/featurebase/v3"
	"github.com/featurebasedb/featurebase/v3/ctl"
	"github.com/featurebasedb/featurebase/v3/logger"
	"github.com/spf13/cobra"
	"github.com/spf13/pflag"
	"github.com/spf13/viper"
)

// runner represents a thing, like any of the NewFooCommand we produce
// in ctl/*.go, which has a Run(context) error method, which is used
// to implement a command. We use this so we can just specify the
// object, rather than its run method, in calling usageErrorWrapper.
type runner interface {
	Run(context.Context) error
}

// UsageErrorWrapper takes a thing with a Run(context) error, and produces
// a func(*cobra.Command, []string) error from it which will run that
// command, and then set Cobra's SilenceUsage flag unless the returned
// error errors.Is() a ctl.UsageError.
func UsageErrorWrapper(inner runner) func(*cobra.Command, []string) error {
	return func(c *cobra.Command, args []string) error {
		return considerUsageError(c, inner.Run(context.Background()))
	}
}

// considerUsageError sets a command to silence usage errors if
// the given error is not a ctl.UsageError, then returns the
// unmodified error. It's here to let us write one-liner Run
// wrappers.
func considerUsageError(cmd *cobra.Command, err error) error {
	cmd.SilenceErrors = true
	if !errors.Is(err, ctl.ErrUsage) {
		cmd.SilenceUsage = true
	}
	return err
}

func NewRootCommand(stderr io.Writer) *cobra.Command {
	logdest := logger.NewStandardLogger(stderr)
	rc := &cobra.Command{
		Use: "featurebase",
		// TODO: These short/long descriptions could use some updating.
		Short: "FeatureBase is a feature extraction and storage technology that enables real-time analytics.",
		Long: `FeatureBase is a feature extraction and storage technology that enables real-time analytics.

This binary contains FeatureBase itself, as well as common
tools for administering FeatureBase, importing/exporting data,
backing up, and more. Complete documentation is available
at https://docs.featurebase.com/.

` + pilosa.VersionInfo(true) + "\n",
		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
			v := viper.New()

			switch cmd.Use {
			case "dax":
				v.Set("future.rename", true) // always use FEATUREBASE env for dax
			}
			if err := SetAllConfig(v, cmd.Flags(), ""); err != nil {
				return err
			}

			// return "dry run" error if "dry-run" flag is set
			ret, err := cmd.Flags().GetBool("dry-run")
			if err != nil {
				return fmt.Errorf("problem getting dry-run flag: %v", err)
			}
			if ret {
				if cmd.Parent() != nil {
					return fmt.Errorf("dry run")
				}
			}

			return nil
		},
		SilenceErrors: true,
	}
	rc.PersistentFlags().Bool("dry-run", false, "stop before executing")
	_ = rc.PersistentFlags().MarkHidden("dry-run")
	rc.PersistentFlags().StringP("config", "c", "", "Configuration file to read from.")

	rc.AddCommand(newChkSumCommand(logdest))
	rc.AddCommand(newBackupCommand(logdest))
	rc.AddCommand(newRestoreCommand(logdest))
	rc.AddCommand(newBackupTarCommand(stderr))
	rc.AddCommand(newRestoreTarCommand(logdest))
	rc.AddCommand(newConfigCommand(stderr))
	rc.AddCommand(newExportCommand(logdest))
	rc.AddCommand(newGenerateConfigCommand(logdest))
	rc.AddCommand(newImportCommand(logdest))
	rc.AddCommand(newAuthTokenCommand(logdest))
	rc.AddCommand(newRBFCommand(logdest))
	rc.AddCommand(newServeCmd(stderr))
	rc.AddCommand(newHolderCmd(stderr))
	rc.AddCommand(newKeygenCommand(logdest))
	rc.AddCommand(newDAXCommand(stderr))
	rc.AddCommand(newDataframeCsvLoaderCommand(logdest))
	rc.AddCommand(newPreSortCommand(logdest))
	rc.AddCommand(newParquetInfoCommand(logdest))

	rc.SetOutput(stderr)
	return rc
}

// SetAllConfig takes a FlagSet to be the definition of all configuration
// options, as well as their defaults. It then reads from the command line, the
// environment, and a config file (if specified), and applies the configuration
// in that priority order. Since each flag in the set contains a pointer to
// where its value should be stored, SetAllConfig can directly modify the value
// of each config variable.
//
// SetAllConfig looks for environment variables which are capitalized versions
// of the flag names with dashes replaced by underscores, and prefixed with
// envPrefix plus an underscore.
func SetAllConfig(v *viper.Viper, flags *pflag.FlagSet, envPrefix string) error { // nolint: unparam
	// add cmd line flag def to viper
	err := v.BindPFlags(flags)
	if err != nil {
		return err
	}

	if envPrefix == "" {
		envPrefix = "PILOSA"
		if v.GetBool("future.rename") {
			envPrefix = "FEATUREBASE"
		}
	}

	// add env to viper
	v.SetEnvPrefix(envPrefix)
	v.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_"))
	v.AutomaticEnv()

	c := v.GetString("config")
	var flagErr error
	validTags := make(map[string]bool)
	flags.VisitAll(func(f *pflag.Flag) {
		validTags[f.Name] = true
	})

	// add config file to viper
	if c != "" {
		v.SetConfigFile(c)
		v.SetConfigType("toml")
		err := v.ReadInConfig()
		if err != nil {
			return fmt.Errorf("error reading configuration file '%s': %v", c, err)
		}

		for _, key := range v.AllKeys() {
			if _, ok := validTags[key]; !ok {
				if key == "future.rename" {
					continue
				}
				return fmt.Errorf("invalid option in configuration file: %v", key)
			}
		}
	}

	// set all values from viper
	flags.VisitAll(func(f *pflag.Flag) {
		if flagErr != nil {
			return
		}
		var value string
		if f.Value.Type() == "stringSlice" {
			// special handling is needed for stringSlice as v.GetString will
			// always return "" in the case that the value is an actual string
			// slice from a config file rather than a comma separated string
			// from a flag or env var.
			vss := v.GetStringSlice(f.Name)
			value = strings.Join(vss, ",")
		} else {
			value = v.GetString(f.Name)
		}

		if f.Changed {
			// If f.Changed is true, that means the value has already been set
			// by a flag, and we don't need to ask viper for it since the flag
			// is the highest priority. This works around a problem with string
			// slices where f.Value.Set(csvString) would cause the elements of
			// csvString to be appended to the existing value rather than
			// replacing it.
			return
		}
		flagErr = f.Value.Set(value)
	})
	return flagErr
}
