// Package configstruct parses unstructured maps into structures
package configstruct

import (
	"encoding/csv"
	"errors"
	"fmt"
	"reflect"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/rclone/rclone/fs/config/configmap"
)

var matchUpper = regexp.MustCompile("([A-Z]+)")

// camelToSnake converts CamelCase to snake_case
func camelToSnake(in string) string {
	out := matchUpper.ReplaceAllString(in, "_$1")
	out = strings.ToLower(out)
	out = strings.Trim(out, "_")
	return out
}

// StringToInterface turns in into an interface{} the same type as def
//
// This supports a subset of builtin types, string, integer types,
// bool, time.Duration and []string.
//
// Builtin types are expected to be encoding as their natural
// stringificatons as produced by fmt.Sprint except for []string which
// is expected to be encoded a a CSV with empty array encoded as "".
//
// Any other types are expected to be encoded by their String()
// methods and decoded by their `Set(s string) error` methods.
func StringToInterface(def any, in string) (newValue any, err error) {
	typ := reflect.TypeOf(def)
	o := reflect.New(typ)
	switch def.(type) {
	case string:
		// return strings unmodified
		newValue = in
	case int, int8, int16, int32, int64,
		uint, uint8, uint16, uint32, uint64, uintptr,
		float32, float64:
		// As per Rob Pike's advice in https://github.com/golang/go/issues/43306
		// we only use Sscan for numbers
		var n int
		n, err = fmt.Sscanln(in, o.Interface())
		if err == nil && n != 1 {
			err = errors.New("no items parsed")
		}
		newValue = o.Elem().Interface()
	case bool:
		newValue, err = strconv.ParseBool(in)
	case time.Duration:
		newValue, err = time.ParseDuration(in)
	case []string:
		// CSV decode arrays of strings - ideally we would use
		// fs.CommaSepList here but we can't as it would cause
		// a circular import.
		if len(in) == 0 {
			newValue = []string{}
		} else {
			r := csv.NewReader(strings.NewReader(in))
			newValue, err = r.Read()
			switch _err := err.(type) {
			case *csv.ParseError:
				err = _err.Err // remove line numbers from the error message
			}
		}
	default:
		// Try using a Set method
		if do, ok := o.Interface().(interface{ Set(s string) error }); ok {
			err = do.Set(in)
		} else {
			err = errors.New("don't know how to parse this type")
		}
		newValue = o.Elem().Interface()
	}
	if err != nil {
		return nil, fmt.Errorf("parsing %q as %T failed: %w", in, def, err)
	}
	return newValue, nil
}

// InterfaceToString turns in into a string
//
// This supports a subset of builtin types, string, integer types,
// bool, time.Duration and []string.
//
// Builtin types are expected to be encoding as their natural
// stringificatons as produced by fmt.Sprint except for []string which
// is expected to be encoded a a CSV with empty array encoded as "".
//
// Any other types are expected to be encoded by their String()
// methods and decoded by their `Set(s string) error` methods.
func InterfaceToString(in any) (strValue string, err error) {
	switch x := in.(type) {
	case string:
		// return strings unmodified
		strValue = x
	case int, int8, int16, int32, int64,
		uint, uint8, uint16, uint32, uint64, uintptr,
		float32, float64:
		strValue = fmt.Sprint(in)
	case bool:
		strValue = fmt.Sprint(in)
	case time.Duration:
		strValue = fmt.Sprint(in)
	case []string:
		// CSV encode arrays of strings - ideally we would use
		// fs.CommaSepList here but we can't as it would cause
		// a circular import.
		if len(x) == 0 {
			strValue = ""
		} else if len(x) == 1 && len(x[0]) == 0 {
			strValue = `""`
		} else {
			var buf strings.Builder
			w := csv.NewWriter(&buf)
			err := w.Write(x)
			if err != nil {
				return "", err
			}
			w.Flush()
			strValue = strings.TrimSpace(buf.String())
		}
	default:
		// Try using a String method
		if do, ok := in.(fmt.Stringer); ok {
			strValue = do.String()
		} else {
			err = errors.New("don't know how to convert this")
		}
	}
	if err != nil {
		return "", fmt.Errorf("interpreting %T as string failed: %w", in, err)
	}
	return strValue, nil
}

// Item describes a single entry in the options structure
type Item struct {
	Name  string    // snake_case
	Field string    // CamelCase
	Set   func(any) // set this field
	Value any
}

// Items parses the opt struct and returns a slice of Item objects.
//
// opt must be a pointer to a struct.  The struct should have entirely
// public fields.
//
// The config_name is looked up in a struct tag called "config" or if
// not found is the field name converted from CamelCase to snake_case.
//
// Nested structs are looked up too. If the parent struct has a struct
// tag, this will be used as a prefix for the values in the sub
// struct, otherwise they will be embedded as they are.
func Items(opt any) (items []Item, err error) {
	def := reflect.ValueOf(opt)
	if def.Kind() != reflect.Ptr {
		return nil, errors.New("argument must be a pointer")
	}
	def = def.Elem() // indirect the pointer
	if def.Kind() != reflect.Struct {
		return nil, errors.New("argument must be a pointer to a struct")
	}
	defType := def.Type()
	for i := range def.NumField() {
		field := def.Field(i)
		fieldType := defType.Field(i)
		fieldName := fieldType.Name
		configName, hasTag := fieldType.Tag.Lookup("config")
		if hasTag && configName == "-" {
			// Skip items with config:"-"
			continue
		}
		if !hasTag {
			configName = camelToSnake(fieldName)
		}
		valuePtr := field.Addr().Interface()                   // pointer to the value as an interface
		_, canSet := valuePtr.(interface{ Set(string) error }) // can we set this with the Option Set protocol
		// If we have a nested struct that isn't a config item then recurse
		if fieldType.Type.Kind() == reflect.Struct && !canSet {
			newItems, err := Items(valuePtr)
			if err != nil {
				return nil, fmt.Errorf("error parsing field %q: %w", fieldName, err)
			}
			for _, newItem := range newItems {
				if hasTag {
					newItem.Name = configName + "_" + newItem.Name
				}
				newItem.Field = fieldName + "." + newItem.Field
				items = append(items, newItem)
			}
		} else {
			defaultItem := Item{
				Name:  configName,
				Field: fieldName,
				Set: func(newValue any) {
					field.Set(reflect.ValueOf(newValue))
				},
				Value: field.Interface(),
			}
			items = append(items, defaultItem)
		}
	}
	return items, nil
}

// setValue sets newValue to configValue returning an updated newValue
func setValue(newValue any, configValue string) (any, error) {
	newNewValue, err := StringToInterface(newValue, configValue)
	if err != nil {
		// Mask errors if setting an empty string as
		// it isn't valid for all types.  This makes
		// empty string be the equivalent of unset.
		if configValue != "" {
			return nil, err
		}
	} else {
		newValue = newNewValue
	}
	return newValue, nil
}

// Set interprets the field names in defaults and looks up config
// values in the config passed in.  Any values found in config will be
// set in the opt structure.
//
// opt must be a pointer to a struct.  The struct should have entirely
// public fields.  The field names are converted from CamelCase to
// snake_case and looked up in the config supplied or a
// `config:"field_name"` is looked up.
//
// If items are found then they are converted from string to native
// types and set in opt.
//
// All the field types in the struct must implement fmt.Scanner.
func Set(config configmap.Getter, opt any) (err error) {
	defaultItems, err := Items(opt)
	if err != nil {
		return err
	}
	for _, defaultItem := range defaultItems {
		newValue := defaultItem.Value
		if configValue, ok := config.Get(defaultItem.Name); ok {
			newValue, err = setValue(newValue, configValue)
			if err != nil {
				return fmt.Errorf("couldn't parse config item %q = %q as %T: %w", defaultItem.Name, configValue, defaultItem.Value, err)
			}
		}
		defaultItem.Set(newValue)
	}
	return nil
}

// setIfSameType set aPtr with b if they are the same type or returns false.
func setIfSameType(aPtr interface{}, b interface{}) bool {
	aVal := reflect.ValueOf(aPtr).Elem()
	bVal := reflect.ValueOf(b)

	if aVal.Type() != bVal.Type() {
		return false
	}
	aVal.Set(bVal)
	return true
}

// SetAny interprets the field names in defaults and looks up config
// values in the config passed in.  Any values found in config will be
// set in the opt structure.
//
// opt must be a pointer to a struct.  The struct should have entirely
// public fields.  The field names are converted from CamelCase to
// snake_case and looked up in the config supplied or a
// `config:"field_name"` is looked up.
//
// If items are found then they are set directly if the correct type,
// otherwise they are converted to string and then converted from
// string to native types and set in opt.
//
// All the field types in the struct must implement fmt.Scanner.
func SetAny(config map[string]any, opt any) (err error) {
	defaultItems, err := Items(opt)
	if err != nil {
		return err
	}
	for _, defaultItem := range defaultItems {
		newValue := defaultItem.Value
		if configValue, ok := config[defaultItem.Name]; ok {
			if !setIfSameType(&newValue, configValue) {
				// Convert the config value to be a string
				stringConfigValue, err := InterfaceToString(configValue)
				if err != nil {
					return err
				}
				newValue, err = setValue(newValue, stringConfigValue)
				if err != nil {
					return fmt.Errorf("couldn't parse config item %q = %q as %T: %w", defaultItem.Name, stringConfigValue, defaultItem.Value, err)
				}
			}
		}
		defaultItem.Set(newValue)
	}
	return nil
}
