package jsonnet

import (
	"bytes"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"os"
	"path/filepath"
	"strings"

	"github.com/fatih/color"
	"github.com/ghodss/yaml"
	"github.com/google/go-jsonnet"
	"github.com/urfave/cli"
)
// Command exports the jsonnet command.
var Command = cli.Command{
	Name:      "jsonnet",
	Usage:     "generate .drone.yml from jsonnet",
	ArgsUsage: "[path/to/.drone.jsonnet]",
	Action: func(c *cli.Context) {
		if err := generate(c); err != nil {
			log.Fatalln(err)
		}
	},
	Flags: []cli.Flag{
		cli.StringFlag{
			Name:  "source",
			Usage: "Source file",
			Value: ".drone.jsonnet",
		},
		cli.StringFlag{
			Name:  "target",
			Usage: "target file",
			Value: ".drone.yml",
		},
		cli.BoolFlag{
			Name:  "stream",
			Usage: "Write output as a YAML stream.",
		},
		cli.BoolFlag{
			Name:  "format",
			Usage: "Write output as formatted YAML",
		},
		cli.BoolFlag{
			Name:  "stdout",
			Usage: "Write output to stdout",
		},
		cli.BoolFlag{
			Name:  "string",
			Usage: "Expect a string, manifest as plain text",
		},
		cli.StringSliceFlag{
			Name:  "extVar, V",
			Usage: "Pass extVars to Jsonnet (can be specified multiple times)",
		},
		cli.StringSliceFlag{
			Name:  "jpath, J",
			Usage: "Specify an additional library search dir (right-most wins)",
		},
	},
}

func generate(c *cli.Context) error {
	result, err := convert(
		c.String("source"),
		c.Bool("string"),
		c.Bool("format"),
		c.Bool("stream"),
		c.StringSlice("extVar"),
		c.StringSlice("jpath"),
	)
	if err != nil {
		return err
	}

	// the user can optionally write the yaml to stdout. This is useful for debugging purposes without mutating an existing file.
	if c.Bool("stdout") {
		io.WriteString(os.Stdout, result)
		return nil
	}

	target := c.String("target")
	return ioutil.WriteFile(target, []byte(result), 0644)
}

func convert(source string, stringOutput bool, format bool, stream bool, vars []string, jpath []string) (string, error) {
	data, err := ioutil.ReadFile(source)
	if err != nil {
		return "", err
	}

	vm := jsonnet.MakeVM()
	vm.MaxStack = 500
	vm.StringOutput = stringOutput
	vm.ErrorFormatter.SetMaxStackTraceSize(20)
	vm.ErrorFormatter.SetColorFormatter(
		color.New(color.FgRed).Fprintf,
	)

	// register native functions
	RegisterNativeFuncs(vm)

	jsonnetPath := filepath.SplitList(os.Getenv("JSONNET_PATH"))
	jsonnetPath = append(jsonnetPath, jpath...)
	vm.Importer(&jsonnet.FileImporter{
		JPaths: jsonnetPath,
	})

	// extVars
	for _, v := range vars {
		name, value, err := getVarVal(v)
		if err != nil {
			return "", err
		}
		vm.ExtVar(name, value)
	}

	formatDoc := func(doc []byte) ([]byte, error) {
		// enable yaml output
		if format {
			formatted, yErr := yaml.JSONToYAML(doc)
			if yErr != nil {
				return nil, fmt.Errorf("failed to convert to YAML: %v", yErr)
			}
			return formatted, nil
		}
		return doc, nil
	}

	buf := new(bytes.Buffer)
	if stream {
		docs, err := vm.EvaluateSnippetStream(source, string(data))
		if err != nil {
			return "", err
		}
		for _, doc := range docs {
			formatted, err := formatDoc([]byte(doc))
			if err != nil {
				return "", err
			}

			buf.WriteString("---")
			buf.WriteString("\n")
			buf.Write(formatted)
		}
	} else {
		result, err := vm.EvaluateSnippet(source, string(data))
		if err != nil {
			return "", err
		}
		formatted, err := formatDoc([]byte(result))
		if err != nil {
			return "", err
		}
		buf.Write(formatted)
	}

	return buf.String(), nil
}

// https://github.com/google/go-jsonnet/blob/master/cmd/jsonnet/cmd.go#L149
func getVarVal(s string) (string, string, error) {
	parts := strings.SplitN(s, "=", 2)
	name := parts[0]
	if len(parts) == 1 {
		content, exists := os.LookupEnv(name)
		if exists {
			return name, content, nil
		}
		return "", "", fmt.Errorf("environment variable %v was undefined", name)
	}
	return name, parts[1], nil
}
