package engine

import (
	"context"
	"fmt"
	"time"

	"github.com/go-logr/logr"
	kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
	"github.com/kyverno/kyverno/pkg/autogen"
	engineapi "github.com/kyverno/kyverno/pkg/engine/api"
	enginecontext "github.com/kyverno/kyverno/pkg/engine/context"
	"github.com/kyverno/kyverno/pkg/engine/internal"
	engineutils "github.com/kyverno/kyverno/pkg/engine/utils"
	"github.com/kyverno/kyverno/pkg/engine/variables"
	"github.com/kyverno/kyverno/pkg/tracing"
	"go.opentelemetry.io/otel/trace"
)

func (e *engine) verifyAndPatchImages(
	ctx context.Context,
	logger logr.Logger,
	policyContext engineapi.PolicyContext,
) (engineapi.PolicyResponse, engineapi.ImageVerificationMetadata) {
	policy := policyContext.Policy()
	resp := engineapi.NewPolicyResponse()

	policyContext.JSONContext().Checkpoint()
	defer policyContext.JSONContext().Restore()

	ivm := engineapi.ImageVerificationMetadata{}
	applyRules := policy.GetSpec().GetApplyRules()

	for _, rule := range autogen.ComputeRules(policyContext.Policy()) {
		tracing.ChildSpan(
			ctx,
			"pkg/engine",
			fmt.Sprintf("RULE %s", rule.Name),
			func(ctx context.Context, span trace.Span) {
				e.doVerifyAndPatch(ctx, logger, policyContext, rule, &resp, &ivm)
			},
		)

		if applyRules == kyvernov1.ApplyOne && resp.Stats.RulesAppliedCount > 0 {
			break
		}
	}
	// TODO: i doesn't make sense to not return the patched resource here
	return resp, ivm
}

func (e *engine) doVerifyAndPatch(
	ctx context.Context,
	logger logr.Logger,
	policyContext engineapi.PolicyContext,
	rule kyvernov1.Rule,
	resp *engineapi.PolicyResponse,
	ivm *engineapi.ImageVerificationMetadata,
) {
	if len(rule.VerifyImages) == 0 {
		return
	}
	startTime := time.Now()
	logger = internal.LoggerWithRule(logger, rule)

	if err := matches(rule, policyContext, policyContext.NewResource()); err != nil {
		logger.V(5).Info("resource does not match rule", "reason", err.Error())
		return
	}

	// check if there is a corresponding policy exception
	ruleResp := e.hasPolicyExceptions(logger, engineapi.ImageVerify, policyContext, rule)
	if ruleResp != nil {
		resp.Rules = append(resp.Rules, *ruleResp)
		return
	}

	logger.V(3).Info("processing image verification rule")

	ruleImages, _, err := engineutils.ExtractMatchingImages(
		policyContext.NewResource(),
		policyContext.JSONContext(),
		rule,
		e.configuration,
	)
	if err != nil {
		internal.AddRuleResponse(
			resp,
			internal.RuleError(rule, engineapi.ImageVerify, "failed to extract images", err),
			startTime,
		)
		return
	}
	if len(ruleImages) == 0 {
		return
	}
	policyContext.JSONContext().Restore()
	if err := internal.LoadContext(ctx, e, policyContext, rule); err != nil {
		internal.AddRuleResponse(
			resp,
			internal.RuleError(rule, engineapi.ImageVerify, "failed to load context", err),
			startTime,
		)
		return
	}
	ruleCopy, err := substituteVariables(&rule, policyContext.JSONContext(), logger)
	if err != nil {
		internal.AddRuleResponse(
			resp,
			internal.RuleError(rule, engineapi.ImageVerify, "failed to substitute variables", err),
			startTime,
		)
		return
	}
	iv := internal.NewImageVerifier(
		logger,
		e.rclient,
		policyContext,
		*ruleCopy,
		ivm,
	)
	for _, imageVerify := range ruleCopy.VerifyImages {
		for _, r := range iv.Verify(ctx, imageVerify, ruleImages, e.configuration) {
			internal.AddRuleResponse(resp, r, startTime)
		}
	}
}

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 {
		ruleCopy.VerifyImages[i].Attestations = nil
	}

	var err error
	ruleCopy, err = variables.SubstituteAllInRule(logger, ctx, ruleCopy)
	if err != nil {
		return nil, err
	}

	// replace attestations
	for i := range rule.VerifyImages {
		ruleCopy.VerifyImages[i].Attestations = rule.VerifyImages[i].Attestations
	}

	return &ruleCopy, nil
}