package cacheman

import (
	"errors"
	"fmt"
	"reflect"
	"strconv"
	"strings"
)

// ScanToStruct takes a []interface{} result from a redis.SliceCmd{} result
// and scans the keys and values from the result into a given struct by its
// field tags.
//
// Example:
// Scan a {name, age} map from an HGETALL call to a struct.
// type Test struct {
// 	Name string `redis:"name"`
// 	Age  int `redis:"age"`
// }

// cmd := redis.NewSliceCmd(p.c.ctx, "HGETALL", "test")
// client.Process(ctx, cmd)

// var out Test
// ScanToStruct(cmd.Val(), &out, "redis")
// fmt.Println(out)
//
func ScanToStruct(vals []interface{}, obj interface{}, fieldTag string) error {
	if len(vals)%2 != 0 {
		return errors.New("args should have an even number of items (key-val)")
	}

	ob := reflect.ValueOf(obj)
	if ob.Kind() == reflect.Ptr {
		ob = ob.Elem()
	}

	if ob.Kind() != reflect.Struct {
		return fmt.Errorf("failed to decode form values to struct, received non struct type: %T", ob)
	}

	for i := 0; i < len(vals)/2; i++ {
		key, ok := vals[i*2].(string)
		if !ok {
			continue
		}

		val, ok := vals[(i*2)+1].(string)
		if !ok {
			continue
		}

		// Get the field from the struct with the matching key.
		f := getField(ob, key, fieldTag)
		if !f.IsValid() {
			continue
		}

		// Convert the string value from Redis and set it on the struct field.
		if _, err := setVal(f, val); err != nil {
			return fmt.Errorf("failed to decode `%v`, got: `%s` (%v)", key, val, err)
		}
	}

	return nil
}

func getField(ob reflect.Value, name, fieldTag string) reflect.Value {
	for i := 0; i < ob.NumField(); i++ {
		f := ob.Field(i)
		if f.IsValid() && f.CanSet() {
			tag := ob.Type().Field(i).Tag.Get(fieldTag)
			if tag == "" || tag == "-" {
				continue
			}

			tag = strings.Split(tag, ",")[0]
			if tag == name {
				return f
			}
		}
	}

	return reflect.Value{}
}

func setVal(f reflect.Value, val string) (bool, error) {
	switch f.Kind() {
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		v, err := strconv.ParseInt(val, 10, 0)
		if err != nil {
			return false, fmt.Errorf("expected int")
		}
		f.SetInt(v)

	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
		v, err := strconv.ParseUint(val, 10, 0)
		if err != nil {
			return false, fmt.Errorf("expected unsigned int")
		}
		f.SetUint(v)

	case reflect.Float32, reflect.Float64:
		v, err := strconv.ParseFloat(val, 0)
		if err != nil {
			return false, fmt.Errorf("expected decimal")
		}
		f.SetFloat(v)

	case reflect.String:
		f.SetString(val)

	case reflect.Slice:
		// []byte slice ([]uint8).
		if f.Type().Elem().Kind() == reflect.Uint8 {
			f.SetBytes([]byte(val))
		}

	case reflect.Bool:
		b, err := strconv.ParseBool(val)
		if err != nil {
			return false, fmt.Errorf("expected boolean")
		}
		f.SetBool(b)

	default:
		return false, nil
	}

	return true, nil
}