package struct_utils

import (
	"github.com/samber/lo"
	"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/errors"
	"reflect"
	"strings"
)

// KeyValuePair defines a key/value pair derived from form data
type KeyValuePair struct {
	Key   string `json:"key"`
	Value string `json:"value"`
}

// FormToKeyValuePairs returns a string-based map of strings as derived from posted form keys and values.
// e.g. oauth_consumer_key=mlhgs&oauth_consumer_secret=x240ar&oauth_verifier=b0qjbx&store_base_url=http%3A%2F%2Flocalhost.com%2Fstore
func FormToKeyValuePairs(body string) []KeyValuePair {
	out := []KeyValuePair{}
	parts := strings.Split(body, "&")
	for _, p := range parts {
		split := strings.Split(p, "=")

		var key string
		if len(split) > 0 {
			key = split[0]
		}

		var value string
		if len(split) > 1 {
			value = split[1]
		}

		kv := KeyValuePair{
			Key:   key,
			Value: value,
		}
		out = append(out, kv)
	}

	return out
}

// GetValue returns the value for the given key from a KeyValuePair slice.
func GetValue(key string, kv []KeyValuePair) string {
	for _, v := range kv {
		if v.Key == key {
			return v.Value
		}
	}

	return ""
}

// ValidateRequiredFields checks the required tag on struct fields and returns an error if any are set to zero.
func ValidateRequiredFields(object any, parentFieldKeys ...string) error {
	objectValue := reflect.ValueOf(object)

	if objectValue.Kind() == reflect.Interface {
		objectValue = objectValue.Elem()
	}

	if objectValue.Kind() == reflect.Ptr {
		objectValue = objectValue.Elem()
	}

	if objectValue.Kind() != reflect.Struct {
		if objectValue.IsZero() {
			errorKey := strings.Join(parentFieldKeys, ".")
			return errors.Error(errorKey + " is required")
		}
		return nil
	}

	for i := 0; i < objectValue.NumField(); i++ {
		field := objectValue.Field(i)
		fieldType := objectValue.Type().Field(i)
		requiredTag := fieldType.Tag.Get("required")
		requiredOptions := strings.Split(requiredTag, ",")
		required := lo.Contains(requiredOptions, "true")
		notNil := lo.Contains(requiredOptions, "not-nil")
		deep := lo.Contains(requiredOptions, "deep")

		thisFieldKey := getRequiredKey(fieldType)
		err := validateFieldByKind(field, required, notNil, deep, thisFieldKey, parentFieldKeys...)
		if err != nil {
			return err
		}
	}

	return nil
}

func validateFieldByKind(field reflect.Value, required bool, notNil bool, deep bool, thisFieldKey string, parentFieldKeys ...string) error {
	switch field.Kind() {
	case reflect.Struct:
		if required &&
			field.IsZero() {
			return errors.Error(getErrorKey(thisFieldKey, parentFieldKeys) + " is required")
		}

		if deep {
			parentFieldKeys = append(parentFieldKeys, thisFieldKey)
			err := ValidateRequiredFields(field.Interface(), parentFieldKeys...)
			if err != nil {
				return err
			}
		}
	case reflect.Slice:
		if (required || notNil) &&
			field.IsZero() {
			return errors.Error(getErrorKey(thisFieldKey, parentFieldKeys) + " is required")
		}

		if required &&
			field.Len() == 0 {
			return errors.Error(getErrorKey(thisFieldKey, parentFieldKeys) + " may not be empty")
		}

		if deep {
			for j := 0; j < field.Len(); j++ {
				sliceField := field.Index(j)
				if sliceField.Kind() == reflect.Ptr {
					sliceField = sliceField.Elem()
				}

				if sliceField.Kind() == reflect.Struct {
					err := ValidateRequiredFields(sliceField.Interface(), append(parentFieldKeys, thisFieldKey)...)
					if err != nil {
						return err
					}
				} else {
					err := validateFieldByKind(sliceField, true, notNil, deep, thisFieldKey, parentFieldKeys...)
					if err != nil {
						return err
					}
				}
			}
		}
	case reflect.Map:
		if (required || notNil) &&
			field.IsZero() {
			return errors.Error(getErrorKey(thisFieldKey, parentFieldKeys) + " is required")
		}

		if required &&
			field.Len() == 0 {
			return errors.Error(getErrorKey(thisFieldKey, parentFieldKeys) + " may not be empty")
		}

		if deep {
			for _, key := range field.MapKeys() {
				mapField := field.MapIndex(key)
				if mapField.Kind() == reflect.Ptr {
					mapField = mapField.Elem()
				}

				if mapField.Kind() == reflect.Struct {
					err := ValidateRequiredFields(mapField.Interface(), append(parentFieldKeys, thisFieldKey)...)
					if err != nil {
						return err
					}
				} else {
					err := validateFieldByKind(mapField, required, notNil, deep, thisFieldKey, parentFieldKeys...)
					if err != nil {
						return err
					}
				}
			}
		}
	case reflect.Ptr:
		if (required || notNil) &&
			field.IsZero() {
			return errors.Error(getErrorKey(thisFieldKey, parentFieldKeys) + " is required")
		}

		if field.Elem().Kind() == reflect.Struct {
			if required &&
				field.Elem().IsZero() {
				return errors.Error(getErrorKey(thisFieldKey, parentFieldKeys) + " is required")
			}

			if deep {
				parentFieldKeys = append(parentFieldKeys, thisFieldKey)
				err := ValidateRequiredFields(field.Interface(), parentFieldKeys...)
				if err != nil {
					return err
				}
			}
		} else if required {
			return validateFieldByKind(field.Elem(), required, notNil, deep, thisFieldKey, parentFieldKeys...)
		}
	default:
		if required &&
			field.IsZero() {
			return errors.Error(getErrorKey(thisFieldKey, parentFieldKeys) + " is required")
		}
	}

	return nil
}

func getRequiredKey(fieldType reflect.StructField) string {
	if fieldType.Anonymous {
		return ""
	}

	jsonTag := fieldType.Tag.Get("json")
	if jsonTag != "" && jsonTag != "-" {
		commaIdx := strings.Index(jsonTag, ",")
		if commaIdx > 0 {
			// Tag is in the format "key,omitempty"
			return jsonTag[:commaIdx]
		} else {
			return jsonTag
		}
	}

	return fieldType.Name
}

func getErrorKey(thisKey string, parentKeys []string) string {
	parentKeys = append(parentKeys, thisKey)
	parentKeys = lo.Filter(parentKeys, func(key string, _ int) bool {
		return key != ""
	})

	return strings.Join(parentKeys, ".")
}