package config

import (
	"encoding"
	"flag"
	"fmt"
	"io"
	"os"
	"reflect"
	"strconv"
	"strings"

	"code.justin.tv/release/libforerunner/pkg/interchange"
)

type parseFun func() error

func (c *Config) setStruct(loc interface{}, entries interchange.EntrySet) error {
	val, err := getStruct(loc)
	if err != nil {
		return err
	}

	t := reflect.TypeOf(val.Interface())
	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)

		flagName := getName(field)

		err := c.set(loc, field.Name, flagName, entries)
		if err != nil {
			return err
		}
	}

	return nil
}

func (c *Config) parseStruct(loc interface{}) error {
	val, err := getStruct(loc)
	if err != nil {
		return err
	}

	t := reflect.TypeOf(val.Interface())
	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)

		flagName := getName(field)
		// Save the default value
		c.flagVars[field.Name] = &flagValue{Name: flagName, Def: val.Field(i).Interface()}
		flag.Var(c.flagVars[field.Name], flagName, getDescription(field))
		c.currentConfig[field.Name] = &interchange.ConfigEntry{
			Value:  val.Field(i).Interface(),
			Source: interchange.Default,
			Secret: secretField(field),
		}
	}

	return nil
}

// getName will search the struct tags for a forerunner name. If it is not
// found it will use a modified version of the struct field name.
// It will convert camel case to hyphen separated.
//
// Example:
//
// FooBar => foo-bar
// Baz => baz
func getName(field reflect.StructField) string {
	frTag := field.Tag.Get("forerunner")

	tagVars := strings.Split(frTag, ",")

	if len(tagVars) > 0 && tagVars[0] != "" {
		return tagVars[0]
	}

	return ConvertGoName(field.Name)
}

// secretField returns true if the field is marked with the secret flag.
func secretField(field reflect.StructField) bool {
	frTag := field.Tag.Get("forerunner")

	tagVars := strings.Split(frTag, ",")

	for _, v := range tagVars {
		if v == "secret" {
			return true
		}
	}

	return false
}

func getDescription(field reflect.StructField) string {
	return field.Tag.Get("description")
}

func (c *Config) set(loc interface{}, fieldName, flagName string, entries interchange.EntrySet) error {
	val, err := getStruct(loc)
	if err != nil {
		return err
	}

	field := val.FieldByName(fieldName)
	ptr := reflect.New(field.Type()).Interface()

	fv, ok := c.flagVars[fieldName]
	if !ok {
		return fmt.Errorf("You somehow managed to get libforerunner to attempt to load an unknown struct field: %q", fieldName)
	}

	var source interchange.Source
	// Try and use the version set by flag:
	if fv.IsSet {
		source = interchange.Flags
		if err := unmarshal(fv.Val, ptr); err != nil {
			return fmt.Errorf("Error parsing flag %q: %v", fv.Name, err)
		}
	} else if envVar := os.Getenv(ConvertFlagToEnvName(flagName)); envVar != "" {
		source = interchange.ENV
		if err = unmarshal(envVar, ptr); err != nil {
			return fmt.Errorf("Error parsing flag %q: %v", envVar, err)
		}
	} else if configEntry, ok := entries[flagName]; ok {
		source = configEntry.Source
		if err = unmarshal(configEntry.Value, ptr); err != nil {
			return fmt.Errorf("Error parsing config %q from %q: %v", fieldName, configEntry.Source, err)
		}
	} else {
		source = interchange.Default
		// Use default value:
		reflect.Indirect(reflect.ValueOf(ptr)).Set(reflect.ValueOf(fv.Def))
	}

	field.Set(reflect.Indirect(reflect.ValueOf(ptr)))
	c.fields = append(c.fields, fieldName)
	c.currentConfig[fieldName].Value = ptr
	c.currentConfig[fieldName].Source = source
	return nil
}

// getStruct does two things. First it verifies that type (interface{}) is the
// type we really want, that is a pointer to a struct. Second it returns the
// reflect.Value of the struct itself so we can work on it.
func getStruct(loc interface{}) (reflect.Value, error) {
	if loc == nil {
		return reflect.Value{}, fmt.Errorf("interface is nil")
	}

	// We need to make sure it is a pointer that to a struct that way we can
	// lookup information like the name and defaults.
	if k := reflect.TypeOf(loc).Kind(); k != reflect.Ptr {
		return reflect.Value{}, fmt.Errorf("LibForerunner can't load config into a type %q, want ptr; must be a pointer to a struct.", k)
	}

	val := reflect.Indirect(reflect.ValueOf(loc))
	if k := val.Kind(); k != reflect.Struct {
		return reflect.Value{}, fmt.Errorf("LibForerunner can't load config into a type %q, want struct; must be a pointer to a struct.", k)
	}

	return val, nil
}

// unmarshal takes unknown data and piggybacking on top of encoding/json sticks
// it the field of the user's struct. The real work is
func unmarshal(data interface{}, ptr interface{}) error {
	// Validate input:
	if reflect.ValueOf(ptr).Kind() != reflect.Ptr {
		return fmt.Errorf("ptr is %T; want Ptr", ptr)
	}

	// Lookup the reflect.Value for the memory location that ptr is pointing
	// at.
	ptrVal := reflect.Indirect(reflect.ValueOf(ptr))

	// If they are already the same type we win!
	if reflect.TypeOf(data).Kind() == ptrVal.Kind() {
		ptrVal.Set(reflect.ValueOf(data))
		return nil
	}

	// To proceed we can only deal with strings:
	if reflect.TypeOf(data).Kind() != reflect.String {
		return fmt.Errorf("Can't unmarshal non-strings, got: %T", data)
	}
	sData := data.(string)

	k := ptrVal.Kind()
	switch k {
	case reflect.Uint:
		v, err := strconv.ParseUint(sData, 10, 0)
		if err != nil {
			return err
		}

		ptrVal.SetUint(v)
	case reflect.Int:
		v, err := strconv.ParseInt(sData, 10, 0)
		if err != nil {
			return err
		}

		ptrVal.SetInt(v)
	case reflect.Float32:
	case reflect.Float64:
		v, err := strconv.ParseFloat(sData, 0)
		if err != nil {
			return err
		}

		ptrVal.SetFloat(v)
	case reflect.Bool:
		v, err := strconv.ParseBool(sData)
		if err != nil {
			return err
		}

		ptrVal.SetBool(v)
	default:
		// Interfaces are often on the pointer itself not the value:
		if tx, ok := ptr.(encoding.TextUnmarshaler); ok {
			return tx.UnmarshalText([]byte(sData))
		}

		return fmt.Errorf("Don't know how to handle %T", ptrVal.Interface())
	}
	return nil
}

func (c *Config) write(f io.Writer) error {
	var val interface{}
	secret := "**********"

	// In order to maintain printing order with the struct we save the order
	// and use that to determine printing order.
	for _, name := range c.fields {
		v := c.currentConfig[name]
		val = v.Value
		if v.Secret {
			val = &secret
		}

		fmt.Printf("%s [%s]: %v\n", name, v.Source, reflect.Indirect(reflect.ValueOf(val)).Interface())
	}

	return nil
}
