package mutation

import (
	"context"

	json_patch "github.com/evanphx/json-patch/v5"
	"github.com/go-logr/logr"
	kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
	"github.com/kyverno/kyverno/pkg/config"
	engineapi "github.com/kyverno/kyverno/pkg/engine/api"
	enginecontext "github.com/kyverno/kyverno/pkg/engine/context"
	"github.com/kyverno/kyverno/pkg/engine/handlers"
	"github.com/kyverno/kyverno/pkg/engine/internal"
	"github.com/kyverno/kyverno/pkg/engine/mutate/patch"
	engineutils "github.com/kyverno/kyverno/pkg/engine/utils"
	"github.com/kyverno/kyverno/pkg/engine/variables"
	apiutils "github.com/kyverno/kyverno/pkg/utils/api"
	jsonutils "github.com/kyverno/kyverno/pkg/utils/json"
	"gomodules.xyz/jsonpatch/v2"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

type mutateImageHandler struct {
	configuration            config.Configuration
	rclientFactory           engineapi.RegistryClientFactory
	ivm                      *engineapi.ImageVerificationMetadata
	images                   []apiutils.ImageInfo
	imageSignatureRepository string
}

func NewMutateImageHandler(
	policyContext engineapi.PolicyContext,
	resource unstructured.Unstructured,
	rule kyvernov1.Rule,
	configuration config.Configuration,
	rclientFactory engineapi.RegistryClientFactory,
	ivm *engineapi.ImageVerificationMetadata,
	imageSignatureRepository string,
) (handlers.Handler, error) {
	if len(rule.VerifyImages) == 0 {
		return nil, nil
	}
	ruleImages, _, err := engineutils.ExtractMatchingImages(resource, policyContext.JSONContext(), rule, configuration)
	if err != nil {
		return nil, err
	}
	if len(ruleImages) == 0 {
		return nil, nil
	}
	return mutateImageHandler{
		configuration:            configuration,
		rclientFactory:           rclientFactory,
		ivm:                      ivm,
		images:                   ruleImages,
		imageSignatureRepository: imageSignatureRepository,
	}, nil
}

func (h mutateImageHandler) Process(
	ctx context.Context,
	logger logr.Logger,
	policyContext engineapi.PolicyContext,
	resource unstructured.Unstructured,
	rule kyvernov1.Rule,
	contextLoader engineapi.EngineContextLoader,
) (unstructured.Unstructured, []engineapi.RuleResponse) {
	jsonContext := policyContext.JSONContext()
	ruleCopy, err := substituteVariables(rule, jsonContext, logger)
	if err != nil {
		return resource, handlers.WithResponses(
			engineapi.RuleError(rule.Name, engineapi.ImageVerify, "failed to substitute variables", err),
		)
	}
	var engineResponses []*engineapi.RuleResponse
	var patches []jsonpatch.JsonPatchOperation
	for _, imageVerify := range ruleCopy.VerifyImages {
		rclient, err := h.rclientFactory.GetClient(ctx, imageVerify.ImageRegistryCredentials)
		if err != nil {
			return resource, handlers.WithResponses(
				engineapi.RuleError(rule.Name, engineapi.ImageVerify, "failed to fetch secrets", err),
			)
		}
		iv := internal.NewImageVerifier(logger, rclient, policyContext, *ruleCopy, h.ivm, h.imageSignatureRepository)
		patch, ruleResponse := iv.Verify(ctx, imageVerify, h.images, h.configuration)
		patches = append(patches, patch...)
		engineResponses = append(engineResponses, ruleResponse...)
	}
	if len(patches) != 0 {
		patch := jsonutils.JoinPatches(patch.ConvertPatches(patches...)...)
		decoded, err := json_patch.DecodePatch(patch)
		if err != nil {
			return resource, handlers.WithResponses(
				engineapi.RuleError(rule.Name, engineapi.ImageVerify, "failed to decode patch", err),
			)
		}
		options := &json_patch.ApplyOptions{SupportNegativeIndices: true, AllowMissingPathOnRemove: true, EnsurePathExistsOnAdd: true}
		resourceBytes, err := resource.MarshalJSON()
		if err != nil {
			return resource, handlers.WithResponses(
				engineapi.RuleError(rule.Name, engineapi.ImageVerify, "failed to marshal resource", err),
			)
		}
		patchedResourceBytes, err := decoded.ApplyWithOptions(resourceBytes, options)
		if err != nil {
			return resource, handlers.WithResponses(
				engineapi.RuleError(rule.Name, engineapi.ImageVerify, "failed to apply patch", err),
			)
		}
		if err := resource.UnmarshalJSON(patchedResourceBytes); err != nil {
			return resource, handlers.WithResponses(
				engineapi.RuleError(rule.Name, engineapi.ImageVerify, "failed to unmarshal resource", err),
			)
		}
	}
	return resource, handlers.WithResponses(engineResponses...)
}

func substituteVariables(rule kyvernov1.Rule, ctx enginecontext.EvalInterface, logger logr.Logger) (*kyvernov1.Rule, error) {
	// remove attestations as variables are not substituted in them
	ruleCopy := *rule.DeepCopy()
	for i := range ruleCopy.VerifyImages {
		for j := range ruleCopy.VerifyImages[i].Attestations {
			ruleCopy.VerifyImages[i].Attestations[j].Conditions = nil
		}
	}
	var err error
	ruleCopy, err = variables.SubstituteAllInRule(logger, ctx, ruleCopy)
	if err != nil {
		return nil, err
	}
	// replace attestations
	for i := range ruleCopy.VerifyImages {
		for j := range ruleCopy.VerifyImages[i].Attestations {
			ruleCopy.VerifyImages[i].Attestations[j].Conditions = rule.VerifyImages[i].Attestations[j].Conditions
		}
	}
	return &ruleCopy, nil
}
