package admissionpolicy

import (
	"fmt"
	"slices"

	kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
	"github.com/kyverno/kyverno/pkg/clients/dclient"
	engineapi "github.com/kyverno/kyverno/pkg/engine/api"
	controllerutils "github.com/kyverno/kyverno/pkg/utils/controller"
	kubeutils "github.com/kyverno/kyverno/pkg/utils/kube"
	admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// BuildValidatingAdmissionPolicy is used to build a Kubernetes ValidatingAdmissionPolicy from a Kyverno policy
func BuildValidatingAdmissionPolicy(
	discoveryClient dclient.IDiscovery,
	vap *admissionregistrationv1.ValidatingAdmissionPolicy,
	policy engineapi.GenericPolicy,
	exceptions []engineapi.GenericException,
) error {
	var matchResources admissionregistrationv1.MatchResources
	var matchConditions []admissionregistrationv1.MatchCondition
	var paramKind *admissionregistrationv1.ParamKind
	var validations []admissionregistrationv1.Validation
	var auditAnnotations []admissionregistrationv1.AuditAnnotation
	var variables []admissionregistrationv1.Variable

	if cpol := policy.AsKyvernoPolicy(); cpol != nil {
		// construct the rules
		var matchRules, excludeRules []admissionregistrationv1.NamedRuleWithOperations

		rule := cpol.GetSpec().Rules[0]

		// convert the match block
		match := rule.MatchResources
		if !match.ResourceDescription.IsEmpty() {
			if err := translateResource(discoveryClient, &matchResources, &matchRules, match.ResourceDescription, true); err != nil {
				return err
			}
		}

		if match.Any != nil {
			if err := translateResourceFilters(discoveryClient, &matchResources, &matchRules, match.Any, true); err != nil {
				return err
			}
		}
		if match.All != nil {
			if err := translateResourceFilters(discoveryClient, &matchResources, &matchRules, match.All, true); err != nil {
				return err
			}
		}

		// convert the exclude block
		if exclude := rule.ExcludeResources; exclude != nil {
			if !exclude.ResourceDescription.IsEmpty() {
				if err := translateResource(discoveryClient, &matchResources, &excludeRules, exclude.ResourceDescription, false); err != nil {
					return err
				}
			}

			if exclude.Any != nil {
				if err := translateResourceFilters(discoveryClient, &matchResources, &excludeRules, exclude.Any, false); err != nil {
					return err
				}
			}
			if exclude.All != nil {
				if err := translateResourceFilters(discoveryClient, &matchResources, &excludeRules, exclude.All, false); err != nil {
					return err
				}
			}
		}

		// convert the exceptions if exist
		for _, exception := range exceptions {
			if polex := exception.AsException(); polex != nil {
				match := polex.Spec.Match
				if match.Any != nil {
					if err := translateResourceFilters(discoveryClient, &matchResources, &excludeRules, match.Any, false); err != nil {
						return err
					}
				}

				if match.All != nil {
					if err := translateResourceFilters(discoveryClient, &matchResources, &excludeRules, match.All, false); err != nil {
						return err
					}
				}
			}
		}

		matchConditions = rule.CELPreconditions
		paramKind = rule.Validation.CEL.ParamKind
		validations = rule.Validation.CEL.Expressions
		auditAnnotations = rule.Validation.CEL.AuditAnnotations
		variables = rule.Validation.CEL.Variables
	} else if vpol := policy.AsValidatingPolicy(); vpol != nil {
		matchResources = *vpol.Spec.MatchConstraints
		matchConditions = vpol.Spec.MatchConditions
		validations = vpol.Spec.Validations
		auditAnnotations = vpol.Spec.AuditAnnotations
		variables = vpol.Spec.Variables

		// convert celexceptions if exist
		for _, exception := range exceptions {
			if celpolex := exception.AsCELException(); celpolex != nil {
				for _, matchCondition := range celpolex.Spec.MatchConditions {
					// negate the match condition
					expression := "!(" + matchCondition.Expression + ")"
					matchConditions = append(matchConditions, admissionregistrationv1.MatchCondition{
						Name:       matchCondition.Name,
						Expression: expression,
					})
				}
			}
		}
	}

	// set owner reference
	vap.OwnerReferences = []metav1.OwnerReference{
		{
			APIVersion: policy.GetAPIVersion(),
			Kind:       policy.GetKind(),
			Name:       policy.GetName(),
			UID:        policy.GetUID(),
		},
	}
	// set policy spec
	vap.Spec = admissionregistrationv1.ValidatingAdmissionPolicySpec{
		MatchConstraints: &matchResources,
		ParamKind:        paramKind,
		Variables:        variables,
		Validations:      validations,
		AuditAnnotations: auditAnnotations,
		MatchConditions:  matchConditions,
	}
	// set labels
	controllerutils.SetManagedByKyvernoLabel(vap)
	return nil
}

// BuildValidatingAdmissionPolicyBinding is used to build a Kubernetes ValidatingAdmissionPolicyBinding from a Kyverno policy
func BuildValidatingAdmissionPolicyBinding(
	vapbinding *admissionregistrationv1.ValidatingAdmissionPolicyBinding,
	policy engineapi.GenericPolicy,
) error {
	var validationActions []admissionregistrationv1.ValidationAction
	var paramRef *admissionregistrationv1.ParamRef
	var policyName string

	if cpol := policy.AsKyvernoPolicy(); cpol != nil {
		rule := cpol.GetSpec().Rules[0]
		validateAction := rule.Validation.FailureAction
		if validateAction != nil {
			if validateAction.Enforce() {
				validationActions = append(validationActions, admissionregistrationv1.Deny)
			} else if validateAction.Audit() {
				validationActions = append(validationActions, admissionregistrationv1.Audit)
				validationActions = append(validationActions, admissionregistrationv1.Warn)
			}
		} else {
			validateAction := cpol.GetSpec().ValidationFailureAction
			if validateAction.Enforce() {
				validationActions = append(validationActions, admissionregistrationv1.Deny)
			} else if validateAction.Audit() {
				validationActions = append(validationActions, admissionregistrationv1.Audit)
				validationActions = append(validationActions, admissionregistrationv1.Warn)
			}
		}
		paramRef = rule.Validation.CEL.ParamRef
		policyName = "cpol-" + cpol.GetName()
	} else if vpol := policy.AsValidatingPolicy(); vpol != nil {
		validationActions = vpol.Spec.ValidationAction
		policyName = "vpol-" + vpol.GetName()
	}

	// set owner reference
	vapbinding.OwnerReferences = []metav1.OwnerReference{
		{
			APIVersion: policy.GetAPIVersion(),
			Kind:       policy.GetKind(),
			Name:       policy.GetName(),
			UID:        policy.GetUID(),
		},
	}
	// set binding spec
	vapbinding.Spec = admissionregistrationv1.ValidatingAdmissionPolicyBindingSpec{
		PolicyName:        policyName,
		ParamRef:          paramRef,
		ValidationActions: validationActions,
	}
	// set labels
	controllerutils.SetManagedByKyvernoLabel(vapbinding)
	return nil
}

func translateResourceFilters(discoveryClient dclient.IDiscovery,
	matchResources *admissionregistrationv1.MatchResources,
	rules *[]admissionregistrationv1.NamedRuleWithOperations,
	resFilters kyvernov1.ResourceFilters,
	isMatch bool,
) error {
	for _, filter := range resFilters {
		err := translateResource(discoveryClient, matchResources, rules, filter.ResourceDescription, isMatch)
		if err != nil {
			return err
		}
	}
	return nil
}

func translateResource(
	discoveryClient dclient.IDiscovery,
	matchResources *admissionregistrationv1.MatchResources,
	rules *[]admissionregistrationv1.NamedRuleWithOperations,
	res kyvernov1.ResourceDescription,
	isMatch bool,
) error {
	err := constructValidatingAdmissionPolicyRules(discoveryClient, rules, res, isMatch)
	if err != nil {
		return err
	}

	if isMatch {
		matchResources.ResourceRules = *rules
		if len(res.Namespaces) > 0 {
			namespaceSelector := &metav1.LabelSelector{
				MatchExpressions: []metav1.LabelSelectorRequirement{
					{
						Key:      "kubernetes.io/metadata.name",
						Operator: "In",
						Values:   res.Namespaces,
					},
				},
			}
			matchResources.NamespaceSelector = namespaceSelector
		} else {
			matchResources.NamespaceSelector = res.NamespaceSelector
		}
		matchResources.ObjectSelector = res.Selector
	} else {
		matchResources.ExcludeResourceRules = *rules
	}
	return nil
}

func constructValidatingAdmissionPolicyRules(
	discoveryClient dclient.IDiscovery,
	rules *[]admissionregistrationv1.NamedRuleWithOperations,
	res kyvernov1.ResourceDescription,
	isMatch bool,
) error {
	// translate operations to their corresponding values in validating admission policy.
	ops := translateOperations(res.GetOperations())

	resourceNames := res.Names
	if res.Name != "" {
		resourceNames = append(resourceNames, res.Name)
	}

	// get kinds from kyverno policies and translate them to rules in validating admission policies.
	// matched resources in kyverno policies are written in the following format:
	// group/version/kind/subresource
	// whereas matched resources in validating admission policies are written in the following format:
	// apiGroups:   ["group"]
	// apiVersions: ["version"]
	// resources:   ["resource"]
	for _, kind := range res.Kinds {
		var r admissionregistrationv1.NamedRuleWithOperations

		if kind == "*" {
			r = buildNamedRuleWithOperations(resourceNames, "*", "*", ops, "*")
			*rules = append(*rules, r)
		} else {
			group, version, kind, subresource := kubeutils.ParseKindSelector(kind)
			gvrss, err := discoveryClient.FindResources(group, version, kind, subresource)
			if err != nil {
				return err
			}
			if len(gvrss) != 1 {
				return fmt.Errorf("no unique match for kind %s", kind)
			}

			for topLevelApi, apiResource := range gvrss {
				resources := []string{apiResource.Name}

				// Add pods/ephemeralcontainers if pods resource.
				if apiResource.Name == "pods" {
					resources = append(resources, "pods/ephemeralcontainers")
				}

				// Check if there's an existing rule for the same group and version.
				var isNewRule bool = true
				for i := range *rules {
					if slices.Contains((*rules)[i].APIGroups, topLevelApi.Group) && slices.Contains((*rules)[i].APIVersions, topLevelApi.Version) {
						(*rules)[i].Resources = append((*rules)[i].Resources, resources...)
						isNewRule = false
						break
					}
				}

				// If no existing rule found, create a new one.
				if isNewRule {
					r = buildNamedRuleWithOperations(resourceNames, topLevelApi.Group, topLevelApi.Version, ops, resources...)
					*rules = append(*rules, r)
				}
			}
		}
	}

	// if exclude block has namespaces but no kinds, we need to add a rule for the namespaces
	if !isMatch && len(res.Namespaces) > 0 && len(res.Kinds) == 0 {
		r := admissionregistrationv1.NamedRuleWithOperations{
			ResourceNames: res.Namespaces,
			RuleWithOperations: admissionregistrationv1.RuleWithOperations{
				Rule: admissionregistrationv1.Rule{
					Resources:   []string{"namespaces"},
					APIGroups:   []string{""},
					APIVersions: []string{"v1"},
				},
				Operations: ops,
			},
		}
		*rules = append(*rules, r)
	}
	return nil
}

func buildNamedRuleWithOperations(
	resourceNames []string,
	group, version string,
	operations []admissionregistrationv1.OperationType,
	resources ...string,
) admissionregistrationv1.NamedRuleWithOperations {
	return admissionregistrationv1.NamedRuleWithOperations{
		ResourceNames: resourceNames,
		RuleWithOperations: admissionregistrationv1.RuleWithOperations{
			Rule: admissionregistrationv1.Rule{
				Resources:   resources,
				APIGroups:   []string{group},
				APIVersions: []string{version},
			},
			Operations: operations,
		},
	}
}

func translateOperations(operations []string) []admissionregistrationv1.OperationType {
	var vapOperations []admissionregistrationv1.OperationType
	for _, op := range operations {
		if op == string(kyvernov1.Create) {
			vapOperations = append(vapOperations, admissionregistrationv1.Create)
		} else if op == string(kyvernov1.Update) {
			vapOperations = append(vapOperations, admissionregistrationv1.Update)
		} else if op == string(kyvernov1.Connect) {
			vapOperations = append(vapOperations, admissionregistrationv1.Connect)
		} else if op == string(kyvernov1.Delete) {
			vapOperations = append(vapOperations, admissionregistrationv1.Delete)
		}
	}

	// set default values for operations since it's a required field in ValidatingAdmissionPolicies
	if len(vapOperations) == 0 {
		vapOperations = append(vapOperations, admissionregistrationv1.Create)
		vapOperations = append(vapOperations, admissionregistrationv1.Update)
		vapOperations = append(vapOperations, admissionregistrationv1.Connect)
		vapOperations = append(vapOperations, admissionregistrationv1.Delete)
	}
	return vapOperations
}