package config

import (
	"fmt"
	"github.com/BurntSushi/toml"
	"os"
	"path/filepath"
	"reflect"
	"strconv"
	"strings"
)

var globConfig *Config

type Config struct {
	Profile struct {
		Dir     string `toml:"Dir"`
		Default struct {
			Password           string `toml:"Password"`
			NetworkMode        int    `toml:"NetworkMode"`
			Configurable       bool   `toml:"Configurable"`
			RunLevel           int    `toml:"RunLevel"`
			StartupInformation bool   `toml:"StartupInformation"`
			ExitAfter          string `toml:"ExitAfter"`
			KeepOnExit         bool   `toml:"KeepOnExit"`
		} `toml:"default"`
		Dynamic struct {
			Enable             bool   `toml:"Enable"`
			Password           string `toml:"Password"`
			NetworkMode        int    `toml:"NetworkMode"`
			Configurable       bool   `toml:"Configurable"`
			RunLevel           int    `toml:"RunLevel"`
			StartupInformation bool   `toml:"StartupInformation"`
			ExitAfter          string `toml:"ExitAfter"`
			KeepOnExit         bool   `toml:"KeepOnExit"`
		} `toml:"dynamic"`
	} `toml:"profile"`
	Api struct {
		Port      uint16 `toml:"Port"`
		Configure struct {
			Binary string `toml:"Binary"`
			Man    string `toml:"Man"`
		} `toml:"configure"`
	} `toml:"api"`
	SSH struct {
		Port       uint16 `toml:"Port"`
		Keyfile    string `toml:"Keyfile"`
		Passphrase string `toml:"Passphrase"`
	} `toml:"ssh"`
	Database struct {
		Sqlite3File string `toml:"Sqlite3File"`
	} `toml:"Database"`
	Network struct {
		Default struct {
			Subnet string `toml:"Subnet"`
		} `toml:"default"`
		Isolate struct {
			Subnet string `toml:"Subnet"`
		} `toml:"isolate"`
	} `toml:"network"`
	Logging struct {
		Level         string `toml:"Level"`
		OutputFile    string `toml:"OutputFile"`
		ErrorFile     string `toml:"ErrorFile"`
		ConsoleOutput bool   `toml:"ConsoleOutput"`
		ConsoleError  bool   `toml:"ConsoleError"`
	} `toml:"logging"`
}

func InitConfig(includeEnv bool) (*Config, error) {
	configFiles := []string{
		"./docker4ssh.conf",
		"~/.docker4ssh",
		"~/.config/docker4ssh.conf",
		"/etc/docker4ssh/docker4ssh.conf",
	}

	for _, file := range configFiles {
		if _, err := os.Stat(file); !os.IsNotExist(err) {
			return LoadConfig(file, includeEnv)
		}
	}

	return nil, fmt.Errorf("no speicfied config file (%s) could be found", strings.Join(configFiles, ", "))
}

func LoadConfig(file string, includeEnv bool) (*Config, error) {
	config := &Config{}

	if _, err := toml.DecodeFile(file, config); err != nil {
		return nil, err
	}

	// make paths absolute
	dir := filepath.Dir(file)
	config.Profile.Dir = absoluteFile(dir, config.Profile.Dir)
	config.Api.Configure.Binary = absoluteFile(dir, config.Api.Configure.Binary)
	config.Api.Configure.Man = absoluteFile(dir, config.Api.Configure.Man)
	config.SSH.Keyfile = absoluteFile(dir, config.SSH.Keyfile)
	config.Database.Sqlite3File = absoluteFile(dir, config.Database.Sqlite3File)
	config.Logging.OutputFile = absoluteFile(dir, config.Logging.OutputFile)
	config.Logging.ErrorFile = absoluteFile(dir, config.Logging.ErrorFile)

	if includeEnv {
		if err := updateFromEnv(config); err != nil {
			return nil, err
		}
	}

	return config, nil
}

func absoluteFile(path, file string) string {
	if filepath.IsAbs(file) {
		return file
	}
	return filepath.Join(path, file)
}

// updateFromEnv looks up if specific environment variable are given which can
// also be used to configure the program.
// Every key in the config file can also be specified via environment variables.
// The env variable syntax is SECTION_KEY -> e.g. DEFAULT_PASSWORD or API_PORT
func updateFromEnv(config *Config) error {
	re := reflect.ValueOf(config).Elem()
	rt := re.Type()

	for i := 0; i < re.NumField(); i++ {
		rf := re.Field(i)
		ree := rt.Field(i)

		if err := envParseField(strings.ToUpper(ree.Tag.Get("toml")), rf); err != nil {
			return err
		}
	}
	return nil
}

func envParseField(prefix string, value reflect.Value) error {
	for j := 0; j < value.NumField(); j++ {
		rtt := value.Type().Field(j)
		rff := value.Field(j)

		if rff.Kind() == reflect.Struct {
			if err := envParseField(fmt.Sprintf("%s_%s", prefix, strings.ToUpper(rtt.Tag.Get("toml"))), rff); err != nil {
				return err
			}
			continue
		}

		envName := fmt.Sprintf("%s_%s", prefix, strings.ToUpper(rtt.Tag.Get("toml")))
		val, ok := os.LookupEnv(envName)
		if !ok {
			continue
		}
		var expected string
		switch rff.Kind() {
		case reflect.String:
			rff.SetString(val)
			continue
		case reflect.Bool:
			b, err := strconv.ParseBool(val)
			if err == nil {
				rff.SetBool(b)
				continue
			}
			expected = "true / false (boolean)"
		case reflect.Uint16:
			ui, err := strconv.ParseUint(val, 10, 16)
			if err == nil {
				rff.SetUint(ui)
				continue
			}
			expected = "number (uint16)"
		default:
			return fmt.Errorf("parsed not implemented config type '%s'", rff.Kind())
		}
		return fmt.Errorf("failed to parse environment variable '%s': cannot parse value '%s' as %s", envName, val, expected)
	}
	return nil
}

func GetConfig() *Config {
	return globConfig
}

func SetConfig(config *Config) {
	globConfig = config
}