package config

import (
	"flag"
	"fmt"
	"os"
	"reflect"
	"strconv"
	"strings"
)

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

func getEnv[t string | bool | int | int64 | float64](env string, def t) (t, error) {
	val := os.Getenv(env)
	if len(val) == 0 {
		return def, nil
	}

	output := *new(t)
	switch (interface{})(def).(type) {
	case string:
		v, err := typeConversion("string", val)
		if err != nil {
			return (interface{})(false).(t), err
		}
		output = v.(t)
	case bool:
		v, err := typeConversion("bool", val)
		if err != nil {
			return (interface{})(false).(t), err
		}
		output = v.(t)
	case int:
		v, err := typeConversion("int", val)
		if err != nil {
			return (interface{})(int(0)).(t), err
		}
		output = (interface{})(v.(int)).(t)
	case int64:
		v, err := typeConversion("int64", val)
		if err != nil {
			return (interface{})(int64(0)).(t), err
		}
		output = v.(t)
	case float64:
		v, err := typeConversion("float64", val)
		if err != nil {
			return (interface{})(float64(0)).(t), err
		}
		output = v.(t)
	}

	return output, nil
}

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()
		}

		secret, err := typeConversion(ftype.Type.String(), ftype.Tag.Get("secret"))
		if err != nil {
			secret = false
		}

		var desc string
		if len(ftype.Tag.Get("info")) != 0 {
			desc = fmt.Sprintf("(%s) %s", strings.ToUpper(ftype.Tag.Get("env")), ftype.Tag.Get("info"))
		} else {
			desc = fmt.Sprintf("(%s)", strings.ToUpper(ftype.Tag.Get("env")))
		}

		info := structInfo{
			Name:   ftype.Name,
			Alt:    strings.ToUpper(ftype.Tag.Get("env")),
			Info:   desc,
			Key:    ftype.Name,
			Field:  f,
			Tags:   ftype.Tag,
			Type:   ftype.Type,
			Secret: secret,
		}
		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": //nolint:goconst
		return v, nil
	case "int": //nolint:goconst
		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": //nolint:goconst
		return strconv.ParseBool(v)
	}
	return nil, fmt.Errorf("Unable to identify type.")
}

func (cfg *Config) parseFlags(cfgInfo []structInfo) error {
	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)
			retVal, err := getEnv(info.Alt, dv)
			if err != nil {
				return err
			}
			flag.StringVar(p, info.Name, retVal, info.Info)
		case "bool":
			var dv bool

			if info.DefaultValue != nil {
				dv = info.DefaultValue.(bool)
			}
			p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*bool)
			retVal, err := getEnv(info.Alt, dv)
			if err != nil {
				return err
			}
			flag.BoolVar(p, info.Name, retVal, info.Info)
		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)
			retVal, err := getEnv(info.Alt, dv)
			if err != nil {
				return err
			}
			flag.IntVar(p, info.Name, retVal, info.Info)
		case "int64":
			var dv int64

			if info.DefaultValue != nil {
				dv = info.DefaultValue.(int64)
			}
			p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*int64)
			retVal, err := getEnv(info.Alt, dv)
			if err != nil {
				return err
			}
			flag.Int64Var(p, info.Name, retVal, info.Info)
		case "float64":
			var dv float64

			if info.DefaultValue != nil {
				dv = info.DefaultValue.(float64)
			}
			p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*float64)
			retVal, err := getEnv(info.Alt, dv)
			if err != nil {
				return err
			}
			flag.Float64Var(p, info.Name, retVal, info.Info)
		}
	}
	flag.Parse()
	return nil
}