package variables

import (
	"fmt"

	"github.com/go-logr/logr"
	kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
	"github.com/kyverno/kyverno/pkg/engine/context"
	"github.com/kyverno/kyverno/pkg/engine/variables/operator"
	stringutils "github.com/kyverno/kyverno/pkg/utils/strings"
)

// Evaluate evaluates the condition
func Evaluate(logger logr.Logger, ctx context.EvalInterface, condition kyvernov1.Condition) (bool, string, error) {
	key, err := SubstituteAllInPreconditions(logger, ctx, condition.GetKey())
	if err != nil {
		return false, "", fmt.Errorf("failed to substitute variables in condition key: %w", err)
	}
	value, err := SubstituteAllInPreconditions(logger, ctx, condition.GetValue())
	if err != nil {
		return false, "", fmt.Errorf("failed to substitute variables in condition value: %w", err)
	}
	handler := operator.CreateOperatorHandler(logger, ctx, condition.Operator)
	if handler == nil {
		return false, "", fmt.Errorf("failed to create handler for condition operator: %w", err)
	}
	return handler.Evaluate(key, value), condition.Message, nil
}

// EvaluateConditions evaluates all the conditions present in a slice, in a backwards compatible way
func EvaluateConditions(log logr.Logger, ctx context.EvalInterface, conditions interface{}) (bool, string, error) {
	switch typedConditions := conditions.(type) {
	case kyvernov1.AnyAllConditions:
		return evaluateAnyAllConditions(log, ctx, typedConditions)
	case []kyvernov1.Condition: // backwards compatibility
		return evaluateOldConditions(log, ctx, typedConditions)
	}
	return false, "", fmt.Errorf("invalid condition")
}

func EvaluateAnyAllConditions(log logr.Logger, ctx context.EvalInterface, conditions []kyvernov1.AnyAllConditions) (bool, string, error) {
	var conditionTrueMessages []string
	for _, c := range conditions {
		if val, msg, err := evaluateAnyAllConditions(log, ctx, c); err != nil {
			return false, "", err
		} else if !val {
			return false, msg, nil
		} else {
			conditionTrueMessages = append(conditionTrueMessages, msg)
		}
	}

	return true, stringutils.JoinNonEmpty(conditionTrueMessages, ";"), nil
}

// evaluateAnyAllConditions evaluates multiple conditions as a logical AND (all) or OR (any) operation depending on the conditions
func evaluateAnyAllConditions(log logr.Logger, ctx context.EvalInterface, conditions kyvernov1.AnyAllConditions) (bool, string, error) {
	anyConditions, allConditions := conditions.AnyConditions, conditions.AllConditions
	anyConditionsResult, allConditionsResult := true, true
	var conditionFalseMessages []string
	var conditionTrueMessages []string

	// update the anyConditionsResult if they are present
	if anyConditions != nil {
		anyConditionsResult = false
		for _, condition := range anyConditions {
			if val, msg, err := Evaluate(log, ctx, condition); err != nil {
				return false, "", err
			} else if val {
				anyConditionsResult = true
				conditionTrueMessages = append(conditionTrueMessages, msg)
				break
			} else {
				conditionFalseMessages = append(conditionFalseMessages, msg)
			}
		}

		if !anyConditionsResult {
			log.V(3).Info("no condition passed for 'any' block", "any", anyConditions)
		}
	}

	// update the allConditionsResult if they are present
	for _, condition := range allConditions {
		if val, msg, err := Evaluate(log, ctx, condition); err != nil {
			return false, "", err
		} else if !val {
			allConditionsResult = false
			conditionFalseMessages = append(conditionFalseMessages, msg)
			log.V(3).Info("a condition failed in 'all' block", "condition", condition, "message", msg)
			break
		} else {
			conditionTrueMessages = append(conditionTrueMessages, msg)
		}
	}

	finalResult := anyConditionsResult && allConditionsResult
	if finalResult {
		return finalResult, stringutils.JoinNonEmpty(conditionTrueMessages, "; "), nil
	}

	return finalResult, stringutils.JoinNonEmpty(conditionFalseMessages, "; "), nil
}

// evaluateOldConditions evaluates multiple conditions when those conditions are provided in the old manner i.e. without 'any' or 'all'
func evaluateOldConditions(log logr.Logger, ctx context.EvalInterface, conditions []kyvernov1.Condition) (bool, string, error) {
	var conditionTrueMessages []string
	for _, condition := range conditions {
		if val, msg, err := Evaluate(log, ctx, condition); err != nil {
			return false, "", err
		} else if !val {
			return false, msg, nil
		} else {
			conditionTrueMessages = append(conditionTrueMessages, msg)
		}
	}

	return true, stringutils.JoinNonEmpty(conditionTrueMessages, ";"), nil
}
