package main

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

	"github.com/hashicorp/logutils"
	"github.com/prometheus/client_golang/prometheus"
)

type config struct {
	// time configuration
	TimeFormat    string         `env:"TIME_FORMAT" default:"2006-01-02 15:04:05"`
	TimeZoneLocal string         `env:"TIME_ZONE" default:"America/Chicago"`
	TZoneLocal    *time.Location `ignored:"true"`
	TZoneUTC      *time.Location `ignored:"true"`

	// logging
	LogLevel int                   `env:"LOG_LEVEL" default:"20"`
	Log      *logutils.LevelFilter `ignored:"true"`

	// webserver
	WebSrvPort         int    `env:"WEBSRV_PORT" default:"8080"`
	WebSrvIP           string `env:"WEBSRV_IP" default:"0.0.0.0"`
	WebSrvReadTimeout  int    `env:"WEBSRV_TO_READ" default:"5"`
	WebSrvWriteTimeout int    `env:"WEBSRV_TO_WRITE" default:"2"`
	WebSrvIdleTimeout  int    `env:"WEBSRV_TO_READ" default:"2"`

	// devices
	Hosts []string `ignored:"true"`

	// prometheus
	Prometheus configPrometheus `ignored:"true"`
}

type hostStruct []string

func (i *hostStruct) String() string {
	return "dunno"
}
func (i *hostStruct) Set(value string) error {
	*i = append(*i, value)
	return nil
}

type structInfo struct {
	Name         string
	Alt          string
	Key          string
	Field        reflect.Value
	Tags         reflect.StructTag
	Type         reflect.Type
	DefaultValue interface{}
}

type configPrometheus struct {
	CurrentMa *prometheus.GaugeVec
	VoltageMv *prometheus.GaugeVec
	PowerMw   *prometheus.GaugeVec
	TotalWh   *prometheus.CounterVec
}

func defaultConfig() *config {
	return &config{
		Log: &logutils.LevelFilter{
			Levels: []logutils.LogLevel{"TRACE", "DEBUG", "INFO", "WARNING", "ERROR"},
			Writer: os.Stderr,
		},
		Prometheus: configPrometheus{
			CurrentMa: prometheus.NewGaugeVec(prometheus.GaugeOpts{
				Namespace: "tplink",
				Name:      "unknown",
				Help:      "unknown",
			}, []string{"device"}),
			VoltageMv: prometheus.NewGaugeVec(prometheus.GaugeOpts{
				Namespace: "tplink",
				Name:      "volts",
				Help:      "input voltage",
			}, []string{"device"}),
			PowerMw: prometheus.NewGaugeVec(prometheus.GaugeOpts{
				Namespace: "tplink",
				Name:      "watts",
				Help:      "current wattage",
			}, []string{"device"}),
			TotalWh: prometheus.NewCounterVec(prometheus.CounterOpts{
				Namespace: "tplink",
				Name:      "watt_hours",
				Help:      "total watt hours",
			}, []string{"device"}),
		},
	}
}

func (cfg *config) setLogLevel() {
	switch {
	case cfg.LogLevel <= 20:
		cfg.Log.SetMinLevel(logutils.LogLevel("ERROR"))
	case cfg.LogLevel > 20 && cfg.LogLevel <= 40:
		cfg.Log.SetMinLevel(logutils.LogLevel("WARNING"))
	case cfg.LogLevel > 40 && cfg.LogLevel <= 60:
		cfg.Log.SetMinLevel(logutils.LogLevel("INFO"))
	case cfg.LogLevel > 60 && cfg.LogLevel <= 80:
		cfg.Log.SetMinLevel(logutils.LogLevel("DEBUG"))
	case cfg.LogLevel > 80:
		cfg.Log.SetMinLevel(logutils.LogLevel("TRACE"))
	}
	log.SetOutput(cfg.Log)
}

func (cfg *config) printRunningConfig(cfgInfo []structInfo) {
	log.Printf("[DEBUG] Current Running configuration Values:")
	for _, info := range cfgInfo {
		switch info.Type.String() {
		case "string":
			p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*string)
			log.Printf("[DEBUG]\t%s\t\t= %s\n", info.Alt, *p)
		case "bool":
			p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*bool)
			log.Printf("[DEBUG]\t%s\t\t= %s\n", info.Alt, strconv.FormatBool(*p))
		case "int":
			p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*int)
			log.Printf("[DEBUG]\t%s\t\t= %s\n", info.Alt, strconv.FormatInt(int64(*p), 10))
		}
	}
}

func getStructInfo(spec interface{}) ([]structInfo, error) {
	s := reflect.ValueOf(spec)

	if s.Kind() != reflect.Pointer {
		return []structInfo{}, fmt.Errorf("getstructInfo() was sent a %s instead of a pointer to a struct.\n", s.Kind())
	}

	s = s.Elem()
	if s.Kind() != reflect.Struct {
		return []structInfo{}, fmt.Errorf("getstructInfo() was sent a %s instead of a struct.\n", s.Kind())
	}
	typeOfSpec := s.Type()

	infos := make([]structInfo, 0, s.NumField())
	for i := 0; i < s.NumField(); i++ {
		f := s.Field(i)
		ftype := typeOfSpec.Field(i)

		ignored, _ := strconv.ParseBool(ftype.Tag.Get("ignored"))
		if !f.CanSet() || ignored {
			continue
		}

		for f.Kind() == reflect.Pointer {
			if f.IsNil() {
				if f.Type().Elem().Kind() != reflect.Struct {
					break
				}
				f.Set(reflect.New(f.Type().Elem()))
			}
			f = f.Elem()
		}

		info := structInfo{
			Name:  ftype.Name,
			Alt:   strings.ToUpper(ftype.Tag.Get("env")),
			Key:   ftype.Name,
			Field: f,
			Tags:  ftype.Tag,
			Type:  ftype.Type,
		}
		if info.Alt != "" {
			info.Key = info.Alt
		}
		info.Key = strings.ToUpper(info.Key)
		if ftype.Tag.Get("default") != "" {
			v, err := typeConversion(ftype.Type.String(), ftype.Tag.Get("default"))
			if err != nil {
				return []structInfo{}, err
			}
			info.DefaultValue = v
		}
		infos = append(infos, info)
	}
	return infos, nil
}

func typeConversion(t, v string) (interface{}, error) {
	switch t {
	case "string":
		return v, nil
	case "int":
		return strconv.ParseInt(v, 10, 0)
	case "int8":
		return strconv.ParseInt(v, 10, 8)
	case "int16":
		return strconv.ParseInt(v, 10, 16)
	case "int32":
		return strconv.ParseInt(v, 10, 32)
	case "int64":
		return strconv.ParseInt(v, 10, 64)
	case "uint":
		return strconv.ParseUint(v, 10, 0)
	case "uint16":
		return strconv.ParseUint(v, 10, 16)
	case "uint32":
		return strconv.ParseUint(v, 10, 32)
	case "uint64":
		return strconv.ParseUint(v, 10, 64)
	case "float32":
		return strconv.ParseFloat(v, 32)
	case "float64":
		return strconv.ParseFloat(v, 64)
	case "complex64":
		return strconv.ParseComplex(v, 64)
	case "complex128":
		return strconv.ParseComplex(v, 128)
	case "bool":
		return strconv.ParseBool(v)
	}
	return nil, fmt.Errorf("Unable to identify type.")
}

// 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 initialize() *config {
	cfg := defaultConfig()
	var hosts hostStruct

	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.Var(&hosts, "host", "Host to monitor")
	flag.Parse()
	cfg.Hosts = hosts

	// set logging level
	cfg.setLogLevel()

	// 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)

	prometheus.MustRegister(cfg.Prometheus.CurrentMa, cfg.Prometheus.VoltageMv, cfg.Prometheus.PowerMw, cfg.Prometheus.TotalWh)

	// print running config
	cfg.printRunningConfig(cfgInfo)

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