package policycache

import (
	kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
	"github.com/kyverno/kyverno/ext/wildcard"
	"github.com/kyverno/kyverno/pkg/clients/dclient"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime/schema"
)

type ResourceFinder interface {
	FindResources(group, version, kind, subresource string) (map[dclient.TopLevelApiDescription]metav1.APIResource, error)
}

// Cache get method use for to get policy names and mostly use to test cache testcases
type Cache interface {
	// Set inserts a policy in the cache
	Set(string, kyvernov1.PolicyInterface, ResourceFinder) error
	// Unset removes a policy from the cache
	Unset(string)
	// GetPolicies returns all policies that apply to a namespace, including cluster-wide policies
	// If the namespace is empty, only cluster-wide policies are returned
	GetPolicies(PolicyType, schema.GroupVersionResource, string, string) []kyvernov1.PolicyInterface
}

type cache struct {
	store store
}

// NewCache create a new Cache
func NewCache() Cache {
	return &cache{
		store: newPolicyCache(),
	}
}

func (c *cache) Set(key string, policy kyvernov1.PolicyInterface, client ResourceFinder) error {
	return c.store.set(key, policy, client)
}

func (c *cache) Unset(key string) {
	c.store.unset(key)
}

func (c *cache) GetPolicies(pkey PolicyType, gvr schema.GroupVersionResource, subresource string, nspace string) []kyvernov1.PolicyInterface {
	var result []kyvernov1.PolicyInterface
	result = append(result, c.store.get(pkey, gvr, subresource, "")...)
	if nspace != "" {
		result = append(result, c.store.get(pkey, gvr, subresource, nspace)...)
	}
	// also get policies with ValidateEnforce
	if pkey == ValidateAudit {
		result = append(result, c.store.get(ValidateEnforce, gvr, subresource, "")...)
	}
	if pkey == ValidateAudit || pkey == ValidateEnforce {
		result = filterPolicies(pkey, result, nspace)
	}
	return result
}

// Filter cluster policies using validationFailureAction override
func filterPolicies(pkey PolicyType, result []kyvernov1.PolicyInterface, nspace string) []kyvernov1.PolicyInterface {
	var policies []kyvernov1.PolicyInterface
	for _, policy := range result {
		var filteredPolicy kyvernov1.PolicyInterface
		keepPolicy := true
		switch pkey {
		case ValidateAudit:
			keepPolicy, filteredPolicy = checkValidationFailureActionOverrides(false, nspace, policy)
		case ValidateEnforce:
			keepPolicy, filteredPolicy = checkValidationFailureActionOverrides(true, nspace, policy)
		}
		// add policy to result
		if keepPolicy {
			policies = append(policies, filteredPolicy)
		}
	}
	return policies
}

func checkValidationFailureActionOverrides(enforce bool, ns string, policy kyvernov1.PolicyInterface) (bool, kyvernov1.PolicyInterface) {
	filteredRules := make([]kyvernov1.Rule, 0, len(policy.GetSpec().Rules))

	// Use pointer to avoid copying the rule in each iteration
	for i := range policy.GetSpec().Rules {
		rule := &policy.GetSpec().Rules[i]

		if !rule.HasValidate() {
			continue
		}

		// if the field isn't set, use the higher level policy setting
		validationFailureAction := rule.Validation.FailureAction
		if validationFailureAction == nil {
			policyAction := policy.GetSpec().ValidationFailureAction
			validationFailureAction = &policyAction
		}

		validationFailureActionOverrides := rule.Validation.FailureActionOverrides
		if len(validationFailureActionOverrides) == 0 {
			validationFailureActionOverrides = policy.GetSpec().ValidationFailureActionOverrides
		}

		// Track if an override matched for the namespace
		overrideMatched := false
		for _, action := range validationFailureActionOverrides {
			if ns != "" && wildcard.CheckPatterns(action.Namespaces, ns) {
				overrideMatched = true
				if action.Action.Enforce() == enforce {
					filteredRules = append(filteredRules, *rule)
				}
				break // Stop once we find a matching override
			}
		}

		// If no override matched for the namespace, apply the default validation failure action
		if !overrideMatched && validationFailureAction.Enforce() == enforce {
			filteredRules = append(filteredRules, *rule)
		}
	}

	if len(filteredRules) > 0 {
		var filteredPolicy kyvernov1.PolicyInterface
		if _, ok := policy.(*kyvernov1.Policy); ok {
			shallowCopy := *policy.(*kyvernov1.Policy)
			filteredPolicy = &shallowCopy
		} else {
			shallowCopy := *policy.(*kyvernov1.ClusterPolicy)
			filteredPolicy = &shallowCopy
		}
		filteredPolicy.GetSpec().SetRules(filteredRules)
		return true, filteredPolicy
	}
	return false, nil
}