package mutate

import (
	"context"
	"fmt"

	"github.com/go-logr/logr"
	kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
	kyvernov1beta1 "github.com/kyverno/kyverno/api/kyverno/v1beta1"
	"github.com/kyverno/kyverno/pkg/background/common"
	kyvernov1listers "github.com/kyverno/kyverno/pkg/client/listers/kyverno/v1"
	"github.com/kyverno/kyverno/pkg/clients/dclient"
	"github.com/kyverno/kyverno/pkg/config"
	engineapi "github.com/kyverno/kyverno/pkg/engine/api"
	"github.com/kyverno/kyverno/pkg/engine/jmespath"
	"github.com/kyverno/kyverno/pkg/event"
	admissionutils "github.com/kyverno/kyverno/pkg/utils/admission"
	engineutils "github.com/kyverno/kyverno/pkg/utils/engine"
	"go.uber.org/multierr"
	admissionv1 "k8s.io/api/admission/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime/schema"
	corev1listers "k8s.io/client-go/listers/core/v1"
	"k8s.io/client-go/tools/cache"
)

var ErrEmptyPatch error = fmt.Errorf("empty resource to patch")

type mutateExistingController struct {
	// clients
	client        dclient.Interface
	statusControl common.StatusControlInterface
	engine        engineapi.Engine

	// listers
	policyLister  kyvernov1listers.ClusterPolicyLister
	npolicyLister kyvernov1listers.PolicyLister
	nsLister      corev1listers.NamespaceLister

	configuration config.Configuration
	eventGen      event.Interface

	log logr.Logger
	jp  jmespath.Interface
}

// NewMutateExistingController returns an instance of the MutateExistingController
func NewMutateExistingController(
	client dclient.Interface,
	statusControl common.StatusControlInterface,
	engine engineapi.Engine,
	policyLister kyvernov1listers.ClusterPolicyLister,
	npolicyLister kyvernov1listers.PolicyLister,
	nsLister corev1listers.NamespaceLister,
	dynamicConfig config.Configuration,
	eventGen event.Interface,
	log logr.Logger,
	jp jmespath.Interface,
) *mutateExistingController {
	c := mutateExistingController{
		client:        client,
		statusControl: statusControl,
		engine:        engine,
		policyLister:  policyLister,
		npolicyLister: npolicyLister,
		nsLister:      nsLister,
		configuration: dynamicConfig,
		eventGen:      eventGen,
		log:           log,
		jp:            jp,
	}
	return &c
}

func (c *mutateExistingController) ProcessUR(ur *kyvernov1beta1.UpdateRequest) error {
	logger := c.log.WithValues("name", ur.GetName(), "policy", ur.Spec.GetPolicyKey(), "resource", ur.Spec.GetResource().String())
	var errs []error

	policy, err := c.getPolicy(ur)
	if err != nil {
		logger.Error(err, "failed to get policy")
		return err
	}

	for _, rule := range policy.GetSpec().Rules {
		if !rule.IsMutateExisting() || ur.Spec.Rule != rule.Name {
			continue
		}

		var trigger *unstructured.Unstructured
		admissionRequest := ur.Spec.Context.AdmissionRequestInfo.AdmissionRequest
		if admissionRequest == nil {
			trigger, err = common.GetResource(c.client, ur.Spec, c.log)
			if err != nil || trigger == nil {
				logger.WithName(rule.Name).Error(err, "failed to get trigger resource")
				errs = append(errs, err)
				continue
			}
		} else {
			if admissionRequest.Operation == admissionv1.Create {
				trigger, err = common.GetResource(c.client, ur.Spec, c.log)
				if err != nil || trigger == nil {
					if admissionRequest.SubResource == "" {
						logger.WithName(rule.Name).Error(err, "failed to get trigger resource")
						errs = append(errs, err)
						continue
					} else {
						logger.WithName(rule.Name).Info("trigger resource not found for subresource, reverting to resource in AdmissionReviewRequest", "subresource", admissionRequest.SubResource)
						newResource, _, err := admissionutils.ExtractResources(nil, *admissionRequest)
						if err != nil {
							logger.WithName(rule.Name).Error(err, "failed to extract resources from admission review request")
							errs = append(errs, err)
							continue
						}
						trigger = &newResource
					}
				}
			} else {
				newResource, oldResource, err := admissionutils.ExtractResources(nil, *admissionRequest)
				if err != nil {
					logger.WithName(rule.Name).Error(err, "failed to extract resources from admission review request")
					errs = append(errs, err)
					continue
				}

				trigger = &newResource
				if newResource.Object == nil {
					trigger = &oldResource
				}
			}
		}

		namespaceLabels := engineutils.GetNamespaceSelectorsFromNamespaceLister(trigger.GetKind(), trigger.GetNamespace(), c.nsLister, logger)
		policyContext, err := common.NewBackgroundContext(logger, c.client, ur, policy, trigger, c.configuration, c.jp, namespaceLabels)
		if err != nil {
			logger.WithName(rule.Name).Error(err, "failed to build policy context")
			errs = append(errs, err)
			continue
		}
		if admissionRequest != nil {
			var gvk schema.GroupVersionKind
			gvk, err = c.client.Discovery().GetGVKFromGVR(schema.GroupVersionResource(admissionRequest.Resource))
			if err != nil {
				logger.WithName(rule.Name).Error(err, "failed to get GVK from GVR", "GVR", admissionRequest.Resource)
				errs = append(errs, err)
				continue
			}
			policyContext = policyContext.WithResourceKind(gvk, admissionRequest.SubResource)
		}

		er := c.engine.Mutate(context.TODO(), policyContext)
		for _, r := range er.PolicyResponse.Rules {
			patched, parentGVR, patchedSubresource := r.PatchedTarget()
			switch r.Status() {
			case engineapi.RuleStatusFail, engineapi.RuleStatusError, engineapi.RuleStatusWarn:
				err := fmt.Errorf("failed to mutate existing resource, rule response%v: %s", r.Status(), r.Message())
				logger.Error(err, "")
				errs = append(errs, err)
				c.report(err, ur.Spec.Policy, rule.Name, patched)

			case engineapi.RuleStatusSkip:
				logger.Info("mutate existing rule skipped", "rule", r.Name(), "message", r.Message())
				c.report(err, ur.Spec.Policy, rule.Name, patched)

			case engineapi.RuleStatusPass:
				patchedNew := patched
				if patchedNew == nil {
					logger.Error(ErrEmptyPatch, "", "rule", r.Name(), "message", r.Message())
					errs = append(errs, err)
					continue
				}

				if r.Status() == engineapi.RuleStatusPass {
					patchedNew.SetResourceVersion(patched.GetResourceVersion())
					var updateErr error
					if patchedSubresource == "status" {
						_, updateErr = c.client.UpdateStatusResource(context.TODO(), patchedNew.GetAPIVersion(), patchedNew.GetKind(), patchedNew.GetNamespace(), patchedNew.Object, false)
					} else if patchedSubresource != "" {
						parentResourceGVR := parentGVR
						parentResourceGV := schema.GroupVersion{Group: parentResourceGVR.Group, Version: parentResourceGVR.Version}
						parentResourceGVK, err := c.client.Discovery().GetGVKFromGVR(parentResourceGV.WithResource(parentResourceGVR.Resource))
						if err != nil {
							logger.Error(err, "failed to get GVK from GVR", "GVR", parentResourceGVR)
							errs = append(errs, err)
							continue
						}
						_, updateErr = c.client.UpdateResource(context.TODO(), parentResourceGV.String(), parentResourceGVK.Kind, patchedNew.GetNamespace(), patchedNew.Object, false, patchedSubresource)
					} else {
						_, updateErr = c.client.UpdateResource(context.TODO(), patchedNew.GetAPIVersion(), patchedNew.GetKind(), patchedNew.GetNamespace(), patchedNew.Object, false)
					}
					if updateErr != nil {
						errs = append(errs, updateErr)
						logger.WithName(rule.Name).Error(updateErr, "failed to update target resource", "namespace", patchedNew.GetNamespace(), "name", patchedNew.GetName())
					} else {
						logger.WithName(rule.Name).V(4).Info("successfully mutated existing resource", "namespace", patchedNew.GetNamespace(), "name", patchedNew.GetName())
					}

					c.report(updateErr, ur.Spec.Policy, rule.Name, patched)
				}
			}
		}
	}

	err = multierr.Combine(errs...)
	return updateURStatus(c.statusControl, *ur, err)
}

func (c *mutateExistingController) getPolicy(ur *kyvernov1beta1.UpdateRequest) (policy kyvernov1.PolicyInterface, err error) {
	pNamespace, pName, err := cache.SplitMetaNamespaceKey(ur.Spec.Policy)
	if err != nil {
		return nil, err
	}

	if pNamespace != "" {
		return c.npolicyLister.Policies(pNamespace).Get(pName)
	}

	return c.policyLister.Get(pName)
}

func (c *mutateExistingController) report(err error, policy, rule string, target *unstructured.Unstructured) {
	var events []event.Info

	if target == nil {
		c.log.WithName("mutateExisting").Info("cannot generate events for empty target resource", "policy", policy, "rule", rule)
	}

	if err != nil {
		events = event.NewBackgroundFailedEvent(err, policy, rule, event.MutateExistingController, target)
	} else {
		events = event.NewBackgroundSuccessEvent(policy, rule, event.MutateExistingController, target)
	}

	c.eventGen.Add(events...)
}

func updateURStatus(statusControl common.StatusControlInterface, ur kyvernov1beta1.UpdateRequest, err error) error {
	if err != nil {
		if _, err := statusControl.Failed(ur.GetName(), err.Error(), nil); err != nil {
			return err
		}
	} else {
		if _, err := statusControl.Success(ur.GetName(), nil); err != nil {
			return err
		}
	}
	return nil
}
