package runner

import (
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"text/template"

	"github.com/rs/zerolog"
	"github.com/spf13/viper"
)

// Variable ...
type Variable struct {
	Name  string `mapstructure:"name"`
	Value string `mapstructure:"value"`
}

type Chart struct {
	Type string `mapstructure:"type"`
	Path string `mapstructure:"path"`
}

// Spec ...
type Spec struct {
	Variables []Variable       `mapstructure:"variables"`
	Charts    map[string]Chart `mapstructure:"charts"`
}

// Config is the configuration we get after parsing our beaver.yml file
type Config struct {
	APIVersion string `mapstructure:"apiVersion"`
	Kind       string `mapstructure:"kind"`
	Spec       Spec   `mapstructure:"spec"`
}

// NewConfig returns a *Config
func NewConfig(configDir string) (*Config, error) {
	v := viper.New()
	v.SetConfigName("beaver")
	v.AddConfigPath(configDir)
	if err := v.ReadInConfig(); err != nil {
		return nil, err
	}
	var config Config
	cfg := &config
	if err := v.Unmarshal(&cfg); err != nil {
		return nil, err
	}

	return cfg, nil
}

func NewCmdConfig(logger zerolog.Logger, configDir string, namespace string, dryRun bool) *CmdConfig {
	cmdConfig := &CmdConfig{}
	cmdConfig.DryRun = dryRun
	cmdConfig.RootDir = configDir
	cmdConfig.Spec.Charts = make(map[string]CmdChart)
	cmdConfig.Namespace = namespace
	cmdConfig.Logger = logger
	return cmdConfig
}

func (c *CmdConfig) Initialize(tmpDir string) error {
	baseCfg, err := NewConfig(c.RootDir)
	if err != nil {
		return err
	}

	nsCfgDir := filepath.Join(c.RootDir, "environments", c.Namespace)
	nsCfg, err := NewConfig(nsCfgDir)
	var nsCfgNotFound bool
	if err != nil {
		_, nsCfgNotFound = err.(viper.ConfigFileNotFoundError)
		if !nsCfgNotFound {
			return err
		}
	}

	if !nsCfgNotFound {
		// first "import" all variables from baseCfg
		c.Spec.Variables = baseCfg.Spec.Variables
		// then merge in all variables from the nsCfg
		c.MergeVariables(nsCfg)
	}

	for k, chart := range baseCfg.Spec.Charts {
		c.Spec.Charts[k] = cmdChartFromChart(chart)
	}

	c.populate()

	// - hydrate
	if err := c.hydrate(tmpDir); err != nil {
		return err
	}

	return nil
}

type CmdConfig struct {
	Spec      CmdSpec
	RootDir   string
	Namespace string
	Logger    zerolog.Logger
	DryRun    bool
}

type CmdSpec struct {
	Variables []Variable
	Charts    CmdCharts
	Ytt       Ytt
}

type Ytt []string

func (y Ytt) BuildArgs(namespace string, compiled []string) []string {
	// ytt -f $chartsTmpFile --file-mark "$(basename $chartsTmpFile):type=yaml-plain"\
	//   -f base/ytt/ -f base/ytt.yml -f ns1/ytt/ -f ns1/ytt.yml
	var args []string
	for _, c := range compiled {
		args = append(args, "-f", c, "--file-mark", filepath.Base(c))
	}
	for _, entry := range []string{
		filepath.Join("base", "ytt"),
		filepath.Join("base", "ytt.yaml"),
		filepath.Join("environments", namespace, "ytt"),
		filepath.Join("environments", namespace, "ytt.yaml")} {

		if _, err := os.Stat(entry); !os.IsExist(err) {
			args = append(args, "-f", entry)
		}
	}
	return args
}

type CmdCharts map[string]CmdChart

type CmdChart struct {
	Type            string
	Path            string
	ValuesFileNames []string
}

const (
	HelmType = "helm"
	YttType  = "ytt"
)

// BuildArgs is in charge of producing the argument list to be provided
// to the cmd
func (c CmdChart) BuildArgs(name, namespace string) ([]string, error) {
	var args []string
	switch c.Type {
	case HelmType:
		// helm template name vendor/helm/mychart/ --namespace ns1 -f base.values.yaml -f ns.yaml -f ns.values.yaml
		args = append(args, "template", name, c.Path, "--namespace", namespace)
	case YttType:
		args = append(args, "-f", c.Path)
	default:
		return nil, fmt.Errorf("unsupported chart %s type: %q", c.Path, c.Type)
	}
	for _, vFile := range c.ValuesFileNames {
		args = append(args, "-f", vFile)
	}
	return args, nil
}

func cmdChartFromChart(c Chart) CmdChart {
	return CmdChart{
		Type:            c.Type,
		Path:            c.Path,
		ValuesFileNames: nil,
	}
}

// hydrate expands templated variables in our config with concrete values
func (c *CmdConfig) hydrate(tmpDir string) error {
	c.Logger.Debug().Str("charts", fmt.Sprintf("%+v\n", c.Spec.Charts)).Msg("before hydrate")
	if err := c.hydrateFiles(tmpDir); err != nil {
		return err
	}
	c.Logger.Debug().Str("charts", fmt.Sprintf("%+v\n", c.Spec.Charts)).Msg("after hydrate")
	return nil
}

func (c *CmdConfig) prepareVariables(v []Variable) map[string]string {
	variables := make(map[string]string)
	for _, variable := range v {
		variables[variable.Name] = variable.Value
	}
	variables["namespace"] = c.Namespace
	return variables
}

func (c *CmdConfig) populate() {
	c.Spec.Charts = findFiles(c.RootDir, c.Namespace, c.Spec.Charts)
	c.Spec.Ytt = findYttFiles(c.RootDir, c.Namespace)
}

func findYttFiles(rootDir, namespace string) []string {
	var result []string
	for _, dir := range []string{"base", filepath.Join("environments", namespace)} {
		fPath := filepath.Join(rootDir, dir, "ytt")
		baseYttDirInfo, err := os.Stat(fPath)
		if err == nil && baseYttDirInfo.IsDir() {
			result = append(result, fPath)
		}

		for _, ext := range []string{"yaml", "yml"} {
			fPath := filepath.Join(rootDir, dir, fmt.Sprintf("ytt.%s", ext))
			baseYttFileInfo, err := os.Stat(fPath)
			if err == nil && !baseYttFileInfo.IsDir() {
				result = append(result, fPath)
			}
		}
	}
	return result
}

func findFiles(rootdir, namespace string, charts map[string]CmdChart) map[string]CmdChart {
	for name, chart := range charts {
		files := findYaml(rootdir, namespace, name)
		chart.ValuesFileNames = append(chart.ValuesFileNames, files...)
		charts[name] = chart
	}
	return charts
}

func findYaml(rootDir, namespace, name string) []string {
	var files []string
	for _, folder := range []string{"base", filepath.Join("environments", namespace)} {
		for _, ext := range []string{"yaml", "yml"} {
			fpath := filepath.Join(rootDir, folder, fmt.Sprintf("%s.%s", name, ext))
			if _, err := os.Stat(fpath); err == nil {
				files = append(files, fpath)
			}
		}
	}
	return files
}

func hydrateFiles(tmpDir string, variables map[string]string, paths []string) ([]string, error) {
	var result []string
	for _, path := range paths {
		fileInfo, err := os.Stat(path)
		if err != nil {
			return nil, fmt.Errorf("hydrateFiles could not stat file or dir %s: %w", path, err)
		}
		if fileInfo.IsDir() {
			result = append(result, path)
			continue
		}

		if tmpl, err := template.New(filepath.Base(path)).ParseFiles(path); err != nil {
			return nil, err
		} else {
			if tmpFile, err := ioutil.TempFile(tmpDir, fmt.Sprintf("%s-", filepath.Base(path))); err != nil {
				return nil, fmt.Errorf("hydrateFiles failed to create tempfile: %w", err)
			} else {
				defer func() {
					_ = tmpFile.Close()
				}()
				if err := tmpl.Execute(tmpFile, variables); err != nil {
					return nil, fmt.Errorf("hydrateFiles failed to execute template: %w", err)
				}
				result = append(result, tmpFile.Name())
			}
		}
	}
	return result, nil
}

func (c *CmdConfig) hydrateFiles(dirName string) error {
	variables := c.prepareVariables(c.Spec.Variables)

	for key, chart := range c.Spec.Charts {
		if paths, err := hydrateFiles(dirName, variables, chart.ValuesFileNames); err != nil {
			return err
		} else {
			chart.ValuesFileNames = paths
			c.Spec.Charts[key] = chart
		}
	}
	return nil
}

// MergeVariables takes a config (from a file, not a cmd one) and import its
// variables into the current cmdconfig by replacing old ones
// and adding the new ones
func (c *CmdConfig) MergeVariables(other *Config) {
	for _, variable := range other.Spec.Variables {
		c.overlayVariable(variable)
	}
}

// overlayVariable takes a variable in and either replaces an existing variable
// of the same name or create a new variable in the config if no matching name
// is found
func (c *CmdConfig) overlayVariable(v Variable) {
	// find same variable by name and replace is value
	// if not found then create the variable
	for index, originalVariable := range c.Spec.Variables {
		if originalVariable.Name == v.Name {
			c.Spec.Variables[index].Value = v.Value
			return
		}
	}
	c.Spec.Variables = append(c.Spec.Variables, v)
}