package generate

import (
	"context"
	"fmt"

	"github.com/go-logr/logr"
	gojmespath "github.com/kyverno/go-jmespath"
	kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
	"github.com/kyverno/kyverno/pkg/background/common"
	"github.com/kyverno/kyverno/pkg/clients/dclient"
	engineapi "github.com/kyverno/kyverno/pkg/engine/api"
	engineutils "github.com/kyverno/kyverno/pkg/engine/utils"
	"github.com/kyverno/kyverno/pkg/engine/validate"
	"github.com/kyverno/kyverno/pkg/engine/variables"
	"go.uber.org/multierr"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

type generator struct {
	client           dclient.Interface
	logger           logr.Logger
	policyContext    engineapi.PolicyContext
	policy           kyvernov1.PolicyInterface
	rule             kyvernov1.Rule
	contextEntries   []kyvernov1.ContextEntry
	anyAllConditions any
	trigger          unstructured.Unstructured
	forEach          []kyvernov1.ForEachGeneration
	pattern          kyvernov1.GeneratePattern
	contextLoader    engineapi.EngineContextLoader
}

func newGenerator(client dclient.Interface,
	logger logr.Logger,
	policyContext engineapi.PolicyContext,
	policy kyvernov1.PolicyInterface,
	rule kyvernov1.Rule,
	contextEntries []kyvernov1.ContextEntry,
	anyAllConditions any,
	trigger unstructured.Unstructured,
	pattern kyvernov1.GeneratePattern,
	contextLoader engineapi.EngineContextLoader,
) *generator {
	return &generator{
		client:           client,
		logger:           logger,
		policyContext:    policyContext,
		policy:           policy,
		rule:             rule,
		contextEntries:   contextEntries,
		anyAllConditions: anyAllConditions,
		trigger:          trigger,
		pattern:          pattern,
		contextLoader:    contextLoader,
	}
}

func newForeachGenerator(client dclient.Interface,
	logger logr.Logger,
	policyContext engineapi.PolicyContext,
	policy kyvernov1.PolicyInterface,
	rule kyvernov1.Rule,
	contextEntries []kyvernov1.ContextEntry,
	anyAllConditions any,
	trigger unstructured.Unstructured,
	forEach []kyvernov1.ForEachGeneration,
	contextLoader engineapi.EngineContextLoader,
) *generator {
	return &generator{
		client:           client,
		logger:           logger,
		policyContext:    policyContext,
		policy:           policy,
		rule:             rule,
		contextEntries:   contextEntries,
		anyAllConditions: anyAllConditions,
		trigger:          trigger,
		forEach:          forEach,
		contextLoader:    contextLoader,
	}
}

func (g *generator) generate() ([]kyvernov1.ResourceSpec, error) {
	responses := []generateResponse{}
	var err error
	var newGenResources []kyvernov1.ResourceSpec

	if err := g.loadContext(context.TODO()); err != nil {
		return newGenResources, fmt.Errorf("failed to load context: %v", err)
	}

	typeConditions, err := engineutils.TransformConditions(g.anyAllConditions)
	if err != nil {
		return newGenResources, fmt.Errorf("failed to parse preconditions: %v", err)
	}

	preconditionsPassed, msg, err := variables.EvaluateConditions(g.logger, g.policyContext.JSONContext(), typeConditions)
	if err != nil {
		return newGenResources, fmt.Errorf("failed to evaluate preconditions: %v", err)
	}

	if !preconditionsPassed {
		g.logger.V(2).Info("preconditions not met", "msg", msg)
		return newGenResources, nil
	}

	pattern, err := variables.SubstituteAllInType(g.logger, g.policyContext.JSONContext(), &g.pattern)
	if err != nil {
		g.logger.Error(err, "variable substitution failed for rule", "rule", g.rule.Name)
		return nil, err
	}

	target := pattern.ResourceSpec
	logger := g.logger.WithValues("target", target.String())

	if pattern.Clone.Name != "" {
		resp := manageClone(logger.WithValues("type", "clone"), target, kyvernov1.ResourceSpec{}, g.policy.GetSpec().UseServerSideApply, *pattern, g.client)
		responses = append(responses, resp)
	} else if len(pattern.CloneList.Kinds) != 0 {
		responses = manageCloneList(logger.WithValues("type", "cloneList"), target.GetNamespace(), g.policy.GetSpec().UseServerSideApply, *pattern, g.client)
	} else {
		resp := manageData(logger.WithValues("type", "data"), target, pattern.RawData, g.rule.Generation.Synchronize, g.client)
		responses = append(responses, resp)
	}

	for _, response := range responses {
		targetMeta := response.GetTarget()
		if response.GetError() != nil {
			logger.Error(response.GetError(), "failed to generate resource", "mode", response.GetAction())
			return newGenResources, err
		}

		if response.GetAction() == Skip {
			continue
		}

		logger.V(3).Info("applying generate rule", "mode", response.GetAction())
		if response.GetData() == nil && response.GetAction() == Update {
			logger.V(4).Info("no changes required for generate target resource")
			return newGenResources, nil
		}

		newResource := &unstructured.Unstructured{}
		newResource.SetUnstructuredContent(response.GetData())
		newResource.SetName(targetMeta.GetName())
		newResource.SetNamespace(targetMeta.GetNamespace())
		if newResource.GetKind() == "" {
			newResource.SetKind(targetMeta.GetKind())
		}

		newResource.SetAPIVersion(targetMeta.GetAPIVersion())
		common.ManageLabels(newResource, g.trigger, g.policy, g.rule.Name)
		if response.GetAction() == Create {
			newResource.SetResourceVersion("")
			if g.policy.GetSpec().UseServerSideApply {
				_, err = g.client.ApplyResource(context.TODO(), targetMeta.GetAPIVersion(), targetMeta.GetKind(), targetMeta.GetNamespace(), targetMeta.GetName(), newResource, false, "generate")
			} else {
				_, err = g.client.CreateResource(context.TODO(), targetMeta.GetAPIVersion(), targetMeta.GetKind(), targetMeta.GetNamespace(), newResource, false)
			}
			if err != nil {
				if !apierrors.IsAlreadyExists(err) {
					return newGenResources, err
				}
			}
			logger.V(2).Info("created generate target resource")
			newGenResources = append(newGenResources, targetMeta)
		} else if response.GetAction() == Update {
			generatedObj, err := g.client.GetResource(context.TODO(), targetMeta.GetAPIVersion(), targetMeta.GetKind(), targetMeta.GetNamespace(), targetMeta.GetName())
			if err != nil {
				logger.V(2).Info("creating new target due to the failure when fetching", "err", err.Error())
				if g.policy.GetSpec().UseServerSideApply {
					_, err = g.client.ApplyResource(context.TODO(), targetMeta.GetAPIVersion(), targetMeta.GetKind(), targetMeta.GetNamespace(), targetMeta.GetName(), newResource, false, "generate")
				} else {
					_, err = g.client.CreateResource(context.TODO(), targetMeta.GetAPIVersion(), targetMeta.GetKind(), targetMeta.GetNamespace(), newResource, false)
				}
				if err != nil {
					return newGenResources, err
				}
				newGenResources = append(newGenResources, targetMeta)
			} else {
				if !g.rule.Generation.Synchronize {
					logger.V(4).Info("synchronize disabled, skip syncing changes")
					continue
				}
				if err := validate.MatchPattern(logger, newResource.Object, generatedObj.Object); err == nil {
					if err := validate.MatchPattern(logger, generatedObj.Object, newResource.Object); err == nil {
						logger.V(4).Info("patterns match, skipping updates")
						continue
					}
				}

				logger.V(4).Info("updating existing resource")
				if targetMeta.GetAPIVersion() == "" {
					generatedResourceAPIVersion := generatedObj.GetAPIVersion()
					newResource.SetAPIVersion(generatedResourceAPIVersion)
				}
				if targetMeta.GetNamespace() == "" {
					newResource.SetNamespace("default")
				}

				if g.policy.GetSpec().UseServerSideApply {
					_, err = g.client.ApplyResource(context.TODO(), targetMeta.GetAPIVersion(), targetMeta.GetKind(), targetMeta.GetNamespace(), targetMeta.GetName(), newResource, false, "generate")
				} else {
					_, err = g.client.UpdateResource(context.TODO(), targetMeta.GetAPIVersion(), targetMeta.GetKind(), targetMeta.GetNamespace(), newResource, false)
				}
				if err != nil {
					logger.Error(err, "failed to update resource")
					return newGenResources, err
				}
			}
			logger.V(3).Info("updated generate target resource")
		}
	}
	return newGenResources, nil
}

func (g *generator) generateForeach() ([]kyvernov1.ResourceSpec, error) {
	var errors []error
	var genResources []kyvernov1.ResourceSpec

	for i, foreach := range g.forEach {
		elements, err := engineutils.EvaluateList(foreach.List, g.policyContext.JSONContext())
		if err != nil {
			errors = append(errors, fmt.Errorf("failed to evaluate %v foreach list: %v", i, err))
			continue
		}
		gen, err := g.generateElements(foreach, elements, nil)
		if err != nil {
			errors = append(errors, fmt.Errorf("failed to process %v foreach in rule %s: %v", i, g.rule.Name, err))
		}
		if gen != nil {
			genResources = append(genResources, gen...)
		}
	}
	return genResources, multierr.Combine(errors...)
}

func (g *generator) generateElements(foreach kyvernov1.ForEachGeneration, elements []interface{}, elementScope *bool) ([]kyvernov1.ResourceSpec, error) {
	var errors []error
	var genResources []kyvernov1.ResourceSpec
	g.policyContext.JSONContext().Checkpoint()
	defer g.policyContext.JSONContext().Restore()

	for index, element := range elements {
		if element == nil {
			continue
		}

		g.policyContext.JSONContext().Reset()
		policyContext := g.policyContext.Copy()
		if err := engineutils.AddElementToContext(policyContext, element, index, 0, elementScope); err != nil {
			g.logger.Error(err, "")
			errors = append(errors, fmt.Errorf("failed to add %v element to context: %v", index, err))
			continue
		}

		gen, err := newGenerator(g.client,
			g.logger,
			policyContext,
			g.policy,
			g.rule,
			foreach.Context,
			foreach.AnyAllConditions,
			g.trigger,
			foreach.GeneratePattern,
			g.contextLoader).
			generate()
		if err != nil {
			errors = append(errors, fmt.Errorf("failed to process %v element: %v", index, err))
		}
		if gen != nil {
			genResources = append(genResources, gen...)
		}
	}
	return genResources, multierr.Combine(errors...)
}

func (g *generator) loadContext(ctx context.Context) error {
	if err := g.contextLoader(ctx, g.contextEntries, g.policyContext.JSONContext()); err != nil {
		if _, ok := err.(gojmespath.NotFoundError); ok {
			g.logger.V(3).Info("failed to load context", "reason", err.Error())
		} else {
			g.logger.Error(err, "failed to load context")
		}
		return err
	}
	return nil
}