package config

import (
	"context"
	"errors"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"

	"github.com/fatih/color"
	api "github.com/porter-dev/porter/api/client"
	"github.com/porter-dev/porter/cli/cmd/utils"
	"github.com/spf13/viper"
	"k8s.io/client-go/util/homedir"
)

var home = homedir.HomeDir()

// config is a shared object used by all commands
var config = &CLIConfig{}

// CLIConfig is the set of shared configuration options for the CLI commands.
// This config is used by viper: calling Set() function for any parameter will
// update the corresponding field in the viper config file.
type CLIConfig struct {
	// Driver can be either "docker" or "local", and represents which driver is
	// used to run an instance of the server.
	Driver string `yaml:"driver"`

	Host    string `yaml:"host"`
	Project uint   `yaml:"project"`
	Cluster uint   `yaml:"cluster"`

	Token string `yaml:"token"`

	Registry   uint   `yaml:"registry"`
	HelmRepo   uint   `yaml:"helm_repo"`
	Kubeconfig string `yaml:"kubeconfig"`
}

// InitAndLoadConfig populates the config object with the following precedence rules:
// 1. flag
// 2. env
// 3. config
// 4. default
//
// It populates the shared config object above
func InitAndLoadConfig() {
	initAndLoadConfig(config)
}

func InitAndLoadNewConfig() *CLIConfig {
	newConfig := &CLIConfig{}

	initAndLoadConfig(newConfig)

	return newConfig
}

func initAndLoadConfig(_config *CLIConfig) {
	initFlagSet()

	// check that the .porter folder exists; create if not
	porterDir := filepath.Join(home, ".porter")

	if _, err := os.Stat(porterDir); os.IsNotExist(err) {
		os.Mkdir(porterDir, 0o700)
	} else if err != nil {
		color.New(color.FgRed).Fprintf(os.Stderr, "%v\n", err)
		os.Exit(1)
	}

	viper.SetConfigName("porter")
	viper.SetConfigType("yaml")
	viper.AddConfigPath(porterDir)

	// Bind the flagset initialized above
	viper.BindPFlags(utils.DriverFlagSet)
	viper.BindPFlags(utils.DefaultFlagSet)
	viper.BindPFlags(utils.RegistryFlagSet)
	viper.BindPFlags(utils.HelmRepoFlagSet)

	// Bind the environment variables with prefix "PORTER_"
	viper.SetEnvPrefix("PORTER")
	viper.BindEnv("host")
	viper.BindEnv("project")
	viper.BindEnv("cluster")
	viper.BindEnv("token")

	err := viper.ReadInConfig()
	if err != nil {
		if _, ok := err.(viper.ConfigFileNotFoundError); ok {
			// create blank config file
			err := ioutil.WriteFile(filepath.Join(home, ".porter", "porter.yaml"), []byte{}, 0o644)
			if err != nil {
				color.New(color.FgRed).Fprintf(os.Stderr, "%v\n", err)
				os.Exit(1)
			}
		} else {
			// Config file was found but another error was produced
			color.New(color.FgRed).Fprintf(os.Stderr, "%v\n", err)
			os.Exit(1)
		}
	}

	// unmarshal the config into the shared config struct
	viper.Unmarshal(_config)
}

// initFlagSet initializes the shared flags used by multiple commands
func initFlagSet() {
	utils.DriverFlagSet.StringVar(
		&config.Driver,
		"driver",
		"local",
		"driver to use (local or docker)",
	)

	utils.DefaultFlagSet.StringVar(
		&config.Host,
		"host",
		"https://dashboard.getporter.dev",
		"host URL of Porter instance",
	)

	utils.DefaultFlagSet.UintVar(
		&config.Project,
		"project",
		0,
		"project ID of Porter project",
	)

	utils.DefaultFlagSet.UintVar(
		&config.Cluster,
		"cluster",
		0,
		"cluster ID of Porter cluster",
	)

	utils.DefaultFlagSet.StringVar(
		&config.Token,
		"token",
		"",
		"token for Porter authentication",
	)

	utils.RegistryFlagSet.UintVar(
		&config.Registry,
		"registry",
		0,
		"registry ID of connected Porter registry",
	)

	utils.HelmRepoFlagSet.UintVar(
		&config.HelmRepo,
		"helmrepo",
		0,
		"helm repo ID of connected Porter Helm repository",
	)
}

func GetCLIConfig() *CLIConfig {
	if config == nil {
		panic("GetCLIConfig() called before initialisation")
	}

	return config
}

func GetAPIClient() *api.Client {
	config := GetCLIConfig()

	if token := config.Token; token != "" {
		return api.NewClientWithToken(config.Host+"/api", token)
	}

	return api.NewClient(config.Host+"/api", "cookie.json")
}

func (c *CLIConfig) SetDriver(driver string) error {
	viper.Set("driver", driver)
	color.New(color.FgGreen).Printf("Set the current driver as %s\n", driver)
	err := viper.WriteConfig()
	if err != nil {
		return err
	}

	config.Driver = driver

	return nil
}

func (c *CLIConfig) SetHost(host string) error {
	// a trailing / can lead to errors with the api server
	host = strings.TrimRight(host, "/")

	viper.Set("host", host)

	// let us clear the project ID, cluster ID, and token when we reset a host
	viper.Set("project", 0)
	viper.Set("cluster", 0)
	viper.Set("token", "")

	err := viper.WriteConfig()
	if err != nil {
		return err
	}

	color.New(color.FgGreen).Printf("Set the current host as %s\n", host)

	config.Host = host
	config.Project = 0
	config.Cluster = 0
	config.Token = ""

	return nil
}

func (c *CLIConfig) SetProject(projectID uint) error {
	viper.Set("project", projectID)

	color.New(color.FgGreen).Printf("Set the current project as %d\n", projectID)

	if config.Kubeconfig != "" || viper.IsSet("kubeconfig") {
		color.New(color.FgYellow).Println("Please change local kubeconfig if needed")
	}

	err := viper.WriteConfig()
	if err != nil {
		return err
	}

	config.Project = projectID

	client := GetAPIClient()
	if client != nil {
		resp, err := client.ListProjectClusters(context.Background(), projectID)
		if err == nil {
			clusters := *resp
			if len(clusters) == 1 {
				c.SetCluster(clusters[0].ID)
			}
		}
	}

	return nil
}

func (c *CLIConfig) SetCluster(clusterID uint) error {
	viper.Set("cluster", clusterID)

	color.New(color.FgGreen).Printf("Set the current cluster as %d\n", clusterID)

	if config.Kubeconfig != "" || viper.IsSet("kubeconfig") {
		color.New(color.FgYellow).Println("Please change local kubeconfig if needed")
	}

	err := viper.WriteConfig()
	if err != nil {
		return err
	}

	config.Cluster = clusterID

	return nil
}

func (c *CLIConfig) SetToken(token string) error {
	viper.Set("token", token)
	err := viper.WriteConfig()
	if err != nil {
		return err
	}

	config.Token = token

	return nil
}

func (c *CLIConfig) SetRegistry(registryID uint) error {
	viper.Set("registry", registryID)
	color.New(color.FgGreen).Printf("Set the current registry as %d\n", registryID)
	err := viper.WriteConfig()
	if err != nil {
		return err
	}

	config.Registry = registryID

	return nil
}

func (c *CLIConfig) SetHelmRepo(helmRepoID uint) error {
	viper.Set("helm_repo", helmRepoID)
	color.New(color.FgGreen).Printf("Set the current Helm repo as %d\n", helmRepoID)
	err := viper.WriteConfig()
	if err != nil {
		return err
	}

	config.HelmRepo = helmRepoID

	return nil
}

func (c *CLIConfig) SetKubeconfig(kubeconfig string) error {
	path, err := filepath.Abs(kubeconfig)
	if err != nil {
		return err
	}

	if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
		return fmt.Errorf("%s does not exist", path)
	}

	viper.Set("kubeconfig", path)
	color.New(color.FgGreen).Printf("Set the path to kubeconfig as %s\n", path)
	err = viper.WriteConfig()

	if err != nil {
		return err
	}

	config.Kubeconfig = kubeconfig

	return nil
}

func ValidateCLIEnvironment() error {
	if GetCLIConfig().Token == "" {
		return fmt.Errorf("no auth token present, please run 'porter auth login' to authenticate")
	}

	if GetCLIConfig().Project == 0 {
		return fmt.Errorf("no project selected, please run 'porter config set-project' to select a project")
	}

	if GetCLIConfig().Cluster == 0 {
		return fmt.Errorf("no cluster selected, please run 'porter config set-cluster' to select a cluster")
	}

	return nil
}
