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

package commands

import (
	"fmt"
	"io"
	"strings"

	"github.com/spf13/cobra"

	"sigs.k8s.io/kustomize/kyaml/errors"
	"sigs.k8s.io/kustomize/kyaml/fn/runtime/runtimeutil"
	"sigs.k8s.io/kustomize/kyaml/runfn"
	"sigs.k8s.io/kustomize/kyaml/yaml"

	"sigs.k8s.io/kustomize/cmd/config/internal/generateddocs/commands"
)

// GetCatRunner returns a RunFnRunner.
func GetRunFnRunner(name string) *RunFnRunner {
	r := &RunFnRunner{}
	c := &cobra.Command{
		Use:     "run [DIR]",
		Short:   commands.RunFnsShort,
		Long:    commands.RunFnsLong,
		Example: commands.RunFnsExamples,
		RunE:    r.runE,
		PreRunE: r.preRunE,
	}
	fixDocs(name, c)
	c.Flags().BoolVar(&r.IncludeSubpackages, "include-subpackages", true,
		"also print resources from subpackages.")
	r.Command = c
	r.Command.Flags().BoolVar(
		&r.DryRun, "dry-run", false, "print results to stdout")
	r.Command.Flags().BoolVar(
		&r.GlobalScope, "global-scope", false, "set global scope for functions.")
	r.Command.Flags().StringSliceVar(
		&r.FnPaths, "fn-path", []string{},
		"read functions from these directories instead of the configuration directory.")
	r.Command.Flags().StringVar(
		&r.Image, "image", "",
		"run this image as a function instead of discovering them.")
	// NOTE: exec plugins execute arbitrary code -- never change the default value of this flag!!!
	r.Command.Flags().BoolVar(
		&r.EnableExec, "enable-exec", false /*do not change!*/, "enable support for exec functions -- note: exec functions run arbitrary code -- do not use for untrusted configs!!! (Alpha)")
	r.Command.Flags().StringVar(
		&r.ExecPath, "exec-path", "", "run an executable as a function. (Alpha)")
	r.Command.Flags().BoolVar(
		&r.EnableStar, "enable-star", false, "enable support for starlark functions. (Alpha)")
	r.Command.Flags().StringVar(
		&r.StarPath, "star-path", "", "run a starlark script as a function. (Alpha)")
	r.Command.Flags().StringVar(
		&r.StarURL, "star-url", "", "run a starlark script as a function. (Alpha)")
	r.Command.Flags().StringVar(
		&r.StarName, "star-name", "", "name of starlark program. (Alpha)")

	r.Command.Flags().StringVar(
		&r.ResultsDir, "results-dir", "", "write function results to this dir")

	r.Command.Flags().BoolVar(
		&r.Network, "network", false, "enable network access for functions that declare it")
	r.Command.Flags().StringArrayVar(
		&r.Mounts, "mount", []string{},
		"a list of storage options read from the filesystem")
	r.Command.Flags().BoolVar(
		&r.LogSteps, "log-steps", false, "log steps to stderr")
	r.Command.Flags().StringArrayVarP(
		&r.Env, "env", "e", []string{},
		"a list of environment variables to be used by functions")
	return r
}

func RunCommand(name string) *cobra.Command {
	return GetRunFnRunner(name).Command
}

// RunFnRunner contains the run function
type RunFnRunner struct {
	IncludeSubpackages bool
	Command            *cobra.Command
	DryRun             bool
	GlobalScope        bool
	FnPaths            []string
	Image              string
	EnableStar         bool
	StarPath           string
	StarURL            string
	StarName           string
	EnableExec         bool
	ExecPath           string
	RunFns             runfn.RunFns
	ResultsDir         string
	Network            bool
	Mounts             []string
	LogSteps           bool
	Env                []string
}

func (r *RunFnRunner) runE(c *cobra.Command, args []string) error {
	return handleError(c, r.RunFns.Execute())
}

// getContainerFunctions parses the commandline flags and arguments into explicit
// Functions to run.
func (r *RunFnRunner) getContainerFunctions(c *cobra.Command, dataItems []string) (
	[]*yaml.RNode, error) {

	if r.Image == "" && r.StarPath == "" && r.ExecPath == "" && r.StarURL == "" {
		return nil, nil
	}

	var fn *yaml.RNode
	var err error

	if r.Image != "" {
		// create the function spec to set as an annotation
		fn, err = yaml.Parse(`container: {}`)
		if err != nil {
			return nil, err
		}
		// TODO: add support network, volumes, etc based on flag values
		err = fn.PipeE(
			yaml.Lookup("container"),
			yaml.SetField("image", yaml.NewScalarRNode(r.Image)))
		if err != nil {
			return nil, err
		}
		if r.Network {
			err = fn.PipeE(
				yaml.Lookup("container"),
				yaml.SetField("network", yaml.NewScalarRNode("true")))
			if err != nil {
				return nil, err
			}
		}
	} else if r.EnableStar && (r.StarPath != "" || r.StarURL != "") {
		// create the function spec to set as an annotation
		fn, err = yaml.Parse(`starlark: {}`)
		if err != nil {
			return nil, err
		}

		if r.StarPath != "" {
			err = fn.PipeE(
				yaml.Lookup("starlark"),
				yaml.SetField("path", yaml.NewScalarRNode(r.StarPath)))
			if err != nil {
				return nil, err
			}
		}
		if r.StarURL != "" {
			err = fn.PipeE(
				yaml.Lookup("starlark"),
				yaml.SetField("url", yaml.NewScalarRNode(r.StarURL)))
			if err != nil {
				return nil, err
			}
		}
		err = fn.PipeE(
			yaml.Lookup("starlark"),
			yaml.SetField("name", yaml.NewScalarRNode(r.StarName)))
		if err != nil {
			return nil, err
		}

	} else if r.EnableExec && r.ExecPath != "" {
		// create the function spec to set as an annotation
		fn, err = yaml.Parse(`exec: {}`)
		if err != nil {
			return nil, err
		}

		err = fn.PipeE(
			yaml.Lookup("exec"),
			yaml.SetField("path", yaml.NewScalarRNode(r.ExecPath)))
		if err != nil {
			return nil, err
		}
	}

	// create the function config
	rc, err := yaml.Parse(`
metadata:
  name: function-input
data: {}
`)
	if err != nil {
		return nil, err
	}

	// set the function annotation on the function config so it
	// is parsed by RunFns
	value, err := fn.String()
	if err != nil {
		return nil, err
	}
	err = rc.PipeE(
		yaml.LookupCreate(yaml.MappingNode, "metadata", "annotations"),
		yaml.SetField(runtimeutil.FunctionAnnotationKey, yaml.NewScalarRNode(value)))
	if err != nil {
		return nil, err
	}

	// default the function config kind to ConfigMap, this may be overridden
	var kind = "ConfigMap"
	var version = "v1"

	// populate the function config with data.  this is a convention for functions
	// to be more commandline friendly
	if len(dataItems) > 0 {
		dataField, err := rc.Pipe(yaml.Lookup("data"))
		if err != nil {
			return nil, err
		}
		for i, s := range dataItems {
			kv := strings.SplitN(s, "=", 2)
			if i == 0 && len(kv) == 1 {
				// first argument may be the kind
				kind = s
				continue
			}
			if len(kv) != 2 {
				return nil, fmt.Errorf("args must have keys and values separated by =")
			}
			err := dataField.PipeE(yaml.SetField(kv[0], yaml.NewScalarRNode(kv[1])))
			if err != nil {
				return nil, err
			}
		}
	}
	err = rc.PipeE(yaml.SetField("kind", yaml.NewScalarRNode(kind)))
	if err != nil {
		return nil, err
	}
	err = rc.PipeE(yaml.SetField("apiVersion", yaml.NewScalarRNode(version)))
	if err != nil {
		return nil, err
	}
	return []*yaml.RNode{rc}, nil
}

func toStorageMounts(mounts []string) []runtimeutil.StorageMount {
	var sms []runtimeutil.StorageMount
	for _, mount := range mounts {
		sms = append(sms, runtimeutil.StringToStorageMount(mount))
	}
	return sms
}

func (r *RunFnRunner) preRunE(c *cobra.Command, args []string) error {
	if !r.EnableStar && (r.StarPath != "" || r.StarURL != "") {
		return errors.Errorf("must specify --enable-star with --star-path and --star-url")
	}

	if !r.EnableExec && r.ExecPath != "" {
		return errors.Errorf("must specify --enable-exec with --exec-path")
	}

	if c.ArgsLenAtDash() >= 0 && r.Image == "" &&
		!(r.EnableStar && (r.StarPath != "" || r.StarURL != "")) && !(r.EnableExec && r.ExecPath != "") {
		return errors.Errorf("must specify --image")
	}

	var dataItems []string
	if c.ArgsLenAtDash() >= 0 {
		dataItems = args[c.ArgsLenAtDash():]
		args = args[:c.ArgsLenAtDash()]
	}
	if len(args) > 1 {
		return errors.Errorf("0 or 1 arguments supported, function arguments go after '--'")
	}

	fns, err := r.getContainerFunctions(c, dataItems)
	if err != nil {
		return err
	}

	// set the output to stdout if in dry-run mode or no arguments are specified
	var output io.Writer
	var input io.Reader
	if len(args) == 0 {
		output = c.OutOrStdout()
		input = c.InOrStdin()
	} else if r.DryRun {
		output = c.OutOrStdout()
	}

	// set the path if specified as an argument
	var path string
	if len(args) == 1 {
		// argument is the directory
		path = args[0]
	}

	// parse mounts to set storageMounts
	storageMounts := toStorageMounts(r.Mounts)

	r.RunFns = runfn.RunFns{
		FunctionPaths:  r.FnPaths,
		GlobalScope:    r.GlobalScope,
		Functions:      fns,
		Output:         output,
		Input:          input,
		Path:           path,
		Network:        r.Network,
		EnableStarlark: r.EnableStar,
		EnableExec:     r.EnableExec,
		StorageMounts:  storageMounts,
		ResultsDir:     r.ResultsDir,
		LogSteps:       r.LogSteps,
		Env:            r.Env,
	}

	// don't consider args for the function
	return nil
}
