package control

import (
	"bufio"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"os/exec"
	"sort"
	"strings"
	"text/template"

	"github.com/rancher/os/config"
	"github.com/rancher/os/pkg/log"
	"github.com/rancher/os/pkg/util"

	yaml "github.com/cloudfoundry-incubator/candiedyaml"
	"github.com/codegangsta/cli"
	"github.com/pkg/errors"
)

func configSubcommands() []cli.Command {
	return []cli.Command{
		{
			Name:   "get",
			Usage:  "get value",
			Action: configGet,
		},
		{
			Name:   "set",
			Usage:  "set a value",
			Action: configSet,
		},
		{
			Name:   "images",
			Usage:  "List Docker images for a configuration from a file",
			Action: runImages,
			Flags: []cli.Flag{
				cli.StringFlag{
					Name:  "input, i",
					Usage: "File from which to read config",
				},
			},
		},
		{
			Name:     "generate",
			Usage:    "Generate a configuration file from a template",
			Action:   runGenerate,
			HideHelp: true,
		},
		{
			Name:  "export",
			Usage: "export configuration",
			Flags: []cli.Flag{
				cli.StringFlag{
					Name:  "output, o",
					Usage: "File to which to save",
				},
				cli.BoolFlag{
					Name:  "private, p",
					Usage: "Include the generated private keys",
				},
				cli.BoolFlag{
					Name:  "full, f",
					Usage: "Export full configuration, including internal and default settings",
				},
			},
			Action: export,
		},
		{
			Name:   "merge",
			Usage:  "merge configuration from stdin",
			Action: merge,
			Flags: []cli.Flag{
				cli.StringFlag{
					Name:  "input, i",
					Usage: "File from which to read",
				},
			},
		},
		{
			Name:   "syslinux",
			Usage:  "edit Syslinux boot global.cfg",
			Action: editSyslinux,
		},
		{
			Name:   "validate",
			Usage:  "validate configuration from stdin",
			Action: validate,
			Flags: []cli.Flag{
				cli.StringFlag{
					Name:  "input, i",
					Usage: "File from which to read",
				},
			},
		},
	}
}

func imagesFromConfig(cfg *config.CloudConfig) []string {
	imagesMap := map[string]int{}

	for _, service := range cfg.Rancher.BootstrapContainers {
		imagesMap[service.Image] = 1
	}
	for _, service := range cfg.Rancher.Services {
		imagesMap[service.Image] = 1
	}

	images := make([]string, len(imagesMap))
	i := 0
	for image := range imagesMap {
		images[i] = image
		i++
	}
	sort.Strings(images)
	return images
}

func runImages(c *cli.Context) error {
	configFile := c.String("input")
	cfg, err := config.ReadConfig(nil, false, configFile)
	if err != nil {
		log.WithFields(log.Fields{"err": err, "file": configFile}).Fatalf("Could not read config from file")
	}
	images := imagesFromConfig(cfg)
	fmt.Println(strings.Join(images, " "))
	return nil
}

func runGenerate(c *cli.Context) error {
	if err := genTpl(os.Stdin, os.Stdout); err != nil {
		log.Fatalf("Failed to generate config, err: '%s'", err)
	}
	return nil
}

func genTpl(in io.Reader, out io.Writer) error {
	bytes, err := ioutil.ReadAll(in)
	if err != nil {
		log.Fatal("Could not read from stdin")
	}
	tpl := template.Must(template.New("osconfig").Parse(string(bytes)))
	return tpl.Execute(out, env2map(os.Environ()))
}

func env2map(env []string) map[string]string {
	m := make(map[string]string, len(env))
	for _, s := range env {
		d := strings.Split(s, "=")
		m[d[0]] = d[1]
	}
	return m
}

func editSyslinux(c *cli.Context) error {
	// check whether is Raspberry Pi or not
	bytes, err := ioutil.ReadFile("/proc/device-tree/model")
	if err == nil && strings.Contains(strings.ToLower(string(bytes)), "raspberry") {
		buf := bufio.NewWriter(os.Stdout)
		fmt.Fprintln(buf, "raspberry pi can not use this command")
		buf.Flush()
		return errors.New("raspberry pi can not use this command")
	}

	if _, err := os.Stat("/proc/1/root/boot/global.cfg"); os.IsNotExist(err) {
		buf := bufio.NewWriter(os.Stdout)
		fmt.Fprintln(buf, "global.cfg can not be found")
		buf.Flush()
		return errors.New("global.cfg can not be found")
	}

	cmd := exec.Command("system-docker", "run", "--rm", "-it",
		"-v", "/:/host",
		"-w", "/host",
		"--entrypoint=vi",
		"rancher/os-console:"+config.Version,
		"boot/global.cfg")
	cmd.Stdout, cmd.Stderr, cmd.Stdin = os.Stdout, os.Stderr, os.Stdin
	return cmd.Run()
}

func configSet(c *cli.Context) error {
	if c.NArg() < 2 {
		return nil
	}

	key := c.Args().Get(0)
	value := c.Args().Get(1)
	if key == "" {
		return nil
	}

	err := config.Set(key, value)
	if err != nil {
		log.Fatal(err)
	}

	return nil
}

func configGet(c *cli.Context) error {
	arg := c.Args().Get(0)
	if arg == "" {
		return nil
	}

	val, err := config.Get(arg)
	if err != nil {
		log.WithFields(log.Fields{"key": arg, "val": val, "err": err}).Fatal("config get: failed to retrieve value")
	}

	printYaml := false
	switch val.(type) {
	case []interface{}:
		printYaml = true
	case map[interface{}]interface{}:
		printYaml = true
	}

	if printYaml {
		bytes, err := yaml.Marshal(val)
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println(string(bytes))
	} else {
		fmt.Println(val)
	}

	return nil
}

func merge(c *cli.Context) error {
	bytes, err := inputBytes(c)
	if err != nil {
		log.Fatal(err)
	}

	if err = config.Merge(bytes); err != nil {
		log.Error(err)
		validationErrors, err := config.ValidateBytes(bytes)
		if err != nil {
			log.Fatal(err)
		}
		for _, validationError := range validationErrors.Errors() {
			log.Error(validationError)
		}
		log.Fatal("EXITING: Failed to parse configuration")
	}

	return nil
}

func export(c *cli.Context) error {
	content, err := config.Export(c.Bool("private"), c.Bool("full"))
	if err != nil {
		log.Fatal(err)
	}

	output := c.String("output")
	if output == "" {
		fmt.Println(content)
	} else {
		err := util.WriteFileAtomic(output, []byte(content), 0400)
		if err != nil {
			log.Fatal(err)
		}
	}

	return nil
}

func validate(c *cli.Context) error {
	bytes, err := inputBytes(c)
	if err != nil {
		log.Fatal(err)
	}
	validationErrors, err := config.ValidateBytes(bytes)
	if err != nil {
		log.Fatal(err)
	}
	for _, validationError := range validationErrors.Errors() {
		log.Error(validationError)
	}
	return nil
}

func inputBytes(c *cli.Context) ([]byte, error) {
	input := os.Stdin
	inputFile := c.String("input")
	if inputFile != "" {
		var err error
		input, err = os.Open(inputFile)
		if err != nil {
			return nil, err
		}
		defer input.Close()
	}
	return ioutil.ReadAll(input)
}
