package validate

import (
	"fmt"
	"math"
	"regexp"
	"strconv"
	"strings"

	"github.com/go-logr/logr"
	"github.com/minio/minio/pkg/wildcard"
	"github.com/nirmata/kyverno/pkg/engine/operator"
	apiresource "k8s.io/apimachinery/pkg/api/resource"
)

type quantity int

const (
	equal       quantity = 0
	lessThan    quantity = -1
	greaterThan quantity = 1
)

// ValidateValueWithPattern validates value with operators and wildcards
func ValidateValueWithPattern(log logr.Logger, value, pattern interface{}) bool {
	switch typedPattern := pattern.(type) {
	case bool:
		typedValue, ok := value.(bool)
		if !ok {
			log.V(4).Info("Expected type bool", "type", fmt.Sprintf("%T", value), "value", value)
			return false
		}
		return typedPattern == typedValue
	case int:
		return validateValueWithIntPattern(log, value, int64(typedPattern))
	case int64:
		return validateValueWithIntPattern(log, value, typedPattern)
	case float64:
		return validateValueWithFloatPattern(log, value, typedPattern)
	case string:
		return validateValueWithStringPatterns(log, value, typedPattern)
	case nil:
		return validateValueWithNilPattern(log, value)
	case map[string]interface{}:
		// TODO: check if this is ever called?
		return validateValueWithMapPattern(log, value, typedPattern)
	case []interface{}:
		// TODO: check if this is ever called?
		log.Info("arrays as patterns is not supported")
		return false
	default:
		log.Info("Unkown type", "type", fmt.Sprintf("%T", typedPattern), "value", typedPattern)
		return false
	}
}

func validateValueWithMapPattern(log logr.Logger, value interface{}, typedPattern map[string]interface{}) bool {
	// verify the type of the resource value is map[string]interface,
	// we only check for existence of object, not the equality of content and value
	//TODO: check if adding
	_, ok := value.(map[string]interface{})
	if !ok {
		log.Info("Expected type map[string]interface{}", "type", fmt.Sprintf("%T", value), "value", value)
		return false
	}
	return true
}

// Handler for int values during validation process
func validateValueWithIntPattern(log logr.Logger, value interface{}, pattern int64) bool {
	switch typedValue := value.(type) {
	case int:
		return int64(typedValue) == pattern
	case int64:
		return typedValue == pattern
	case float64:
		// check that float has no fraction
		if typedValue == math.Trunc(typedValue) {
			return int64(typedValue) == pattern
		}

		log.Info("Expected type int", "type", fmt.Sprintf("%T", typedValue), "value", typedValue)
		return false
	case string:
		// extract int64 from string
		int64Num, err := strconv.ParseInt(typedValue, 10, 64)
		if err != nil {
			log.Error(err, "Failed to parse int64 from string")
			return false
		}
		return int64Num == pattern
	default:
		log.Info("Expected type int", "type", fmt.Sprintf("%T", value), "value", value)
		return false
	}
}

// Handler for float values during validation process
func validateValueWithFloatPattern(log logr.Logger, value interface{}, pattern float64) bool {
	switch typedValue := value.(type) {
	case int:
		// check that float has no fraction
		if pattern == math.Trunc(pattern) {
			return int(pattern) == value
		}
		log.Info("Expected type float", "type", fmt.Sprintf("%T", typedValue), "value", typedValue)
		return false
	case int64:
		// check that float has no fraction
		if pattern == math.Trunc(pattern) {
			return int64(pattern) == value
		}
		log.Info("Expected type float", "type", fmt.Sprintf("%T", typedValue), "value", typedValue)
		return false
	case float64:
		return typedValue == pattern
	case string:
		// extract float64 from string
		float64Num, err := strconv.ParseFloat(typedValue, 64)
		if err != nil {
			log.Error(err, "Failed to parse float64 from string")
			return false
		}
		return float64Num == pattern
	default:
		log.Info("Expected type float", "type", fmt.Sprintf("%T", value), "value", value)
		return false
	}
}

// Handler for nil values during validation process
func validateValueWithNilPattern(log logr.Logger, value interface{}) bool {
	switch typed := value.(type) {
	case float64:
		return typed == 0.0
	case int:
		return typed == 0
	case int64:
		return typed == 0
	case string:
		return typed == ""
	case bool:
		return !typed
	case nil:
		return true
	case map[string]interface{}, []interface{}:
		log.Info("Maps and arrays could not be checked with nil pattern")
		return false
	default:
		log.Info("Unknown type as value when checking for nil pattern", "type", fmt.Sprintf("%T", value), "value", value)
		return false
	}
}

// Handler for pattern values during validation process
func validateValueWithStringPatterns(log logr.Logger, value interface{}, pattern string) bool {
	statements := strings.Split(pattern, "|")
	for _, statement := range statements {
		statement = strings.Trim(statement, " ")
		if validateValueWithStringPattern(log, value, statement) {
			return true
		}
	}

	return false
}

// Handler for single pattern value during validation process
// Detects if pattern has a number
func validateValueWithStringPattern(log logr.Logger, value interface{}, pattern string) bool {
	operator := operator.GetOperatorFromStringPattern(pattern)
	pattern = pattern[len(operator):]
	number, str := getNumberAndStringPartsFromPattern(pattern)

	if "" == number {
		return validateString(log, value, str, operator)
	}

	return validateNumberWithStr(log, value, pattern, operator)
}

// Handler for string values
func validateString(log logr.Logger, value interface{}, pattern string, operatorVariable operator.Operator) bool {
	if operator.NotEqual == operatorVariable || operator.Equal == operatorVariable {
		strValue, ok := value.(string)
		if !ok {
			log.Info("Expected type string", "type", fmt.Sprintf("%T", value), "value", value)
			return false
		}

		wildcardResult := wildcard.Match(pattern, strValue)

		if operator.NotEqual == operatorVariable {
			return !wildcardResult
		}

		return wildcardResult
	}
	log.Info("Operators >, >=, <, <= are not applicable to strings")
	return false
}

// validateNumberWithStr compares quantity if pattern type is quantity
//  or a wildcard match to pattern string
func validateNumberWithStr(log logr.Logger, value interface{}, pattern string, operator operator.Operator) bool {
	typedValue, err := convertToString(value)
	if err != nil {
		log.Error(err, "failed to convert to string")
		return false
	}

	patternQuan, err := apiresource.ParseQuantity(pattern)
	// 1. nil error - quantity comparison
	if err == nil {
		valueQuan, err := apiresource.ParseQuantity(typedValue)
		if err != nil {
			log.Error(err, "invalid quantity in resource", "type", fmt.Sprintf("%T", typedValue), "value", typedValue)
			return false
		}

		return compareQuantity(valueQuan, patternQuan, operator)
	}

	// 2. wildcard match
	if !wildcard.Match(pattern, typedValue) {
		log.Info("value failed wildcard check", "type", fmt.Sprintf("%T", typedValue), "value", typedValue, "check", pattern)
		return false
	}
	return true
}

func compareQuantity(value, pattern apiresource.Quantity, op operator.Operator) bool {
	result := value.Cmp(pattern)
	switch op {
	case operator.Equal:
		return result == int(equal)
	case operator.NotEqual:
		return result != int(equal)
	case operator.More:
		return result == int(greaterThan)
	case operator.Less:
		return result == int(lessThan)
	case operator.MoreEqual:
		return (result == int(equal)) || (result == int(greaterThan))
	case operator.LessEqual:
		return (result == int(equal)) || (result == int(lessThan))
	}

	return false
}

// detects numerical and string parts in pattern and returns them
func getNumberAndStringPartsFromPattern(pattern string) (number, str string) {
	regexpStr := `^(\d*(\.\d+)?)(.*)`
	re := regexp.MustCompile(regexpStr)
	matches := re.FindAllStringSubmatch(pattern, -1)
	match := matches[0]
	return match[1], match[3]
}
