// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0

package commands

import (
	"fmt"
	"os"
	"os/exec"
	"strings"
	"unicode"

	"github.com/spf13/cobra"
	"sigs.k8s.io/kustomize/cmd/config/runner"
	"sigs.k8s.io/kustomize/kyaml/kio"
	"sigs.k8s.io/kustomize/kyaml/yaml"
)

// GetXArgsRunner returns a command runner.
func GetXArgsRunner() *XArgsRunner {
	r := &XArgsRunner{}
	c := &cobra.Command{
		Use:   "xargs -- CMD...",
		Short: "Convert functionConfig to commandline flags and envs",
		Long: `Convert functionConfig to commandline flags and envs.

xargs reads a ResourceList from stdin and parses the functionConfig field.  xargs then
reads each of the fields under .spec and parses them as flags.  If the fields have non-scalar
values, then xargs encoded the values as yaml strings.

  CMD:
    The command to run and pass the functionConfig as arguments.
`,
		Example: `
# given this example functionConfig in config.yaml
kind: Foo
spec:
  flag1: value1
  flag2: value2
items:
- 2
- 1

# this command:
$ kyaml cat pkg/ --function-config config.yaml --wrap-kind ResourceList | kyaml run xargs -- app

# is equivalent to this command:
$ kyaml cat pkg/ --function-config config.yaml --wrap-kind ResourceList | app --flag1=value1 --flag2=value2 2 1

# echo: prints the app arguments
$ kyaml cat pkg/ --function-config config.yaml --wrap-kind ResourceList | kyaml reconcile xargs -- echo
--flag1=value1 --flag2=value2 2 1

# env: prints the app env
$ kyaml cat pkg/ --function-config config.yaml --wrap-kind ResourceList | kyaml reconcile xargs -- env

# cat: prints the app stdin -- prints the package contents and functionConfig wrapped in a
# ResourceList
$ kyaml cat pkg/ --function-config config.yaml --wrap-kind ResourceList | kyaml reconcile xargs --no-flags -- env

`,
		RunE:               r.runE,
		SilenceUsage:       true,
		FParseErrWhitelist: cobra.FParseErrWhitelist{UnknownFlags: true},
		Args:               cobra.MinimumNArgs(1),
	}
	r.Command = c
	r.Command.Flags().BoolVar(&r.EnvOnly, "env-only", false, "only add env vars, not flags")
	c.Flags().StringVar(&r.WrapKind, "wrap-kind", "List", "wrap the input xargs give to the command in this type.")
	c.Flags().StringVar(&r.WrapVersion, "wrap-version", "v1", "wrap the input xargs give to the command in this type.")
	return r
}

// Runner contains the run function
type XArgsRunner struct {
	Command     *cobra.Command
	Args        []string
	EnvOnly     bool
	WrapKind    string
	WrapVersion string
}

func XArgsCommand() *cobra.Command {
	return GetXArgsRunner().Command
}

func (r *XArgsRunner) runE(c *cobra.Command, _ []string) error {
	if len(r.Args) == 0 {
		r.Args = os.Args
	}
	cmdIndex := -1
	for i := range r.Args {
		if r.Args[i] == "--" {
			cmdIndex = i + 1
			break
		}
	}
	if cmdIndex < 0 {
		return fmt.Errorf("must specify -- before command")
	}
	r.Args = r.Args[cmdIndex:]
	run := exec.Command(r.Args[0])

	if len(r.Args) > 1 {
		r.Args = r.Args[cmdIndex+1:]
	} else {
		r.Args = []string{}
	}
	run.Stdout = c.OutOrStdout()
	run.Stderr = c.ErrOrStderr()

	rw := &kio.ByteReadWriter{
		Reader: c.InOrStdin(),
	}
	nodes, err := rw.Read()
	if err != nil {
		return err
	}

	env := os.Environ()

	// append the config to the flags
	if err = func() error {
		if rw.FunctionConfig == nil {
			return nil
		}
		str, err := rw.FunctionConfig.String()
		if err != nil {
			return err
		}
		// add the API object to the env
		env = append(env, fmt.Sprintf("KUST_FUNCTION_CONFIG=%s", str))

		// parse the fields
		meta := rw.FunctionConfig.Field("metadata")
		if meta != nil {
			err = meta.Value.VisitFields(func(node *yaml.MapNode) error {
				if !r.EnvOnly {
					r.Args = append(r.Args, fmt.Sprintf("--%s=%s",
						node.Key.YNode().Value, parseYNode(node.Value.YNode())))
				}
				env = append(env, fmt.Sprintf("%s=%s", strings.ToUpper(node.Key.YNode().Value),
					node.Value.YNode().Value))
				return nil
			})
			if err != nil {
				return err
			}
		}

		spec := rw.FunctionConfig.Field("spec")
		if spec != nil {
			err = spec.Value.VisitFields(func(node *yaml.MapNode) error {
				if !r.EnvOnly {
					r.Args = append(r.Args, fmt.Sprintf("--%s=%s",
						node.Key.YNode().Value, parseYNode(node.Value.YNode())))
				}
				env = append(env, fmt.Sprintf("%s=%s", strings.ToUpper(node.Key.YNode().Value),
					node.Value.YNode().Value))
				return nil
			})
			if err != nil {
				return err
			}
		}

		if !r.EnvOnly {
			items := rw.FunctionConfig.Field("items")
			if items != nil {
				err = items.Value.VisitElements(func(node *yaml.RNode) error {
					r.Args = append(r.Args, parseYNode(node.YNode()))
					return nil
				})
				if err != nil {
					return err
				}
			}
		}

		if r.WrapKind != "" {
			if kind := rw.FunctionConfig.Field("kind"); !kind.IsNilOrEmpty() {
				kind.Value.YNode().Value = r.WrapKind
			}
			rw.WrappingKind = r.WrapKind
		}
		if r.WrapVersion != "" {
			if version := rw.FunctionConfig.Field("apiVersion"); !version.IsNilOrEmpty() {
				version.Value.YNode().Value = r.WrapVersion
			}
			rw.WrappingAPIVersion = r.WrapVersion
		}
		return nil
	}(); err != nil {
		return err
	}
	run.Args = append(run.Args, r.Args...)
	run.Env = append(run.Env, env...)

	// write ResourceList to stdin
	if err = func() error {
		in, err := run.StdinPipe()
		if err != nil {
			return err
		}
		defer in.Close()
		rw.Writer = in
		if r.WrapKind != kio.ResourceListKind {
			rw.FunctionConfig = nil
		}
		return rw.Write(nodes)
	}(); err != nil {
		return err
	}

	return runner.HandleError(c, run.Run())
}

func parseYNode(node *yaml.Node) string {
	node.Value = strings.TrimSpace(node.Value)
	for _, b := range node.Value {
		if unicode.IsSpace(b) {
			// wrap in '' -- contains whitespace
			return fmt.Sprintf("'%s'", node.Value)
		}
	}
	return node.Value
}
