package config

import (
	"flag"
	"log"
	"os"
	"reflect"
	"strconv"
	"time"

	"crypto/rand"
	"encoding/hex"
	"io/ioutil"

	"gopkg.in/yaml.v2"
)

// getEnvString returns string from environment variable
func getEnvString(env, def string) (val string) { //nolint:deadcode
	val = os.Getenv(env)

	if val == "" {
		return def
	}

	return
}

// getEnvInt returns int from environment variable
func getEnvInt(env string, def int) (ret int) {
	val := os.Getenv(env)

	if val == "" {
		return def
	}

	ret, err := strconv.Atoi(val)
	if err != nil {
		log.Fatalf("[ERROR] Environment variable is not numeric: %v\n", env)
	}

	return
}

// getEnvBool returns boolean from environment variable
func getEnvBool(env string, def bool) bool {
	var (
		err    error
		retVal bool
		val    = os.Getenv(env)
	)

	if len(val) == 0 {
		return def
	} else {
		retVal, err = strconv.ParseBool(val)
		if err != nil {
			log.Fatalf("[ERROR] Environment variable is not boolean: %v\n", env)
		}
	}

	return retVal
}

// Init initializes the application configuration by reading default values from the struct's tags
// and environment variables. Tags processed by this process are as follows:
// `ignored:"true" env:"ENVIRONMENT_VARIABLE" default:"default value"`
func Init() *Config {
	var cryptovault string
	cfg := DefaultConfig()

	cfgInfo, err := getStructInfo(cfg)
	if err != nil {
		log.Fatalf("[FATAL] %v", err)
	}

	for _, info := range cfgInfo {
		switch info.Type.String() {
		case "string":
			var dv string

			if info.DefaultValue != nil {
				dv = info.DefaultValue.(string)
			}
			p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*string)
			flag.StringVar(p, info.Name, getEnvString(info.Name, dv), "("+info.Key+")")
		case "bool":
			var dv bool

			if info.DefaultValue != nil {
				dv = info.DefaultValue.(bool)
			}
			p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*bool)
			flag.BoolVar(p, info.Name, getEnvBool(info.Name, dv), "("+info.Key+")")
		case "int":
			var dv int

			if info.DefaultValue != nil {
				dv = int(info.DefaultValue.(int64))
			}
			p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*int)
			flag.IntVar(p, info.Name, getEnvInt(info.Name, dv), "("+info.Key+")")
		}
	}

	flag.StringVar(&cryptovault, "label", "", "Deprecated support feature. Please use -SecretID.\nCryptovault compatibility feature used to identify the ID of the secret")
	flag.Parse()

	if len(cryptovault) != 0 {
		cfg.SecretID = cryptovault
	}

	// set logging level
	cfg.setLogLevel()

	// check if configuration file is present, if not create the example config file
	if _, err := os.Stat(cfg.ConfigFile); os.IsNotExist(err) {
		log.Printf("[WARNING] No configuration file present. Creating example configuration file at %s", cfg.ConfigFile)
		file, err := os.Create(cfg.ConfigFile)
		if err != nil {
			log.Fatalf("[ERROR] Unable to create the config file %s: %v", cfg.ConfigFile, err)
		}
		defer file.Close()

		file.Write([]byte(cfg.ConfigFileExample))
	}

	// readConfiguration File
	fileData, err := ioutil.ReadFile(cfg.ConfigFile)
	if err != nil {
		log.Fatalf("[ERROR] Unable to read the config file %s: %v", cfg.ConfigFile, err)
	}
	err = yaml.Unmarshal(fileData, &cfg.ConfigFileData)
	if err != nil {
		log.Printf("[ERROR] Unable to process the config file %s: %v", cfg.ConfigFile, err)
	}

	// make sure there is a secure salt
	if cfg.ConfigFileData.Salt == "" || cfg.ConfigFileData.Salt == "exampleSalt" {
		s := make([]byte, 32)
		rand.Read(s)
		cfg.ConfigFileData.Salt = hex.EncodeToString(s)
	}

	// timezone & format configuration
	cfg.TZoneUTC, _ = time.LoadLocation("UTC")
	if err != nil {
		log.Fatalf("[ERROR] Unable to parse timezone string. Please use one of the timezone database values listed here: %s", "https://en.wikipedia.org/wiki/List_of_tz_database_time_zones")
	}
	cfg.TZoneLocal, err = time.LoadLocation(cfg.TimeZoneLocal)
	if err != nil {
		log.Fatalf("[ERROR] Unable to parse timezone string. Please use one of the timezone database values listed here: %s", "https://en.wikipedia.org/wiki/List_of_tz_database_time_zones")
	}
	time.Now().Format(cfg.TimeFormat)

	// print running config
	cfg.printRunningConfig(cfgInfo)

	log.Println("[INFO] initialization complete")
	return cfg
}