package main

import (
	"errors"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"strconv"

	"github.com/fleetdm/fleet/v4/pkg/secure"
	"github.com/ghodss/yaml"
	"github.com/urfave/cli/v2"
)

const (
	configFilePerms = 0600
)

type configFile struct {
	Contexts map[string]Context `json:"contexts"`
}

type Context struct {
	Address       string `json:"address"`
	Email         string `json:"email"`
	Token         string `json:"token"`
	TLSSkipVerify bool   `json:"tls-skip-verify"`
	RootCA        string `json:"rootca"`
	URLPrefix     string `json:"url-prefix"`
}

func configFlag() cli.Flag {
	homeDir, err := os.UserHomeDir()
	if err != nil {
		homeDir = "~"
	}
	defaultConfigPath := filepath.Join(homeDir, ".fleet", "config")
	return &cli.StringFlag{
		Name:    "config",
		Value:   defaultConfigPath,
		EnvVars: []string{"CONFIG"},
		Usage:   "Path to the fleetctl config file",
	}
}

func contextFlag() cli.Flag {
	return &cli.StringFlag{
		Name:    "context",
		Value:   "default",
		EnvVars: []string{"CONTEXT"},
		Usage:   "Name of fleetctl config context to use",
	}
}

func makeConfigIfNotExists(fp string) error {
	if _, err := os.Stat(filepath.Dir(fp)); errors.Is(err, os.ErrNotExist) {
		if err := secure.MkdirAll(filepath.Dir(fp), 0700); err != nil {
			return err
		}
	}

	f, err := secure.OpenFile(fp, os.O_RDONLY|os.O_CREATE, configFilePerms)
	if err == nil {
		f.Close()
	}
	return err
}

func readConfig(fp string) (configFile, error) {
	var c configFile
	b, err := ioutil.ReadFile(fp)
	if err != nil {
		return c, err
	}

	if err := yaml.Unmarshal(b, &c); err != nil {
		return c, fmt.Errorf("unmarshal config: %w", err)
	}

	if c.Contexts == nil {
		c.Contexts = map[string]Context{
			"default": {},
		}
	}
	return c, nil
}

func writeConfig(fp string, c configFile) error {
	b, err := yaml.Marshal(c)
	if err != nil {
		return err
	}

	return ioutil.WriteFile(fp, b, configFilePerms)
}

func getConfigValue(configPath, context, key string) (interface{}, error) {
	if err := makeConfigIfNotExists(configPath); err != nil {
		return nil, fmt.Errorf("error verifying that config exists at %s: %w", configPath, err)
	}

	config, err := readConfig(configPath)
	if err != nil {
		return nil, fmt.Errorf("error reading config at %s: %w", configPath, err)
	}

	currentContext, ok := config.Contexts[context]
	if !ok {
		fmt.Printf("[+] Context %q not found, creating it with default values\n", context)
		currentContext = Context{}
	}

	switch key {
	case "address":
		return currentContext.Address, nil
	case "email":
		return currentContext.Email, nil
	case "token":
		return currentContext.Token, nil
	case "rootca":
		return currentContext.RootCA, nil
	case "tls-skip-verify":
		if currentContext.TLSSkipVerify {
			return true, nil
		}
		return false, nil
	case "url-prefix":
		return currentContext.URLPrefix, nil
	default:
		return nil, fmt.Errorf("%q is an invalid key", key)
	}
}

func setConfigValue(configPath, context, key, value string) error {
	if err := makeConfigIfNotExists(configPath); err != nil {
		return fmt.Errorf("error verifying that config exists at %s: %w", configPath, err)
	}

	config, err := readConfig(configPath)
	if err != nil {
		return fmt.Errorf("error reading config at %s: %w", configPath, err)
	}

	currentContext, ok := config.Contexts[context]
	if !ok {
		fmt.Printf("[+] Context %q not found, creating it with default values\n", context)
		currentContext = Context{}
	}

	switch key {
	case "address":
		currentContext.Address = value
	case "email":
		currentContext.Email = value
	case "token":
		currentContext.Token = value
	case "rootca":
		currentContext.RootCA = value
	case "tls-skip-verify":
		boolValue, err := strconv.ParseBool(value)
		if err != nil {
			return fmt.Errorf("error parsing %q as bool: %w", value, err)
		}
		currentContext.TLSSkipVerify = boolValue
	case "url-prefix":
		currentContext.URLPrefix = value
	default:
		return fmt.Errorf("%q is an invalid option", key)
	}

	config.Contexts[context] = currentContext

	if err := writeConfig(configPath, config); err != nil {
		return fmt.Errorf("error saving config file: %w", err)
	}

	return nil
}

func configSetCommand() *cli.Command {
	var (
		flAddress       string
		flEmail         string
		flToken         string
		flTLSSkipVerify bool
		flRootCA        string
		flURLPrefix     string
	)
	return &cli.Command{
		Name:      "set",
		Usage:     "Set config options",
		UsageText: `fleetctl config set [options]`,
		Flags: []cli.Flag{
			configFlag(),
			contextFlag(),
			&cli.StringFlag{
				Name:        "address",
				EnvVars:     []string{"ADDRESS"},
				Value:       "",
				Destination: &flAddress,
				Usage:       "Address of the Fleet server",
			},
			&cli.StringFlag{
				Name:        "email",
				EnvVars:     []string{"EMAIL"},
				Value:       "",
				Destination: &flEmail,
				Usage:       "Email to use when connecting to the Fleet server",
			},
			&cli.StringFlag{
				Name:        "token",
				EnvVars:     []string{"TOKEN"},
				Value:       "",
				Destination: &flToken,
				Usage:       "Fleet API token",
			},
			&cli.BoolFlag{
				Name:        "tls-skip-verify",
				EnvVars:     []string{"INSECURE"},
				Destination: &flTLSSkipVerify,
				Usage:       "Skip TLS certificate validation",
			},
			&cli.StringFlag{
				Name:        "rootca",
				EnvVars:     []string{"ROOTCA"},
				Value:       "",
				Destination: &flRootCA,
				Usage:       "Specify RootCA chain used to communicate with Fleet",
			},
			&cli.StringFlag{
				Name:        "url-prefix",
				EnvVars:     []string{"URL_PREFIX"},
				Value:       "",
				Destination: &flURLPrefix,
				Usage:       "Specify URL Prefix to use with Fleet server (copy from server configuration)",
			},
		},
		Action: func(c *cli.Context) error {
			set := false

			configPath, context := c.String("config"), c.String("context")

			if flAddress != "" {
				set = true
				if err := setConfigValue(configPath, context, "address", flAddress); err != nil {
					return fmt.Errorf("error setting address: %w", err)
				}
				fmt.Printf("[+] Set the address config key to %q in the %q context\n", flAddress, c.String("context"))
			}

			if flEmail != "" {
				set = true
				if err := setConfigValue(configPath, context, "email", flEmail); err != nil {
					return fmt.Errorf("error setting email: %w", err)
				}
				fmt.Printf("[+] Set the email config key to %q in the %q context\n", flEmail, c.String("context"))
			}

			if flToken != "" {
				set = true
				if err := setConfigValue(configPath, context, "token", flToken); err != nil {
					return fmt.Errorf("error setting token: %w", err)
				}
				fmt.Printf("[+] Set the token config key to %q in the %q context\n", flToken, c.String("context"))
			}

			if flTLSSkipVerify {
				set = true
				if err := setConfigValue(configPath, context, "tls-skip-verify", "true"); err != nil {
					return fmt.Errorf("error setting tls-skip-verify: %w", err)
				}
				fmt.Printf("[+] Set the tls-skip-verify config key to \"true\" in the %q context\n", c.String("context"))
			}

			if flRootCA != "" {
				set = true
				if err := setConfigValue(configPath, context, "rootca", flRootCA); err != nil {
					return fmt.Errorf("error setting rootca: %w", err)
				}
				fmt.Printf("[+] Set the rootca config key to %q in the %q context\n", flRootCA, c.String("context"))
			}

			if flURLPrefix != "" {
				set = true
				if err := setConfigValue(configPath, context, "url-prefix", flURLPrefix); err != nil {
					return fmt.Errorf("error setting URL Prefix: %w", err)
				}
				fmt.Printf("[+] Set the url-prefix config key to %q in the %q context\n", flURLPrefix, c.String("context"))
			}

			if !set {
				return cli.ShowCommandHelp(c, "set")
			}

			return nil
		},
	}
}

func configGetCommand() *cli.Command {
	return &cli.Command{
		Name:      "get",
		Usage:     "Get a config option",
		UsageText: `fleetctl config get [options]`,
		Flags: []cli.Flag{
			configFlag(),
			contextFlag(),
		},
		Action: func(c *cli.Context) error {
			if c.Args().Len() != 1 {
				return cli.ShowCommandHelp(c, "get")
			}

			key := c.Args().Get(0)

			// validate key
			switch key {
			case "address", "email", "token", "tls-skip-verify", "rootca":
			default:
				return cli.ShowCommandHelp(c, "get")
			}

			configPath, context := c.String("config"), c.String("context")

			value, err := getConfigValue(configPath, context, key)
			if err != nil {
				return fmt.Errorf("error getting config value: %w", err)
			}

			fmt.Printf("  %s.%s => %s\n", c.String("context"), key, value)

			return nil
		},
	}
}
