package bexpr

import (
	"fmt"
	"regexp"
)

func validateRecurse(ast Expression, fields FieldConfigurations, maxRawValueLength int) (int, error) {
	switch node := ast.(type) {
	case *UnaryExpression:
		switch node.Operator {
		case UnaryOpNot:
			// this is fine
		default:
			return 0, fmt.Errorf("Invalid unary expression operator: %d", node.Operator)
		}

		if node.Operand == nil {
			return 0, fmt.Errorf("Invalid unary expression operand: nil")
		}
		return validateRecurse(node.Operand, fields, maxRawValueLength)
	case *BinaryExpression:
		switch node.Operator {
		case BinaryOpAnd, BinaryOpOr:
			// this is fine
		default:
			return 0, fmt.Errorf("Invalid binary expression operator: %d", node.Operator)
		}

		if node.Left == nil {
			return 0, fmt.Errorf("Invalid left hand side of binary expression: nil")
		} else if node.Right == nil {
			return 0, fmt.Errorf("Invalid right hand side of binary expression: nil")
		}

		leftMatches, err := validateRecurse(node.Left, fields, maxRawValueLength)
		if err != nil {
			return leftMatches, err
		}

		rightMatches, err := validateRecurse(node.Right, fields, maxRawValueLength)
		return leftMatches + rightMatches, err
	case *MatchExpression:
		if len(node.Selector) < 1 {
			return 1, fmt.Errorf("Invalid selector: %q", node.Selector)
		}

		if node.Value != nil && maxRawValueLength != 0 && len(node.Value.Raw) > maxRawValueLength {
			return 1, fmt.Errorf("Value in expression with length %d for selector %q exceeds maximum length of", len(node.Value.Raw), maxRawValueLength)
		}

		// exit early if we have no fields to check against
		if len(fields) < 1 {
			return 1, nil
		}

		configs := fields
		var lastConfig *FieldConfiguration
		// validate the selector
		for idx, field := range node.Selector {
			if fcfg, ok := configs[FieldName(field)]; ok {
				lastConfig = fcfg
				configs = fcfg.SubFields
			} else if fcfg, ok := configs[FieldNameAny]; ok {
				lastConfig = fcfg
				configs = fcfg.SubFields
			} else {
				return 1, fmt.Errorf("Selector %q is not valid", node.Selector[:idx+1])
			}

			// this just verifies that the FieldConfigurations we are using was created properly
			if lastConfig == nil {
				return 1, fmt.Errorf("FieldConfiguration for selector %q is nil", node.Selector[:idx])
			}
		}

		// check the operator
		found := false
		for _, op := range lastConfig.SupportedOperations {
			if op == node.Operator {
				found = true
				break
			}
		}

		if !found {
			return 1, fmt.Errorf("Invalid match operator %q for selector %q", node.Operator, node.Selector)
		}

		// coerce/validate the value
		if node.Value != nil {
			if lastConfig.CoerceFn != nil {
				coerced, err := lastConfig.CoerceFn(node.Value.Raw)
				if err != nil {
					return 1, fmt.Errorf("Failed to coerce value %q for selector %q: %v", node.Value.Raw, node.Selector, err)
				}

				node.Value.Converted = coerced
			}

			if node.Operator == MatchMatches || node.Operator == MatchNotMatches {
				var regRaw string
				if strVal, ok := node.Value.Converted.(string); ok {
					regRaw = strVal
				} else if node.Value.Converted == nil {
					regRaw = node.Value.Raw
				} else {
					return 1, fmt.Errorf("Match operator %q cannot be used with fields whose coercion functions return non string values", node.Operator)
				}

				re, err := regexp.Compile(regRaw)
				if err != nil {
					return 1, fmt.Errorf("Failed to compile regular expression %q: %v", regRaw, err)
				}

				node.Value.Converted = re
			}
		} else {
			switch node.Operator {
			case MatchIsEmpty, MatchIsNotEmpty:
				// these don't require values
			default:
				return 1, fmt.Errorf("Match operator %q requires a non-nil value", node.Operator)
			}
		}
		return 1, nil
	}
	return 0, fmt.Errorf("Cannot validate: Invalid AST")
}

func validate(ast Expression, fields FieldConfigurations, maxMatches, maxRawValueLength int) error {
	matches, err := validateRecurse(ast, fields, maxRawValueLength)
	if err != nil {
		return err
	}

	if maxMatches != 0 && matches > maxMatches {
		return fmt.Errorf("Number of match expressions (%d) exceeds the limit (%d)", matches, maxMatches)
	}

	return nil
}
