package main

import (
	"fmt"
	"log"
	"net/url"
	"os"
	"strings"

	"github.com/influxdata/influx-cli/v2/clients"
	"github.com/influxdata/influx-cli/v2/clients/apply"
	"github.com/influxdata/influx-cli/v2/pkg/cli/middleware"
	"github.com/influxdata/influx-cli/v2/pkg/template"
	"github.com/mattn/go-isatty"
	"github.com/urfave/cli"
)

func newApplyCmd() cli.Command {
	var params struct {
		orgParams      clients.OrgParams
		stackId        string
		inPaths        cli.StringSlice
		inUrls         cli.StringSlice
		recursive      bool
		encoding       template.Encoding
		noColor        bool
		noTableBorders bool
		quiet          bool
		force          string
		secrets        cli.StringSlice
		envVars        cli.StringSlice
		filters        cli.StringSlice
	}
	return cli.Command{
		Name:  "apply",
		Usage: "Apply a template to manage resources",
		Description: `The apply command applies InfluxDB template(s). Use the command to create new
resources via a declarative template. The apply command can consume templates
via file(s), url(s), stdin, or any combination of the 3. Each run of the apply
command ensures that all templates applied are applied in unison as a transaction.
If any unexpected errors are discovered then all side effects are rolled back.

Examples:
	# Apply a template via a file
	influx apply -f $PATH_TO_TEMPLATE/template.json

	# Apply a stack that has associated templates. In this example the stack has a remote
	# template associated with it.
	influx apply --stack-id $STACK_ID

	# Apply a template associated with a stack. Stacks make template application idempotent.
	influx apply -f $PATH_TO_TEMPLATE/template.json --stack-id $STACK_ID

	# Apply multiple template files together (mix of yaml and json)
	influx apply \
		-f $PATH_TO_TEMPLATE/template_1.json \
		-f $PATH_TO_TEMPLATE/template_2.yml

	# Apply a template from a url
	influx apply -u https://raw.githubusercontent.com/influxdata/community-templates/master/docker/docker.yml

	# Apply a template from STDIN
	cat $TEMPLATE.json | influx apply --encoding json

	# Applying a directory of templates, takes everything from provided directory
	influx apply -f $PATH_TO_TEMPLATE_DIR

	# Applying a directory of templates, recursively descending into child directories
	influx apply -R -f $PATH_TO_TEMPLATE_DIR

	# Applying directories from many sources, file and URL
	influx apply -f $PATH_TO_TEMPLATE/template.yml -f $URL_TO_TEMPLATE

	# Applying a template with actions to skip resources applied. The
	# following example skips all buckets and the dashboard whose 
	# metadata.name field matches the provided $DASHBOARD_TMPL_NAME.
	# format for filters:
	#	--filter=kind=Bucket
	#	--filter=resource=Label:$Label_TMPL_NAME
	influx apply \
		-f $PATH_TO_TEMPLATE/template.yml \
		--filter kind=Bucket \
		--filter resource=Dashboard:$DASHBOARD_TMPL_NAME

For information about finding and using InfluxDB templates, see
https://docs.influxdata.com/influxdb/latest/reference/cli/influx/apply/.

For more templates created by the community, see
https://github.com/influxdata/community-templates.
`,
		Flags: append(
			append(commonFlags(), getOrgFlags(&params.orgParams)...),
			&cli.StringFlag{
				Name:        "stack-id",
				Usage:       "Stack ID to associate with template application",
				Destination: &params.stackId,
			},
			&cli.StringSliceFlag{
				Name:      "file, f",
				Usage:     "Path to template file; Supports file paths or (deprecated) HTTP(S) URLs",
				TakesFile: true,
				Value:     &params.inPaths,
			},
			&cli.StringSliceFlag{
				Name:  "template-url, u",
				Usage: "HTTP(S) URL to template file",
				Value: &params.inUrls,
			},
			&cli.BoolFlag{
				Name:        "recurse, R",
				Usage:       "Process the directory used in -f, --file recursively. Useful when you want to manage related templates organized within the same directory.",
				Destination: &params.recursive,
			},
			&cli.GenericFlag{
				Name:  "encoding, e",
				Usage: "Encoding for the input stream. If a file is provided will gather encoding type from file extension. If extension provided will override.",
				Value: &params.encoding,
			},
			&cli.BoolFlag{
				Name:        "disable-color",
				Usage:       "Disable color in output",
				Destination: &params.noColor,
			},
			&cli.BoolFlag{
				Name:        "disable-table-borders",
				Usage:       "Disable table borders",
				Destination: &params.noTableBorders,
			},
			&cli.BoolFlag{
				Name:        "quiet, q",
				Usage:       "Disable output printing",
				Destination: &params.quiet,
			},
			&cli.StringFlag{
				Name:        "force",
				Usage:       "Set to 'yes' to skip confirmation before applying changes (recommended for non-interactive scripts).",
				Destination: &params.force,
			},
			&cli.StringSliceFlag{
				Name:  "secret",
				Usage: "Secrets to provide alongside the template; format --secret SECRET_KEY=SECRET_VALUE --secret SECRET_KEY_2=SECRET_VALUE_2",
				Value: &params.secrets,
			},
			&cli.StringSliceFlag{
				Name:  "env-ref",
				Usage: "Environment references to provide alongside the template; format --env-ref REF_KEY=REF_VALUE --env-ref REF_KEY_2=REF_VALUE_2",
				Value: &params.envVars,
			},
			&cli.StringSliceFlag{
				Name:  "filter",
				Usage: "Resources to skip when applying the template. Filter out by `kind` or by `resource`",
				Value: &params.filters,
			},
		),
		Before: middleware.WithBeforeFns(withCli(), withApi(true), middleware.NoArgs),
		Action: func(ctx *cli.Context) error {
			parsedParams := apply.Params{
				OrgParams:          params.orgParams,
				StackId:            params.stackId,
				Recursive:          params.recursive,
				Quiet:              params.quiet,
				RenderTableBorders: !params.noTableBorders,
				RenderTableColors:  !params.noColor,
				Secrets:            make(map[string]string, len(params.secrets.Value())),
				EnvVars:            make(map[string]string, len(params.envVars.Value())),
				Filters:            make([]apply.ResourceFilter, len(params.filters.Value())),
			}
			// check that orgName and orgFlags aren't both set
			if err := checkOrgFlags(&params.orgParams); err != nil {
				return err
			}

			// Collect all the sources the CLI needs to read templates from.
			var deprecationShown bool
			for _, in := range params.inPaths.Value() {
				// Heuristic to guess what's a URL and what's a local file.
				// TODO: Remove this once we stop supporting URLs in the --file arg.
				u, err := url.Parse(in)
				if err != nil {
					return fmt.Errorf("failed to parse input path %q: %w", in, err)
				}
				if strings.HasPrefix(u.Scheme, "http") {
					if !deprecationShown {
						log.Println("WARN: Passing URLs via -f/--file is deprecated, please use -u/--template-url instead")
						deprecationShown = true
					}
					parsedParams.Sources = append(parsedParams.Sources, template.SourceFromURL(u, params.encoding))
				} else {
					fileSources, err := template.SourcesFromPath(in, params.recursive, params.encoding)
					if err != nil {
						return err
					}
					parsedParams.Sources = append(parsedParams.Sources, fileSources...)
				}
			}
			for _, in := range params.inUrls.Value() {
				u, err := url.Parse(in)
				if err != nil {
					return fmt.Errorf("failed to parse input URL %q: %w", in, err)
				}
				parsedParams.Sources = append(parsedParams.Sources, template.SourceFromURL(u, params.encoding))
			}
			if !isatty.IsTerminal(os.Stdin.Fd()) {
				parsedParams.Sources = append(parsedParams.Sources, template.SourceFromReader(os.Stdin, params.encoding))
			}

			// Parse env and secret values.
			for _, e := range params.envVars.Value() {
				pieces := strings.Split(e, "=")
				if len(pieces) != 2 {
					return fmt.Errorf("env-ref %q has invalid format, must be `REF_KEY=REF_VALUE`", e)
				}
				parsedParams.EnvVars[pieces[0]] = pieces[1]
			}
			for _, s := range params.secrets.Value() {
				pieces := strings.Split(s, "=")
				if len(pieces) != 2 {
					return fmt.Errorf("secret %q has invalid format, must be `SECRET_KEY=SECRET_VALUE`", s)
				}
				parsedParams.Secrets[pieces[0]] = pieces[1]
			}

			// Parse filters.
			for i, f := range params.filters.Value() {
				pieces := strings.Split(f, "=")
				if len(pieces) != 2 {
					return fmt.Errorf("filter %q has invalid format, expected `resource=KIND:NAME` or `kind=KIND`", f)
				}
				key, val := pieces[0], pieces[1]
				switch strings.ToLower(key) {
				case "kind":
					parsedParams.Filters[i] = apply.ResourceFilter{Kind: val}
				case "resource":
					valPieces := strings.Split(val, ":")
					if len(valPieces) != 2 {
						return fmt.Errorf("resource filter %q has invalid format, expected `resource=KIND:NAME", val)
					}
					kind, name := valPieces[0], valPieces[1]
					parsedParams.Filters[i] = apply.ResourceFilter{Kind: kind, Name: &name}
				default:
					return fmt.Errorf("invalid filter type %q, supported values are `resource` and `kind`", key)
				}
			}

			// Parse our strange way of passing 'force'
			switch params.force {
			case "":
				// no force
			case "conflict":
				log.Println("WARN: Passing '--force conflict' is deprecated, assuming '--force yes'")
				parsedParams.Force = true
			case "true":
				log.Println("WARN: Passing '--force true' is deprecated, assuming '--force yes'")
				parsedParams.Force = true
			case "yes":
				parsedParams.Force = true
			default:
				return fmt.Errorf("invalid argument '--force %s', should use '--force yes'", params.force)
			}

			api := getAPI(ctx)
			client := apply.Client{
				CLI:              getCLI(ctx),
				TemplatesApi:     api.TemplatesApi,
				OrganizationsApi: api.OrganizationsApi,
			}

			return client.Apply(getContext(ctx), &parsedParams)
		},
	}
}
