// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.

package gvalid

import (
	"github.com/gogf/gf/errors/gcode"
	"github.com/gogf/gf/internal/structs"
	"github.com/gogf/gf/util/gconv"
	"github.com/gogf/gf/util/gutil"
	"strings"
)

// CheckStruct validates struct and returns the error result.
// The parameter `object` should be type of struct/*struct.
func (v *Validator) CheckStruct(object interface{}) Error {
	return v.doCheckStruct(object)
}

func (v *Validator) doCheckStruct(object interface{}) Error {
	var (
		errorMaps           = make(map[string]map[string]string) // Returning error.
		fieldToAliasNameMap = make(map[string]string)            // Field name to alias name map.
	)
	fieldMap, err := structs.FieldMap(structs.FieldMapInput{
		Pointer:          object,
		PriorityTagArray: aliasNameTagPriority,
		RecursiveOption:  structs.RecursiveOptionEmbedded,
	})
	if err != nil {
		return newErrorStr(internalObjectErrRuleName, err.Error())
	}
	// It checks the struct recursively the its attribute is an embedded struct.
	for _, field := range fieldMap {
		if field.IsEmbedded() {
			// No validation interface implements check.
			if _, ok := field.Value.Interface().(apiNoValidation); ok {
				continue
			}
			if _, ok := field.TagLookup(noValidationTagName); ok {
				continue
			}
			if err := v.doCheckStruct(field.Value); err != nil {
				// It merges the errors into single error map.
				for k, m := range err.(*validationError).errors {
					errorMaps[k] = m
				}
			}
		} else {
			if field.TagValue != "" {
				fieldToAliasNameMap[field.Name()] = field.TagValue
			}
		}
	}
	// It here must use structs.TagFields not structs.FieldMap to ensure error sequence.
	tagField, err := structs.TagFields(object, structTagPriority)
	if err != nil {
		return newErrorStr(internalObjectErrRuleName, err.Error())
	}
	// If there's no struct tag and validation rules, it does nothing and returns quickly.
	if len(tagField) == 0 && v.messages == nil {
		return nil
	}

	var (
		inputParamMap  map[string]interface{}
		checkRules     = make([]fieldRule, 0)
		nameToRuleMap  = make(map[string]string) // just for internally searching index purpose.
		customMessage  = make(CustomMsg)         // Custom rule error message map.
		checkValueData = v.data                  // Ready to be validated data, which can be type of .
	)
	if checkValueData == nil {
		checkValueData = object
	}
	switch assertValue := v.rules.(type) {
	// Sequence tag: []sequence tag
	// Sequence has order for error results.
	case []string:
		for _, tag := range assertValue {
			name, rule, msg := parseSequenceTag(tag)
			if len(name) == 0 {
				continue
			}
			if len(msg) > 0 {
				var (
					msgArray  = strings.Split(msg, "|")
					ruleArray = strings.Split(rule, "|")
				)
				for k, v := range ruleArray {
					// If length of custom messages is lesser than length of rules,
					// the rest rules use the default error messages.
					if len(msgArray) <= k {
						continue
					}
					if len(msgArray[k]) == 0 {
						continue
					}
					array := strings.Split(v, ":")
					if _, ok := customMessage[name]; !ok {
						customMessage[name] = make(map[string]string)
					}
					customMessage[name].(map[string]string)[strings.TrimSpace(array[0])] = strings.TrimSpace(msgArray[k])
				}
			}
			nameToRuleMap[name] = rule
			checkRules = append(checkRules, fieldRule{
				Name: name,
				Rule: rule,
			})
		}

	// Map type rules does not support sequence.
	// Format: map[key]rule
	case map[string]string:
		nameToRuleMap = assertValue
		for name, rule := range assertValue {
			checkRules = append(checkRules, fieldRule{
				Name: name,
				Rule: rule,
			})
		}
	}
	// If there's no struct tag and validation rules, it does nothing and returns quickly.
	if len(tagField) == 0 && len(checkRules) == 0 {
		return nil
	}
	// Input parameter map handling.
	if v.data == nil || !v.useDataInsteadOfObjectAttributes {
		inputParamMap = make(map[string]interface{})
	} else {
		inputParamMap = gconv.Map(v.data)
	}
	// Checks and extends the parameters map with struct alias tag.
	if !v.useDataInsteadOfObjectAttributes {
		for nameOrTag, field := range fieldMap {
			inputParamMap[nameOrTag] = field.Value.Interface()
			if nameOrTag != field.Name() {
				inputParamMap[field.Name()] = field.Value.Interface()
			}
		}
	}

	// Merge the custom validation rules with rules in struct tag.
	// The custom rules has the most high priority that can overwrite the struct tag rules.
	for _, field := range tagField {
		var (
			fieldName       = field.Name()                     // Attribute name.
			name, rule, msg = parseSequenceTag(field.TagValue) // The `name` is different from `attribute alias`, which is used for validation only.
		)
		if len(name) == 0 {
			if v, ok := fieldToAliasNameMap[fieldName]; ok {
				// It uses alias name of the attribute if its alias name tag exists.
				name = v
			} else {
				// It or else uses the attribute name directly.
				name = fieldName
			}
		} else {
			// It uses the alias name from validation rule.
			fieldToAliasNameMap[fieldName] = name
		}
		// It here extends the params map using alias names.
		// Note that the variable `name` might be alias name or attribute name.
		if _, ok := inputParamMap[name]; !ok {
			if !v.useDataInsteadOfObjectAttributes {
				inputParamMap[name] = field.Value.Interface()
			} else {
				if name != fieldName {
					if foundKey, foundValue := gutil.MapPossibleItemByKey(inputParamMap, fieldName); foundKey != "" {
						inputParamMap[name] = foundValue
					}
				}
			}
		}

		if _, ok := nameToRuleMap[name]; !ok {
			if _, ok := nameToRuleMap[fieldName]; ok {
				// If there's alias name,
				// use alias name as its key and remove the field name key.
				nameToRuleMap[name] = nameToRuleMap[fieldName]
				delete(nameToRuleMap, fieldName)
				for index, checkRuleItem := range checkRules {
					if fieldName == checkRuleItem.Name {
						checkRuleItem.Name = name
						checkRules[index] = checkRuleItem
						break
					}
				}
			} else {
				nameToRuleMap[name] = rule
				checkRules = append(checkRules, fieldRule{
					Name: name,
					Rule: rule,
				})
			}
		} else {
			// The input rules can overwrite the rules in struct tag.
			continue
		}

		if len(msg) > 0 {
			var (
				msgArray  = strings.Split(msg, "|")
				ruleArray = strings.Split(rule, "|")
			)
			for k, v := range ruleArray {
				// If length of custom messages is lesser than length of rules,
				// the rest rules use the default error messages.
				if len(msgArray) <= k {
					continue
				}
				if len(msgArray[k]) == 0 {
					continue
				}
				array := strings.Split(v, ":")
				if _, ok := customMessage[name]; !ok {
					customMessage[name] = make(map[string]string)
				}
				customMessage[name].(map[string]string)[strings.TrimSpace(array[0])] = strings.TrimSpace(msgArray[k])
			}
		}
	}

	// Custom error messages,
	// which have the most priority than `rules` and struct tag.
	if msg, ok := v.messages.(CustomMsg); ok && len(msg) > 0 {
		for k, v := range msg {
			if a, ok := fieldToAliasNameMap[k]; ok {
				// Overwrite the key of field name.
				customMessage[a] = v
			} else {
				customMessage[k] = v
			}
		}
	}

	// The following logic is the same as some of CheckMap but with sequence support.
	var (
		value interface{}
	)
	for _, checkRuleItem := range checkRules {
		_, value = gutil.MapPossibleItemByKey(inputParamMap, checkRuleItem.Name)
		// It checks each rule and its value in loop.
		if validatedError := v.doCheckValue(doCheckValueInput{
			Name:     checkRuleItem.Name,
			Value:    value,
			Rule:     checkRuleItem.Rule,
			Messages: customMessage[checkRuleItem.Name],
			DataRaw:  checkValueData,
			DataMap:  inputParamMap,
		}); validatedError != nil {
			_, errorItem := validatedError.FirstItem()
			// ===================================================================
			// Only in map and struct validations, if value is nil or empty string
			// and has no required* rules, it clears the error message.
			// ===================================================================
			if value == nil || gconv.String(value) == "" {
				required := false
				// rule => error
				for ruleKey := range errorItem {
					// Default required rules.
					if _, ok := mustCheckRulesEvenValueEmpty[ruleKey]; ok {
						required = true
						break
					}
					// Custom rules are also required in default.
					if f := v.getRuleFunc(ruleKey); f != nil {
						required = true
						break
					}
				}
				if !required {
					continue
				}
			}
			if _, ok := errorMaps[checkRuleItem.Name]; !ok {
				errorMaps[checkRuleItem.Name] = make(map[string]string)
			}
			for ruleKey, errorItemMsgMap := range errorItem {
				errorMaps[checkRuleItem.Name][ruleKey] = errorItemMsgMap
			}
			if v.bail {
				break
			}
		}
	}
	if len(errorMaps) > 0 {
		return newError(gcode.CodeValidationFailed, checkRules, errorMaps)
	}
	return nil
}
