package main

import (
	"context"
	"fmt"
	"io"
	"net/url"
	"runtime"
	"strings"

	"github.com/influxdata/influx-cli/v2/api"
	"github.com/influxdata/influx-cli/v2/clients"
	"github.com/influxdata/influx-cli/v2/clients/signin"
	"github.com/influxdata/influx-cli/v2/config"
	"github.com/influxdata/influx-cli/v2/pkg/cli/middleware"
	"github.com/influxdata/influx-cli/v2/pkg/signals"
	"github.com/influxdata/influx-cli/v2/pkg/stdio"
	"github.com/urfave/cli"
)

const (
	tokenFlagName           = "token"
	hostFlagName            = "host"
	skipVerifyFlagName      = "skip-verify"
	traceIdFlagName         = "trace-debug-id"
	extraHttpHeaderFlagName = "extra-http-header"
	configPathFlagName      = "configs-path"
	configNameFlagName      = "active-config"
	httpDebugFlagName       = "http-debug"
	printJsonFlagName       = "json"
	hideHeadersFlagName     = "hide-headers"
)

// newCli builds a CLI core that reads from stdin, writes to stdout/stderr, manages a local config store,
// and optionally tracks a trace ID specified over the CLI.
func newCli(ctx *cli.Context) (clients.CLI, error) {
	configPath := ctx.String(configPathFlagName)
	var err error
	if configPath == "" {
		configPath, err = config.DefaultPath()
		if err != nil {
			return clients.CLI{}, err
		}
	}
	configSvc := config.NewLocalConfigService(configPath)
	var activeConfig config.Config
	if ctx.IsSet(configNameFlagName) {
		if activeConfig, err = configSvc.SwitchActive(ctx.String(configNameFlagName)); err != nil {
			return clients.CLI{}, err
		}
	} else if activeConfig, err = configSvc.Active(); err != nil {
		return clients.CLI{}, err
	}

	return clients.CLI{
		StdIO:            stdio.TerminalStdio,
		PrintAsJSON:      ctx.Bool(printJsonFlagName),
		HideTableHeaders: ctx.Bool(hideHeadersFlagName),
		ActiveConfig:     activeConfig,
		ConfigService:    configSvc,
	}, nil
}

// newApiClient returns an API clients configured to communicate with a remote InfluxDB instance over HTTP.
// Client parameters are pulled from the CLI context.
func newApiClient(ctx *cli.Context, configSvc config.Service, injectToken bool) (*api.APIClient, error) {
	cfg, err := configSvc.Active()
	if err != nil {
		return nil, err
	}
	if ctx.IsSet(tokenFlagName) {
		cfg.Token = ctx.String(tokenFlagName)
	}
	if ctx.IsSet(hostFlagName) {
		cfg.Host = ctx.String(hostFlagName)
	}

	configParams := api.ConfigParams{
		UserAgent:        fmt.Sprintf("influx/%s (%s) Sha/%s Date/%s", version, runtime.GOOS, commit, date),
		AllowInsecureTLS: ctx.Bool(skipVerifyFlagName),
		Debug:            ctx.Bool(httpDebugFlagName),
	}

	parsedHost, err := url.Parse(cfg.Host)
	if err != nil {
		return nil, fmt.Errorf("host URL %q is invalid: %w", cfg.Host, err)
	}
	configParams.Host = parsedHost

	if injectToken && cfg.Token != "" {
		configParams.Token = &cfg.Token
	} else if cfg.Cookie != "" {
		cookie, err := signin.GetCookie(getContext(ctx), configParams, cfg.Cookie)
		if err != nil {
			return nil, fmt.Errorf("error creating session: %w", err)
		}
		configParams.Cookie = &cookie
	}
	if ctx.IsSet(traceIdFlagName) {
		configParams.TraceId = api.PtrString(ctx.String(traceIdFlagName))
	}

	apiConfig := api.NewAPIConfig(configParams)

	if ctx.IsSet(extraHttpHeaderFlagName) {
		for _, h := range ctx.StringSlice(extraHttpHeaderFlagName) {
			k, v, ok := strings.Cut(h, ":")
			if !ok {
				return nil, fmt.Errorf(`header flag syntax "key:value", missing value in %q`, h)
			}
			apiConfig.AddDefaultHeader(k, v)
		}
	}

	return api.NewAPIClient(apiConfig), nil
}

func withContext() cli.BeforeFunc {
	return func(ctx *cli.Context) error {
		ctx.App.Metadata["context"] = signals.WithStandardSignals(context.Background())
		return nil
	}
}

func getContext(ctx *cli.Context) context.Context {
	c, ok := ctx.App.Metadata["context"].(context.Context)
	if !ok {
		panic("missing context")
	}
	return c
}

func withCli() cli.BeforeFunc {
	return func(ctx *cli.Context) error {
		c, err := newCli(ctx)
		if err != nil {
			return err
		}
		ctx.App.Metadata["cli"] = c
		return nil
	}
}

func getCLI(ctx *cli.Context) clients.CLI {
	i, ok := ctx.App.Metadata["cli"].(clients.CLI)
	if !ok {
		panic("missing CLI")
	}
	return i
}

func withApi(injectToken bool) cli.BeforeFunc {
	key := "api-no-token"
	if injectToken {
		key = "api"
	}

	makeFn := func(ctx *cli.Context) error {
		c := getCLI(ctx)
		apiClient, err := newApiClient(ctx, c.ConfigService, injectToken)
		if err != nil {
			return err
		}
		ctx.App.Metadata[key] = apiClient
		return nil
	}
	return middleware.WithBeforeFns(makeFn)
}

func getAPI(ctx *cli.Context) *api.APIClient {
	i, ok := ctx.App.Metadata["api"].(*api.APIClient)
	if !ok {
		panic("missing APIClient with token")
	}
	return i
}

func getAPINoToken(ctx *cli.Context) *api.APIClient {
	i, ok := ctx.App.Metadata["api-no-token"].(*api.APIClient)
	if !ok {
		panic("missing APIClient without token")
	}
	return i
}

type CommonBoolFlag struct {
	cli.BoolFlag
}

type CommonStringFlag struct {
	cli.StringFlag
}

type CommonStringSliceFlag struct {
	cli.StringSliceFlag
}

// NOTE: urfave/cli has dedicated support for global flags, but it only parses those flags
// if they're specified before any command names. This is incompatible with the old influx
// CLI, which repeatedly registered common flags on every "leaf" command, forcing the flags
// to be specified after _all_ command names were given.
//
// We replicate the pattern from the old CLI so existing scripts and docs stay valid.

// configPathFlag returns the flag used by commands that access the CLI's local config store.
func configPathFlag() cli.Flag {
	return &CommonStringFlag{
		cli.StringFlag{
			Name:   configPathFlagName,
			Usage:  "Path to the influx CLI configurations",
			EnvVar: "INFLUX_CONFIGS_PATH",
		},
	}
}

// coreFlags returns flags used by all CLI commands that make HTTP requests.
func coreFlags() []cli.Flag {
	return []cli.Flag{
		&CommonStringFlag{cli.StringFlag{
			Name:   hostFlagName,
			Usage:  "HTTP address of InfluxDB",
			EnvVar: "INFLUX_HOST",
		}},
		&CommonBoolFlag{cli.BoolFlag{
			Name:   skipVerifyFlagName,
			Usage:  "Skip TLS certificate chain and host name verification",
			EnvVar: "INFLUX_SKIP_VERIFY",
		}},
		configPathFlag(),
		&CommonStringFlag{cli.StringFlag{
			Name:   configNameFlagName + ", c",
			Usage:  "Config name to use for command",
			EnvVar: "INFLUX_ACTIVE_CONFIG",
		}},
		&CommonStringFlag{cli.StringFlag{
			Name:   traceIdFlagName,
			Hidden: true,
			EnvVar: "INFLUX_TRACE_DEBUG_ID",
		}},
		&CommonStringSliceFlag{cli.StringSliceFlag{
			Name:   extraHttpHeaderFlagName,
			Hidden: true,
			EnvVar: "INFLUX_EXTRA_HTTP_HEADER",
		}},
		&CommonBoolFlag{cli.BoolFlag{
			Name: httpDebugFlagName,
		}},
	}
}

// printFlags returns flags used by commands that display API resources to the user.
func printFlags() []cli.Flag {
	return []cli.Flag{
		&CommonBoolFlag{cli.BoolFlag{
			Name:   printJsonFlagName,
			Usage:  "Output data as JSON",
			EnvVar: "INFLUX_OUTPUT_JSON",
		}},
		&CommonBoolFlag{cli.BoolFlag{
			Name:   hideHeadersFlagName,
			Usage:  "Hide the table headers in output data",
			EnvVar: "INFLUX_HIDE_HEADERS",
		}},
	}
}

// commonTokenFlag returns the flag used by commands that hit an authenticated API.
func commonTokenFlag() cli.Flag {
	return &CommonStringFlag{cli.StringFlag{
		Name:   tokenFlagName + ", t",
		Usage:  "Token to authenticate request",
		EnvVar: "INFLUX_TOKEN",
	}}
}

func init() {

	// SubcommandHelpTemplate is the text template for the subcommand help topic.
	// cli.go uses text/template to render templates. You can
	// render custom help text by setting this variable.
	cli.CommandHelpTemplate = `NAME:
   {{.HelpName}} - {{.Usage}}

USAGE:
   {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Category}}

CATEGORY:
   {{.Category}}{{end}}{{if .Description}}

DESCRIPTION:
   {{.Description}}{{end}}{{if .VisibleFlags}}

COMMON OPTIONS:
   {{range .VisibleFlags}}{{if iscommon .}}{{.}}
   {{end}}{{end}}
OPTIONS:
   {{range .VisibleFlags}}{{if not (iscommon .)}}{{.}}
   {{end}}{{end}}{{end}}
`
	cli.HelpPrinter = func(w io.Writer, templ string, data interface{}) {
		customFunc := make(map[string]interface{})
		customFunc["iscommon"] = func(flag cli.Flag) bool {
			switch flag.(type) {
			case *CommonBoolFlag:
				return true
			case *CommonStringFlag:
				return true
			default:
				return false
			}
		}
		cli.HelpPrinterCustom(w, templ, data, customFunc)
	}
}

// commonFlagsNoToken returns flags used by commands that display API resources to the user, but don't need auth.
func commonFlagsNoToken() []cli.Flag {
	return append(coreFlags(), printFlags()...)
}

// commonFlagsNoPrint returns flags used by commands that need auth, but don't display API resources to the user.
func commonFlagsNoPrint() []cli.Flag {
	return append(coreFlags(), commonTokenFlag())
}

// commonFlags returns flags used by commands that need auth and display API resources to the user.
func commonFlags() []cli.Flag {
	return append(commonFlagsNoToken(), commonTokenFlag())
}

// getOrgFlags returns flags used by commands that are scoped to a single org, binding
// the flags to the given params container.
func getOrgFlags(params *clients.OrgParams) []cli.Flag {
	return []cli.Flag{
		&cli.StringFlag{
			Name:        "org-id",
			Usage:       "The ID of the organization",
			EnvVar:      "INFLUX_ORG_ID",
			Destination: &params.OrgID,
		},
		&cli.StringFlag{
			Name:        "org, o",
			Usage:       "The name of the organization",
			EnvVar:      "INFLUX_ORG",
			Destination: &params.OrgName,
		},
	}
}

// checkOrgFlags returns an error if OrgId and OrgName are both set.
func checkOrgFlags(params *clients.OrgParams) error {
	if params.OrgID != "" && params.OrgName != "" {
		return fmt.Errorf("ambiguous org: use OrgId or OrgName, but not both. OrgID: %s, OrgName: %s",
			params.OrgID, params.OrgName)
	}
	return nil
}

// getBucketFlags returns flags used by commands that are scoped to a single bucket, binding
// the flags to the given params container.
func getBucketFlags(params *clients.BucketParams) []cli.Flag {
	return []cli.Flag{
		&cli.StringFlag{
			Name:        "bucket-id, i",
			Usage:       "The bucket ID, required if name isn't provided",
			Destination: &params.BucketID,
		},
		&cli.StringFlag{
			Name:        "bucket, n",
			Usage:       "The bucket name, org or org-id will be required by choosing this",
			Destination: &params.BucketName,
		},
	}
}

// getOrgBucketFlags returns flags used by commands that are scoped to a single org/bucket, binding
// the flags to the given params container.
func getOrgBucketFlags(c *clients.OrgBucketParams) []cli.Flag {
	return append(getBucketFlags(&c.BucketParams), getOrgFlags(&c.OrgParams)...)
}
