/*
Copyright © LiquidWeb

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package validate

import (
	"fmt"
	"reflect"

	"github.com/spf13/cast"
)

func Validate(chk map[interface{}]interface{}) (err error) {
	defer func() {
		if paniced := recover(); paniced != nil {
			err = fmt.Errorf("%w %s", ValidationFailure, paniced)
		}
	}()

	for inputFieldValue, inputField := range chk {

		inputFieldVal := reflect.ValueOf(inputField)

		// by default, assume input field passed by user cannot be empty
		inputFieldOptional := false
		if inputFieldVal.Kind() == reflect.Map {
			iface := inputFieldVal.Interface()
			inputFieldType := iface.(map[string]string)["type"]
			if iface.(map[string]string)["optional"] == "true" {
				inputFieldOptional = true
			}
			inputFieldVal = reflect.ValueOf(inputFieldType)
		}

		inputFieldStr := cast.ToString(inputFieldVal)

		// inputField must be defined in InputTypes struct
		defined, shouldBeType, fieldVal := inputTypeDefined(inputFieldStr)
		if !defined {
			err = fmt.Errorf("%w for input field [%+v] type [%s] is not valid", ValidationFailure,
				inputFieldValue, inputFieldStr)
			return
		}

		// inputFieldValue must be of the correct type
		reflectValue := reflect.TypeOf(inputFieldValue).Name()
		if reflectValue != shouldBeType {
			err = fmt.Errorf("%w for input field [%+v] type [%s] has an invalid type of [%s] wanted [%s]",
				ValidationFailure, inputFieldValue, inputFieldStr, reflectValue, shouldBeType)
			return
		}

		// if the input field wasn't passed, and allow optional is true, continue
		if inputFieldOptional {
			// if inputFieldValue is a zero value, return without error
			inputFieldValueVal := reflect.ValueOf(inputFieldValue)

			if inputFieldStr == "NonEmptyString" {
				// since we check by going by the zero value for the type if the input field was passed,
				// we can't enforce the string type to not be empty optionally for NonEmptyString. Since
				// we can't differentiate between not passed and its zero value.
				err = fmt.Errorf("NonEmptyString input fields cannot be optional")
				return
			}

			if inputFieldValueVal.IsZero() {
				continue
			}
		}

		// if there's a Validate method call it
		iface := fieldVal.Interface()
		if interfaceHasMethod(iface, "Validate") {
			if validateErr := interfaceInputTypeValidate(iface, inputFieldValue); validateErr != nil {
				err = fmt.Errorf("%w for input field [%+v] %s", ValidationFailure, inputFieldValue,
					validateErr)
				return
			}
		}

	}

	return
}

func interfaceInputTypeValidate(iface, inputFieldValue interface{}) error {
	switch iface.(type) {
	case InputTypeUniqId:
		var obj InputTypeUniqId
		obj.UniqId = cast.ToString(inputFieldValue)
		if err := obj.Validate(); err != nil {
			return err
		}
	case InputTypeIP:
		var obj InputTypeIP
		obj.IP = cast.ToString(inputFieldValue)
		if err := obj.Validate(); err != nil {
			return err
		}
	case InputTypeCIDR:
		var obj InputTypeCIDR
		obj.CIDR = cast.ToString(inputFieldValue)
		if err := obj.Validate(); err != nil {
			return err
		}
	case InputTypeIpOrCidr:
		var obj InputTypeIpOrCidr
		obj.IpOrCidr = cast.ToString(inputFieldValue)
		if err := obj.Validate(); err != nil {
			return err
		}
	case InputTypePositiveInt64:
		var obj InputTypePositiveInt64
		obj.PositiveInt64 = cast.ToInt64(inputFieldValue)
		if err := obj.Validate(); err != nil {
			return err
		}
	case InputTypePositiveInt:
		var obj InputTypePositiveInt
		obj.PositiveInt = cast.ToInt(inputFieldValue)
		if err := obj.Validate(); err != nil {
			return err
		}
	case InputTypeNonEmptyString:
		var obj InputTypeNonEmptyString
		obj.NonEmptyString = cast.ToString(inputFieldValue)
		if err := obj.Validate(); err != nil {
			return err
		}
	case InputTypeLoadBalancerStrategyString:
		var obj InputTypeLoadBalancerStrategyString
		obj.LoadBalancerStrategy = cast.ToString(inputFieldValue)
		if err := obj.Validate(); err != nil {
			return err
		}
	case InputTypeHttpsLiquidwebUrl:
		var obj InputTypeHttpsLiquidwebUrl
		obj.HttpsLiquidwebUrl = cast.ToString(inputFieldValue)
		if err := obj.Validate(); err != nil {
			return err
		}
	case InputTypeNetworkPortPair:
		var obj InputTypeNetworkPortPair
		obj.NetworkPortPair = cast.ToString(inputFieldValue)
		if err := obj.Validate(); err != nil {
			return err
		}
	case InputTypeNetworkPort:
		var obj InputTypeNetworkPort
		obj.NetworkPort = cast.ToInt(inputFieldValue)
		if err := obj.Validate(); err != nil {
			return err
		}
	case InputTypeLoadBalancerHealthCheckProtocol:
		var obj InputTypeLoadBalancerHealthCheckProtocol
		obj.LoadBalancerHealthCheckProtocol = cast.ToString(inputFieldValue)
		if err := obj.Validate(); err != nil {
			return err
		}
	case InputTypeLoadBalancerHttpCodeRange:
		var obj InputTypeLoadBalancerHttpCodeRange
		obj.LoadBalancerHttpCodeRange = cast.ToString(inputFieldValue)
		if err := obj.Validate(); err != nil {
			return err
		}
	default:
		return fmt.Errorf("bug: validation missing entry for %s", inputFieldValue)
	}

	return nil
}

func interfaceHasMethod(iface interface{}, methodName string) bool {
	ifaceVal := reflect.ValueOf(iface)

	if !ifaceVal.IsValid() {
		// not valid, so we already know its false
		return false
	}

	if ifaceVal.Type().Kind() != reflect.Ptr {
		ifaceVal = reflect.New(reflect.TypeOf(iface))
	}

	method := ifaceVal.MethodByName(methodName)

	if method.IsValid() {
		return true
	}

	return false
}

func inputTypeDefined(inputType string) (bool, string, reflect.Value) {
	var validTypes InputTypes

	err, fieldType, fieldVal := structHasField(validTypes, inputType)
	if err != nil {
		return false, fieldType, fieldVal
	}

	return true, fieldType, fieldVal
}

func structHasField(data interface{}, fieldName string) (error, string, reflect.Value) {
	dataVal := reflect.ValueOf(data)

	if !dataVal.IsValid() {
		return fmt.Errorf("failed fetching value for fieldName [%s]", fieldName), "",
			reflect.Value{}
	}

	if dataVal.Type().Kind() != reflect.Ptr {
		dataVal = reflect.New(reflect.TypeOf(data))
	}

	fieldVal := dataVal.Elem().FieldByName(fieldName)
	if !fieldVal.IsValid() {
		return fmt.Errorf("[%s] has no field [%s]", dataVal.Type(), fieldName), "", fieldVal
	}

	fieldValKindStr := fieldVal.Kind().String()

	if fieldValKindStr == "struct" {
		fieldValKindStr = fieldVal.Field(0).Kind().String()
	}

	return nil, fieldValKindStr, fieldVal
}
