package policycache

import (
	"sync"

	kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
	"github.com/kyverno/kyverno/pkg/autogen"
	kubeutils "github.com/kyverno/kyverno/pkg/utils/kube"
	"go.uber.org/multierr"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/util/sets"
	kcache "k8s.io/client-go/tools/cache"
)

type store interface {
	// set inserts a policy in the cache
	set(string, kyvernov1.PolicyInterface, ResourceFinder) error
	// unset removes a policy from the cache
	unset(string)
	// get finds policies that match a given type, gvr, subresource and namespace
	get(PolicyType, schema.GroupVersionResource, string, string) []kyvernov1.PolicyInterface
}

type policyCache struct {
	store store
	lock  sync.RWMutex
}

func newPolicyCache() store {
	return &policyCache{
		store: newPolicyMap(),
	}
}

func (pc *policyCache) set(key string, policy kyvernov1.PolicyInterface, client ResourceFinder) error {
	pc.lock.Lock()
	defer pc.lock.Unlock()
	if err := pc.store.set(key, policy, client); err != nil {
		return err
	}
	logger.V(4).Info("policy is added to cache", "key", key)
	return nil
}

func (pc *policyCache) unset(key string) {
	pc.lock.Lock()
	defer pc.lock.Unlock()
	pc.store.unset(key)
	logger.V(4).Info("policy is removed from cache", "key", key)
}

func (pc *policyCache) get(pkey PolicyType, gvr schema.GroupVersionResource, subresource string, nspace string) []kyvernov1.PolicyInterface {
	pc.lock.RLock()
	defer pc.lock.RUnlock()
	return pc.store.get(pkey, gvr, subresource, nspace)
}

type policyKey struct {
	Group       string
	Version     string
	Resource    string
	SubResource string
}

var podsKey = policyKey{"", "v1", "pods", ""}

type policyMap struct {
	// policies maps names to policy interfaces
	policies map[string]kyvernov1.PolicyInterface
	// kindType stores names of ClusterPolicies and Namespaced Policies.
	// They are accessed first by GVRS then by PolicyType.
	kindType map[policyKey]map[PolicyType]sets.Set[string]
}

func newPolicyMap() *policyMap {
	return &policyMap{
		policies: map[string]kyvernov1.PolicyInterface{},
		kindType: map[policyKey]map[PolicyType]sets.Set[string]{},
	}
}

func computeEnforcePolicy(spec *kyvernov1.Spec) bool {
	if spec.ValidationFailureAction.Enforce() {
		return true
	}
	for _, k := range spec.ValidationFailureActionOverrides {
		if k.Action.Enforce() {
			return true
		}
	}
	return false
}

func set(set sets.Set[string], item string, value bool) sets.Set[string] {
	if value {
		return set.Insert(item)
	} else {
		return set.Delete(item)
	}
}

func (m *policyMap) set(key string, policy kyvernov1.PolicyInterface, client ResourceFinder) error {
	var errs []error
	enforcePolicy := computeEnforcePolicy(policy.GetSpec())
	m.policies[key] = policy
	type state struct {
		hasMutate, hasValidate, hasGenerate, hasVerifyImages, hasImagesValidationChecks bool
	}
	kindStates := map[policyKey]state{}
	for _, rule := range autogen.ComputeRules(policy) {
		entries := sets.New[policyKey]()
		for _, gvk := range rule.MatchResources.GetKinds() {
			group, version, kind, subresource := kubeutils.ParseKindSelector(gvk)
			gvrss, err := client.FindResources(group, version, kind, subresource)
			if err != nil {
				logger.Error(err, "failed to fetch resource group versions", "group", group, "version", version, "kind", kind)
				errs = append(errs, err)
			} else {
				for gvrs := range gvrss {
					entries.Insert(policyKey{
						Group:       gvrs.Group,
						Version:     gvrs.Version,
						Resource:    gvrs.Resource,
						SubResource: gvrs.SubResource,
					})
				}
			}
		}
		if entries.Len() > 0 {
			// account for pods/ephemeralcontainers special case
			if entries.Has(podsKey) {
				entries.Insert(policyKey{
					Group:       podsKey.Group,
					Version:     podsKey.Version,
					Resource:    podsKey.Resource,
					SubResource: "ephemeralcontainers",
				})
			}
			hasMutate := rule.HasMutate()
			hasValidate := rule.HasValidate()
			hasGenerate := rule.HasGenerate()
			hasVerifyImages := rule.HasVerifyImages()
			hasImagesValidationChecks := rule.HasVerifyImageChecks()
			for gvrs := range entries {
				entry := kindStates[gvrs]
				entry.hasMutate = entry.hasMutate || hasMutate
				entry.hasValidate = entry.hasValidate || hasValidate
				entry.hasGenerate = entry.hasGenerate || hasGenerate
				entry.hasVerifyImages = entry.hasVerifyImages || hasVerifyImages
				entry.hasImagesValidationChecks = entry.hasImagesValidationChecks || hasImagesValidationChecks
				kindStates[gvrs] = entry
			}
		}
	}
	for gvrs, state := range kindStates {
		if m.kindType[gvrs] == nil {
			m.kindType[gvrs] = map[PolicyType]sets.Set[string]{
				Mutate:               sets.New[string](),
				ValidateEnforce:      sets.New[string](),
				ValidateAudit:        sets.New[string](),
				Generate:             sets.New[string](),
				VerifyImagesMutate:   sets.New[string](),
				VerifyImagesValidate: sets.New[string](),
			}
		}
		m.kindType[gvrs][Mutate] = set(m.kindType[gvrs][Mutate], key, state.hasMutate)
		m.kindType[gvrs][ValidateEnforce] = set(m.kindType[gvrs][ValidateEnforce], key, state.hasValidate && enforcePolicy)
		m.kindType[gvrs][ValidateAudit] = set(m.kindType[gvrs][ValidateAudit], key, state.hasValidate && !enforcePolicy)
		m.kindType[gvrs][Generate] = set(m.kindType[gvrs][Generate], key, state.hasGenerate)
		m.kindType[gvrs][VerifyImagesMutate] = set(m.kindType[gvrs][VerifyImagesMutate], key, state.hasVerifyImages)
		m.kindType[gvrs][VerifyImagesValidate] = set(m.kindType[gvrs][VerifyImagesValidate], key, state.hasVerifyImages && state.hasImagesValidationChecks)
	}
	return multierr.Combine(errs...)
}

func (m *policyMap) unset(key string) {
	delete(m.policies, key)
	for gvrs := range m.kindType {
		for policyType := range m.kindType[gvrs] {
			m.kindType[gvrs][policyType] = m.kindType[gvrs][policyType].Delete(key)
		}
	}
}

func (m *policyMap) get(key PolicyType, gvr schema.GroupVersionResource, subresource string, namespace string) []kyvernov1.PolicyInterface {
	var result []kyvernov1.PolicyInterface
	pKey := policyKey{gvr.Group, gvr.Version, gvr.Resource, subresource}
	for policyName := range m.kindType[pKey][key] {
		ns, _, err := kcache.SplitMetaNamespaceKey(policyName)
		if err != nil {
			logger.Error(err, "failed to parse policy name", "policyName", policyName)
		}
		isNamespacedPolicy := ns != ""
		policy := m.policies[policyName]
		if policy == nil {
			logger.Info("nil policy in the cache, this should not happen")
		}
		if !isNamespacedPolicy && namespace == "" {
			result = append(result, policy)
		} else {
			if ns == namespace {
				result = append(result, policy)
			}
		}
	}
	return result
}
