/*
Copyright 2021 The Crossplane Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

	http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package applicationconfiguration

import (
	"context"
	"encoding/json"
	"fmt"
	"reflect"
	"strconv"
	"strings"

	"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
	"github.com/crossplane/crossplane-runtime/pkg/resource"
	"github.com/pkg/errors"
	corev1 "k8s.io/api/core/v1"
	kerrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apimachinery/pkg/util/intstr"
	"k8s.io/klog/v2"
	"k8s.io/utils/pointer"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
	"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
	oamtype "github.com/oam-dev/kubevela/apis/types"
	helmapi "github.com/oam-dev/kubevela/pkg/appfile/helm/flux2apis"
	"github.com/oam-dev/kubevela/pkg/controller/common"
	"github.com/oam-dev/kubevela/pkg/controller/utils"
	"github.com/oam-dev/kubevela/pkg/oam"
	"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
	"github.com/oam-dev/kubevela/pkg/oam/util"
)

// Render error strings.
const (
	errUnmarshalWorkload = "cannot unmarshal workload"
	errUnmarshalTrait    = "cannot unmarshal trait"
)

// Render error format strings.
const (
	errFmtGetComponent     = "cannot get component %q"
	errFmtGetScope         = "cannot get scope %q"
	errFmtResolveParams    = "cannot resolve parameter values for component %q"
	errFmtRenderWorkload   = "cannot render workload for component %q"
	errFmtRenderTrait      = "cannot render trait for component %q"
	errFmtSetParam         = "cannot set parameter %q"
	errFmtUnsupportedParam = "unsupported parameter %q"
	errFmtRequiredParam    = "required parameter %q not specified"
	errFmtCompRevision     = "cannot get latest revision for component %q while revision is enabled"
)

var (
	// ErrDataOutputNotExist is an error indicating the DataOutput specified doesn't not exist
	ErrDataOutputNotExist = errors.New("DataOutput does not exist")
)

// A ComponentRenderer renders an ApplicationConfiguration's Components into
// workloads and traits.
type ComponentRenderer interface {
	Render(ctx context.Context, ac *v1alpha2.ApplicationConfiguration) ([]Workload, *v1alpha2.DependencyStatus, error)
}

// A ComponentRenderFn renders an ApplicationConfiguration's Components into
// workloads and traits.
type ComponentRenderFn func(ctx context.Context, ac *v1alpha2.ApplicationConfiguration) ([]Workload, *v1alpha2.DependencyStatus, error)

// Render an ApplicationConfiguration's Components into workloads and traits.
func (fn ComponentRenderFn) Render(ctx context.Context, ac *v1alpha2.ApplicationConfiguration) ([]Workload, *v1alpha2.DependencyStatus, error) {
	return fn(ctx, ac)
}

var _ ComponentRenderer = &components{}

type components struct {
	// indicate that if this is generated by application
	client   client.Reader
	dm       discoverymapper.DiscoveryMapper
	params   ParameterResolver
	workload ResourceRenderer
	trait    ResourceRenderer
}

func (r *components) Render(ctx context.Context, ac *v1alpha2.ApplicationConfiguration) ([]Workload, *v1alpha2.DependencyStatus, error) {
	workloads := make([]*Workload, 0, len(ac.Spec.Components))
	dag := newDAG()

	// we have special logics for application generated applicationConfiguration during rolling out phase
	rollingComponents := make(map[string]bool)
	var needRolloutTemplate bool
	if _, isAppRolling := ac.GetAnnotations()[oam.AnnotationAppRollout]; isAppRolling {
		// we only care about the new components when there is rolling out
		if anc, exist := ac.GetAnnotations()[oam.AnnotationRollingComponent]; exist {
			// the rolling components annotation contains all the rolling components in the application
			for _, componentName := range strings.Split(anc, common.RollingComponentsSep) {
				rollingComponents[componentName] = true
			}
		}
		// we need to do a template roll out if it's not done yet or forced
		needRolloutTemplate = ac.Status.RollingStatus != oamtype.RollingTemplated
		if needRolloutTemplate {
			klog.InfoS("need to template the ac ", "appConfig", klog.KRef(ac.Namespace, ac.Name),
				"rolling status", ac.Status.RollingStatus)
		}
	} else if ac.Status.RollingStatus == oamtype.RollingTemplated {
		klog.InfoS("mark the ac rolling status as completed", "appConfig", klog.KRef(ac.Namespace, ac.Name))
		ac.Status.RollingStatus = oamtype.RollingCompleted
	}

	for _, acc := range ac.Spec.Components {
		if acc.RevisionName != "" {
			acc.ComponentName = utils.ExtractComponentName(acc.RevisionName)
		}
		isComponentRolling := rollingComponents[acc.ComponentName]
		w, err := r.renderComponent(ctx, acc, ac, isControlledByApp(ac), isComponentRolling, needRolloutTemplate, dag)
		if err != nil {
			return nil, nil, err
		}
		workloads = append(workloads, w)
		// TODO: handle rolling status better when there are multiple components
		if isComponentRolling && needRolloutTemplate {
			ac.Status.RollingStatus = oamtype.RollingTemplating
		}
	}
	ds := &v1alpha2.DependencyStatus{}
	res := make([]Workload, 0, len(ac.Spec.Components))
	for i, acc := range ac.Spec.Components {
		unsatisfied, err := r.handleDependency(ctx, workloads[i], acc, dag, ac)
		if err != nil {
			return nil, nil, err
		}
		ds.Unsatisfied = append(ds.Unsatisfied, unsatisfied...)
		res = append(res, *workloads[i])
	}

	return res, ds, nil
}

func (r *components) renderComponent(ctx context.Context, acc v1alpha2.ApplicationConfigurationComponent,
	ac *v1alpha2.ApplicationConfiguration, isControlledByApp, isComponentRolling, needRolloutTemplate bool, dag *dag) (*Workload, error) {
	if acc.RevisionName != "" {
		acc.ComponentName = utils.ExtractComponentName(acc.RevisionName)
	}
	klog.InfoS("render a component", "component name", acc.ComponentName, "component revision", acc.RevisionName,
		"is generated by application", isControlledByApp, "is the component rolling", isComponentRolling,
		"is the appConfig a rollout template", needRolloutTemplate)

	c, componentRevisionName, err := util.GetComponent(ctx, r.client, acc, ac.GetNamespace())
	if err != nil {
		return nil, err
	}
	p, err := r.params.Resolve(c.Spec.Parameters, acc.ParameterValues)
	if err != nil {
		return nil, errors.Wrapf(err, errFmtResolveParams, acc.ComponentName)
	}

	w, err := r.workload.Render(c.Spec.Workload.Raw, p...)
	if err != nil {
		return nil, errors.Wrapf(err, errFmtRenderWorkload, acc.ComponentName)
	}

	compInfoLabels := map[string]string{
		oam.LabelAppName:              ac.Name,
		oam.LabelAppComponent:         acc.ComponentName,
		oam.LabelAppComponentRevision: componentRevisionName,
		oam.LabelOAMResourceType:      oam.ResourceTypeWorkload,
	}
	util.AddLabels(w, compInfoLabels)

	compInfoAnnotations := map[string]string{
		oam.AnnotationAppGeneration: strconv.Itoa(int(ac.Generation)),
	}
	util.AddAnnotations(w, compInfoAnnotations)
	var inplaceUpgrade string
	if acAnnotations := ac.GetAnnotations(); acAnnotations != nil {
		inplaceUpgrade = acAnnotations[oam.AnnotationInplaceUpgrade]
	}
	// pass through labels and annotation from app-config to workload
	util.PassLabelAndAnnotation(ac, w)
	// don't pass the following annotation as those are for appConfig only
	util.RemoveAnnotations(w, []string{oam.AnnotationAppRollout, oam.AnnotationRollingComponent, oam.AnnotationInplaceUpgrade})
	ref := getOwnerFromAC(ac)

	// Don't override if the resources already has namespace, it was set by user or the application controller which is by design.
	if len(w.GetNamespace()) == 0 {
		w.SetNamespace(ac.GetNamespace())
	}
	traits := make([]*Trait, 0, len(acc.Traits))
	traitDefs := make([]v1alpha2.TraitDefinition, 0, len(acc.Traits))
	compInfoLabels[oam.LabelOAMResourceType] = oam.ResourceTypeTrait

	for _, ct := range acc.Traits {
		t, traitDef, err := r.renderTrait(ctx, ct, ac, acc.ComponentName, ref, dag)
		if err != nil {
			return nil, err
		}
		util.AddLabels(t, compInfoLabels)
		util.AddAnnotations(t, compInfoAnnotations)

		// pass through labels and annotation from app-config to trait
		util.PassLabelAndAnnotation(ac, t)
		util.RemoveAnnotations(t, []string{oam.AnnotationAppRollout, oam.AnnotationRollingComponent, oam.AnnotationInplaceUpgrade})
		traits = append(traits, &Trait{Object: *t, Definition: *traitDef})
		traitDefs = append(traitDefs, *traitDef)
	}
	if !isControlledByApp {
		// This is the legacy standalone appConfig approach
		existingWorkload, err := r.getExistingWorkload(ctx, ac, c, w)
		if err != nil {
			return nil, err
		}
		if err := setWorkloadInstanceName(traitDefs, w, c, existingWorkload); err != nil {
			return nil, err
		}
	} else {
		// we have completely different approaches on workload name for application generated appConfig
		if c.Spec.Helm != nil {
			// for helm workload, make sure the workload is already generated by Helm successfully
			existingWorkloadByHelm, err := discoverHelmModuleWorkload(ctx, r.client, c, ac.GetNamespace())
			if err != nil {
				klog.ErrorS(err, "Could not get the workload created by Helm module",
					"component name", acc.ComponentName, "component revision", acc.RevisionName)
				return nil, errors.Wrap(err, "cannot get the workload created by a Helm module")
			}
			klog.InfoS("Successfully discovered the workload created by Helm",
				"component name", acc.ComponentName, "component revision", acc.RevisionName,
				"workload name", existingWorkloadByHelm.GetName())
			// use the name already generated instead of setting a new one
			w.SetName(existingWorkloadByHelm.GetName())
		} else {
			// for non-helm workload, we generate a workload name based on component name and revision
			revision, err := utils.ExtractRevision(acc.RevisionName)
			if err != nil {
				return nil, err
			}
			// Pass inpalce upgrade into it
			SetAppWorkloadInstanceName(acc.ComponentName, w, revision, inplaceUpgrade)
			if isComponentRolling && needRolloutTemplate {
				// we have a special logic to emit the workload as a template so that the rollout
				// controller can take over.
				// TODO: We might need to add the owner reference to the existing object in case the resource
				// is going to be shared (ie. CloneSet)
				if err := prepWorkloadInstanceForRollout(w); err != nil {
					return nil, err
				}
				// yield the controller to the rollout
				ref.Controller = pointer.BoolPtr(false)
				klog.InfoS("Successfully rendered a workload instance for rollout", "workload", w.GetName())
			}
		}
	}
	// set the owner reference after its ref is edited
	// If workload is in different namespace with application set the ownerReference, otherwise the owner was set with a resourceTracker by application controller already.
	if ac.GetNamespace() == w.GetNamespace() {
		w.SetOwnerReferences([]metav1.OwnerReference{*ref})
	}

	// create the ref after the workload name is set
	workloadRef := corev1.ObjectReference{
		APIVersion: w.GetAPIVersion(),
		Kind:       w.GetKind(),
		Name:       w.GetName(),
	}
	//  We only patch a TypedReference object to the trait if it asks for it
	for i := range acc.Traits {
		traitDef := traitDefs[i]
		trait := traits[i]
		workloadRefPath := traitDef.Spec.WorkloadRefPath
		if len(workloadRefPath) != 0 {
			if err := fieldpath.Pave(trait.Object.UnstructuredContent()).SetValue(workloadRefPath, workloadRef); err != nil {
				return nil, errors.Wrapf(err, errFmtSetWorkloadRef, trait.Object.GetName(), w.GetName())
			}
		}
	}
	scopes := make([]unstructured.Unstructured, 0, len(acc.Scopes))
	for _, cs := range acc.Scopes {
		scopeObject, err := r.renderScope(ctx, cs, ac.GetNamespace())
		if err != nil {
			return nil, err
		}

		scopes = append(scopes, *scopeObject)
	}

	addDataOutputsToDAG(dag, acc.DataOutputs, w)
	// To avoid conflict with rollout controller, we will not render the workload until the rollout phase is over
	// indicated by the AnnotationAppRollout annotation disappear
	return &Workload{ComponentName: acc.ComponentName, ComponentRevisionName: componentRevisionName,
		SkipApply: isComponentRolling && !needRolloutTemplate,
		Workload:  w, Traits: traits, RevisionEnabled: isRevisionEnabled(traitDefs), Scopes: scopes}, nil
}

func (r *components) renderTrait(ctx context.Context, ct v1alpha2.ComponentTrait, ac *v1alpha2.ApplicationConfiguration,
	componentName string, ref *metav1.OwnerReference, dag *dag) (*unstructured.Unstructured, *v1alpha2.TraitDefinition, error) {
	t, err := r.trait.Render(ct.Trait.Raw)
	if err != nil {
		return nil, nil, errors.Wrapf(err, errFmtRenderTrait, componentName)
	}
	traitDef, err := util.FetchTraitDefinition(ctx, r.client, r.dm, t)
	if err != nil {
		if !kerrors.IsNotFound(err) {
			return nil, nil, errors.Wrapf(err, errFmtGetTraitDefinition, t.GetAPIVersion(), t.GetKind(), t.GetName())
		}
		traitDef = util.GetDummyTraitDefinition(t)
	}

	traitName := getTraitName(ac, componentName, &ct, t, traitDef)
	setTraitProperties(t, traitName, ac.GetNamespace(), ref)

	addDataOutputsToDAG(dag, ct.DataOutputs, t)

	return t, traitDef, nil
}

func (r *components) renderScope(ctx context.Context, cs v1alpha2.ComponentScope, ns string) (*unstructured.Unstructured, error) {
	// Get Scope instance from k8s, since it is global and not a child resource of workflow.
	scopeObject := &unstructured.Unstructured{}
	scopeObject.SetAPIVersion(cs.ScopeReference.APIVersion)
	scopeObject.SetKind(cs.ScopeReference.Kind)
	scopeObjectRef := types.NamespacedName{Namespace: ns, Name: cs.ScopeReference.Name}
	if err := r.client.Get(ctx, scopeObjectRef, scopeObject); err != nil {
		return nil, errors.Wrapf(err, errFmtGetScope, cs.ScopeReference.Name)
	}
	return scopeObject, nil
}

func setTraitProperties(t *unstructured.Unstructured, traitName, namespace string, ref *metav1.OwnerReference) {
	// Set metadata name for `Trait` if the metadata name is NOT set.
	if t.GetName() == "" {
		t.SetName(traitName)
	}

	if controller := metav1.GetControllerOf(t); controller != nil {
		if controller.APIVersion == v1beta1.SchemeGroupVersion.String() &&
			controller.Kind == v1beta1.ResourceTrackerKind {
			// if a resource is controlled by a ResourceTracker,
			// it's cluster-scoped or in the different namespace with
			// application, so no need to check/set namespace
			return
		}
	}
	// Don't override if the resources already has namespace, it was set by user or the application controller which is by design.
	if len(t.GetNamespace()) == 0 {
		t.SetNamespace(namespace)
	}
	// If trait is in different namespace with application set the ownerReference, otherwise the owner was set with a resourceTracker by application controller already.
	if t.GetNamespace() == namespace {
		t.SetOwnerReferences([]metav1.OwnerReference{*ref})
	}

}

// setWorkloadInstanceName will set metadata.name for workload CR according to createRevision flag in traitDefinition
func setWorkloadInstanceName(traitDefs []v1alpha2.TraitDefinition, w *unstructured.Unstructured,
	c *v1alpha2.Component, existingWorkload *unstructured.Unstructured) error {
	// Don't override the specified name
	if w.GetName() != "" {
		return nil
	}
	// TODO: revisit this logic
	// the name of the workload should depend on the workload type and if we are rolling or replacing upgrade
	// i.e Cloneset type of workload just use the component name while deployment type of workload will have revision
	// if we are doing rolling upgrades. We can just override if we are replacing the deployment.
	if isRevisionEnabled(traitDefs) {
		if c.Status.LatestRevision == nil {
			return fmt.Errorf(errFmtCompRevision, c.Name)
		}

		componentLastRevision := c.Status.LatestRevision.Name
		// if workload exists, check the revision label, we will not change the name if workload exists and no revision changed
		if existingWorkload != nil && existingWorkload.GetLabels()[oam.LabelAppComponentRevision] == componentLastRevision {
			// using the existing name
			w.SetName(existingWorkload.GetName())
			return nil
		}

		// if revisionEnabled and the running workload's revision isn't equal to the component's latest reversion,
		// use revisionName as the workload name
		w.SetName(componentLastRevision)
		return nil
	}
	// use component name as workload name, which means we will always use one workload for different revisions
	w.SetName(c.GetName())
	return nil
}

// isRevisionEnabled will check if any of the traitDefinitions has a createRevision flag
func isRevisionEnabled(traitDefs []v1alpha2.TraitDefinition) bool {
	for _, td := range traitDefs {
		if td.Spec.RevisionEnabled {
			return true
		}
	}
	return false
}

// A ResourceRenderer renders a Kubernetes-compliant YAML resource into an
// Unstructured object, optionally setting the supplied parameters.
type ResourceRenderer interface {
	Render(data []byte, p ...Parameter) (*unstructured.Unstructured, error)
}

// A ResourceRenderFn renders a Kubernetes-compliant YAML resource into an
// Unstructured object, optionally setting the supplied parameters.
type ResourceRenderFn func(data []byte, p ...Parameter) (*unstructured.Unstructured, error)

// Render the supplied Kubernetes YAML resource.
func (fn ResourceRenderFn) Render(data []byte, p ...Parameter) (*unstructured.Unstructured, error) {
	return fn(data, p...)
}

func renderWorkload(data []byte, p ...Parameter) (*unstructured.Unstructured, error) {
	// TODO(negz): Is there a better decoder to use here?
	w := &fieldpath.Paved{}
	if err := json.Unmarshal(data, w); err != nil {
		return nil, errors.Wrap(err, errUnmarshalWorkload)
	}

	for _, param := range p {
		for _, path := range param.FieldPaths {
			// TODO(negz): Infer parameter type from workload OpenAPI schema.
			switch param.Value.Type {
			case intstr.String:
				if err := w.SetString(path, param.Value.StrVal); err != nil {
					return nil, errors.Wrapf(err, errFmtSetParam, param.Name)
				}
			case intstr.Int:
				if err := w.SetNumber(path, float64(param.Value.IntVal)); err != nil {
					return nil, errors.Wrapf(err, errFmtSetParam, param.Name)
				}
			}
		}
	}

	return &unstructured.Unstructured{Object: w.UnstructuredContent()}, nil
}

func renderTrait(data []byte, _ ...Parameter) (*unstructured.Unstructured, error) {
	// TODO(negz): Is there a better decoder to use here?
	u := &unstructured.Unstructured{}
	if err := json.Unmarshal(data, u); err != nil {
		return nil, errors.Wrap(err, errUnmarshalTrait)
	}
	return u, nil
}

// A Parameter may be used to set the supplied paths to the supplied value.
type Parameter struct {
	// Name of this parameter.
	Name string

	// Value of this parameter.
	Value intstr.IntOrString

	// FieldPaths that should be set to this parameter's value.
	FieldPaths []string
}

// A ParameterResolver resolves the parameters accepted by a component and the
// parameter values supplied to a component into configured parameters.
type ParameterResolver interface {
	Resolve([]v1alpha2.ComponentParameter, []v1alpha2.ComponentParameterValue) ([]Parameter, error)
}

// A ParameterResolveFn resolves the parameters accepted by a component and the
// parameter values supplied to a component into configured parameters.
type ParameterResolveFn func([]v1alpha2.ComponentParameter, []v1alpha2.ComponentParameterValue) ([]Parameter, error)

// Resolve the supplied parameters.
func (fn ParameterResolveFn) Resolve(cp []v1alpha2.ComponentParameter, cpv []v1alpha2.ComponentParameterValue) ([]Parameter, error) {
	return fn(cp, cpv)
}

func resolve(cp []v1alpha2.ComponentParameter, cpv []v1alpha2.ComponentParameterValue) ([]Parameter, error) {
	supported := make(map[string]bool)
	for _, v := range cp {
		supported[v.Name] = true
	}

	set := make(map[string]*Parameter)
	for _, v := range cpv {
		if !supported[v.Name] {
			return nil, errors.Errorf(errFmtUnsupportedParam, v.Name)
		}
		set[v.Name] = &Parameter{Name: v.Name, Value: v.Value}
	}

	for _, p := range cp {
		_, ok := set[p.Name]
		if !ok && p.Required != nil && *p.Required {
			// This parameter is required, but not set.
			return nil, errors.Errorf(errFmtRequiredParam, p.Name)
		}
		if !ok {
			// This parameter is not required, and not set.
			continue
		}

		set[p.Name].FieldPaths = p.FieldPaths
	}

	params := make([]Parameter, 0, len(set))
	for _, p := range set {
		params = append(params, *p)
	}

	return params, nil
}

func addDataOutputsToDAG(dag *dag, outs []v1alpha2.DataOutput, obj *unstructured.Unstructured) {
	for _, out := range outs {
		r := &corev1.ObjectReference{
			APIVersion: obj.GetAPIVersion(),
			Kind:       obj.GetKind(),
			Name:       obj.GetName(),
			Namespace:  obj.GetNamespace(),
			FieldPath:  out.FieldPath,
		}
		dag.AddSource(out.Name, r, out.Conditions)
	}
}

func (r *components) handleDependency(ctx context.Context, w *Workload, acc v1alpha2.ApplicationConfigurationComponent, dag *dag, ac *v1alpha2.ApplicationConfiguration) ([]v1alpha2.UnstaifiedDependency, error) {
	uds := make([]v1alpha2.UnstaifiedDependency, 0)
	unstructuredAC, err := util.Object2Unstructured(ac)
	if err != nil {
		return nil, errors.Wrapf(err, "handleDataInput by convert AppConfig (%s) to unstructured object failed", ac.Name)
	}
	// Record the dataOutput with ready conditions
	var unsatisfied []v1alpha2.UnstaifiedDependency
	unsatisfied, w.DataOutputs = r.handleDataOutput(ctx, acc.DataOutputs, dag, unstructuredAC)
	if len(unsatisfied) != 0 {
		uds = append(uds, unsatisfied...)
	}
	unsatisfied, err = r.handleDataInput(ctx, acc.DataInputs, dag, w.Workload, unstructuredAC)
	if err != nil {
		return nil, errors.Wrapf(err, "handleDataInput for workload (%s/%s) failed", w.Workload.GetNamespace(), w.Workload.GetName())
	}
	if len(unsatisfied) != 0 {
		uds = append(uds, unsatisfied...)
		w.HasDep = true
	} else {
		w.DataInputs = acc.DataInputs
	}

	for i, ct := range acc.Traits {
		trait := w.Traits[i]
		unsatisfied, trait.DataOutputs = r.handleDataOutput(ctx, ct.DataOutputs, dag, unstructuredAC)
		if len(unsatisfied) != 0 {
			uds = append(uds, unsatisfied...)
		}
		unsatisfied, err := r.handleDataInput(ctx, ct.DataInputs, dag, &trait.Object, unstructuredAC)
		if err != nil {
			return nil, errors.Wrapf(err, "handleDataInput for trait (%s/%s) failed", trait.Object.GetNamespace(), trait.Object.GetName())
		}
		if len(unsatisfied) != 0 {
			uds = append(uds, unsatisfied...)
			trait.HasDep = true
		} else {
			trait.DataInputs = ct.DataInputs
		}
	}
	return uds, nil
}

func makeUnsatisfiedDependency(obj *unstructured.Unstructured, s *dagSource, toPaths []string, reason string) v1alpha2.UnstaifiedDependency {
	return v1alpha2.UnstaifiedDependency{
		Reason: reason,
		From: v1alpha2.DependencyFromObject{
			ObjectReference: corev1.ObjectReference{
				APIVersion: s.ObjectRef.APIVersion,
				Kind:       s.ObjectRef.Kind,
				Name:       s.ObjectRef.Name,
			},
			FieldPath: s.ObjectRef.FieldPath,
		},
		To: v1alpha2.DependencyToObject{
			ObjectReference: corev1.ObjectReference{
				APIVersion: obj.GetAPIVersion(),
				Kind:       obj.GetKind(),
				Name:       obj.GetName(),
			},
			FieldPaths: toPaths,
		},
	}
}

func (r *components) handleDataOutput(ctx context.Context, outputs []v1alpha2.DataOutput, dag *dag, ac *unstructured.Unstructured) ([]v1alpha2.UnstaifiedDependency, map[string]v1alpha2.DataOutput) {
	uds := make([]v1alpha2.UnstaifiedDependency, 0)
	outputMap := make(map[string]v1alpha2.DataOutput)
	for _, out := range outputs {
		if reflect.DeepEqual(out.OutputStore, v1alpha2.StoreReference{}) {
			continue
		}
		s, ok := dag.Sources[out.Name]
		if !ok {
			continue
		}
		// the outputStore is considered ready when all conditions are ready
		allConditionsReady := true
		for _, oper := range out.OutputStore.Operations {
			newS := &dagSource{
				ObjectRef: &corev1.ObjectReference{
					APIVersion: s.ObjectRef.APIVersion,
					Kind:       s.ObjectRef.Kind,
					Name:       s.ObjectRef.Name,
					Namespace:  ac.GetNamespace(),
					FieldPath:  oper.ValueFrom.FieldPath,
				},
				Conditions: oper.Conditions,
			}
			_, ready, reason, err := r.getDataInput(ctx, newS, ac, false)
			if err != nil || !ready {
				if err == nil {
					outObj := &unstructured.Unstructured{}
					outObj.SetGroupVersionKind(out.OutputStore.GroupVersionKind())
					outObj.SetName(out.OutputStore.Name)
					toPath := oper.ToFieldPath
					if len(oper.ToDataPath) != 0 {
						toPath = toPath + "(" + oper.ToDataPath + ")"
					}
					uds = append(uds, makeUnsatisfiedDependency(outObj, newS, []string{toPath}, reason))
				}
				allConditionsReady = false
				break
			}
		}
		if allConditionsReady {
			outputMap[out.Name] = out
		}
	}
	return uds, outputMap
}

func (r *components) handleDataInput(ctx context.Context, ins []v1alpha2.DataInput, dag *dag, obj, ac *unstructured.Unstructured) ([]v1alpha2.UnstaifiedDependency, error) {
	uds := make([]v1alpha2.UnstaifiedDependency, 0)
	for _, in := range ins {
		if !reflect.DeepEqual(in.ValueFrom, v1alpha2.DataInputValueFrom{}) && len(strings.TrimSpace(in.ValueFrom.DataOutputName)) != 0 {
			dep, err := r.handleDataOutputConds(ctx, in, dag, obj, ac)
			if dep != nil {
				uds = append(uds, *dep)
				return uds, err
			}
			if err != nil {
				return nil, err
			}
		}
		if !reflect.DeepEqual(in.InputStore, v1alpha2.StoreReference{}) {
			dep, err := r.handleDataStoreConds(ctx, in, obj, ac)
			if dep != nil {
				uds = append(uds, *dep)
				return uds, err
			}
			if err != nil {
				return nil, err
			}
		}

		if len(in.Conditions) != 0 {
			dep, err := r.handleDataInputConds(ctx, in, dag, obj, ac)
			if dep != nil {
				uds = append(uds, *dep)
				return uds, err
			}
			if err != nil {
				return nil, err
			}
		}
	}
	return uds, nil
}

func (r *components) handleDataOutputConds(ctx context.Context, in v1alpha2.DataInput, dag *dag, obj, ac *unstructured.Unstructured) (*v1alpha2.UnstaifiedDependency, error) {
	s, ok := dag.Sources[in.ValueFrom.DataOutputName]
	if !ok {
		return nil, errors.Wrapf(ErrDataOutputNotExist, "DataOutputName (%s)", in.ValueFrom.DataOutputName)
	}
	val, ready, reason, err := r.getDataInput(ctx, s, ac, false)
	if err != nil {
		return nil, errors.Wrap(err, "getDataInput failed")
	}
	if !ready {
		dep := makeUnsatisfiedDependency(obj, s, in.ToFieldPaths, reason)
		return &dep, nil
	}
	err = fillDataInputValue(obj, in.ToFieldPaths, val, in.StrategyMergeKeys)
	if err != nil {
		return nil, errors.Wrap(err, "fillDataInputValue failed")
	}
	return nil, nil
}

func (r *components) handleDataStoreConds(ctx context.Context, in v1alpha2.DataInput, obj, ac *unstructured.Unstructured) (*v1alpha2.UnstaifiedDependency, error) {
	for _, oper := range in.InputStore.Operations {
		s := &dagSource{
			ObjectRef: &corev1.ObjectReference{
				APIVersion: in.InputStore.APIVersion,
				Kind:       in.InputStore.Kind,
				Name:       in.InputStore.Name,
				// according current implementation, outputRef use the namespace of workload which is set with the namespace of ac. So it's ok to use ac.GetNamespace() here.
				// obj.GetNamespace() may be empty when obj has not been created.
				Namespace: ac.GetNamespace(),
				FieldPath: oper.ValueFrom.FieldPath,
			},
			Conditions: oper.Conditions,
		}
		_, ready, reason, err := r.getDataInput(ctx, s, ac, false)
		if err != nil {
			return nil, errors.Wrap(err, "getDataInput failed")
		}
		if !ready {
			toPath := oper.ToFieldPath
			if len(oper.ToDataPath) != 0 {
				toPath = toPath + "(" + oper.ToDataPath + ")"
			}
			dep := makeUnsatisfiedDependency(obj, s, []string{toPath}, reason)
			return &dep, nil
		}
	}
	return nil, nil
}
func (r *components) handleDataInputConds(ctx context.Context, in v1alpha2.DataInput, dag *dag, obj, ac *unstructured.Unstructured) (*v1alpha2.UnstaifiedDependency, error) {
	_, ok := dag.Sources[in.ValueFrom.DataOutputName]
	if !ok {
		return nil, errors.Wrapf(ErrDataOutputNotExist, "DataOutputName (%s)", in.ValueFrom.DataOutputName)
	}
	s := &dagSource{
		ObjectRef: &corev1.ObjectReference{
			APIVersion: obj.GetAPIVersion(),
			Kind:       obj.GetKind(),
			Name:       obj.GetName(),
			// according current implementation, outputRef use the namespace of workload which is set with the namespace of ac. So it's ok to use ac.GetNamespace() here.
			// obj.GetNamespace() may be empty when obj has not been created.
			Namespace: ac.GetNamespace(),
		},
		Conditions: in.Conditions,
	}
	_, ready, reason, err := r.getDataInput(ctx, s, ac, true)
	if err != nil {
		return nil, errors.Wrap(err, "getDataInput failed")
	}
	if !ready {
		dep := makeUnsatisfiedDependency(obj, dag.Sources[in.ValueFrom.DataOutputName], in.ToFieldPaths, "DataInputs Conditions: "+reason)
		return &dep, nil
	}
	return nil, nil
}

func (r *components) getDataInput(ctx context.Context, s *dagSource, ac *unstructured.Unstructured, ignoreNotFound bool) (interface{}, bool, string, error) {
	obj := s.ObjectRef
	key := types.NamespacedName{
		Namespace: obj.Namespace,
		Name:      obj.Name,
	}
	// If obj.FieldPath is empty and the length of dagSource's Conditions is 0, return true
	if len(obj.FieldPath) == 0 && len(s.Conditions) == 0 {
		return nil, true, "", nil
	}
	u := &unstructured.Unstructured{}
	u.SetGroupVersionKind(obj.GroupVersionKind())
	err := r.client.Get(ctx, key, u)
	if err != nil {
		if resource.IgnoreNotFound(err) == nil && ignoreNotFound {
			return nil, true, "", nil
		}
		reason := fmt.Sprintf("failed to get object (%s)", key.String())
		return nil, false, reason, errors.Wrap(resource.IgnoreNotFound(err), reason)
	}
	paved := fieldpath.Pave(u.UnstructuredContent())
	pavedAC := fieldpath.Pave(ac.UnstructuredContent())
	rawval, err := paved.GetValue(obj.FieldPath)
	if err != nil {
		if fieldpath.IsNotFound(err) {
			return "", false, fmt.Sprintf("%s not found in object", obj.FieldPath), nil
		}
		err = fmt.Errorf("failed to get field value (%s) in object (%s): %w", obj.FieldPath, key.String(), err)
		return nil, false, err.Error(), err
	}

	var ok bool
	var reason string
	switch val := rawval.(type) {
	case string:
		// For string input we will:
		// - check its value not empty if no condition is given.
		// - check its value against conditions if no field path is specified.
		ok, reason = matchValue(s.Conditions, val, paved, pavedAC)
	default:
		ok, reason = checkConditions(s.Conditions, paved, nil, pavedAC)
	}
	if !ok {
		return nil, false, reason, nil
	}

	return rawval, true, "", nil
}

func isControlledByApp(ac *v1alpha2.ApplicationConfiguration) bool {
	for _, owner := range ac.GetOwnerReferences() {
		if owner.APIVersion == v1beta1.SchemeGroupVersion.String() && owner.Kind == v1beta1.ApplicationKind &&
			owner.Controller != nil && *owner.Controller {
			return true
		}
	}
	return false
}

// getOwnerFromAC will check and get the real owner, if the owner is Application, it will use ApplicationContext as owner
// or it will make the AC as the owner
func getOwnerFromAC(ac *v1alpha2.ApplicationConfiguration) *metav1.OwnerReference {
	return metav1.NewControllerRef(ac, v1alpha2.ApplicationConfigurationGroupVersionKind)
}

func matchValue(conds []v1alpha2.ConditionRequirement, val string, paved, ac *fieldpath.Paved) (bool, string) {
	// If no condition is specified, it is by default to check value not empty.
	if len(conds) == 0 {
		if val == "" {
			return false, "value should not be empty"
		}
		return true, ""
	}

	return checkConditions(conds, paved, &val, ac)
}

func getCheckVal(m v1alpha2.ConditionRequirement, paved *fieldpath.Paved, val *string) (string, error) {
	var checkVal string
	switch {
	case m.FieldPath != "":
		return paved.GetString(m.FieldPath)
	case val != nil:
		checkVal = *val
	default:
		return "", errors.New("FieldPath not specified")
	}
	return checkVal, nil
}

func getExpectVal(m v1alpha2.ConditionRequirement, ac *fieldpath.Paved) (string, error) {
	if m.Value != "" {
		return m.Value, nil
	}
	if m.ValueFrom.FieldPath == "" || ac == nil {
		return "", nil
	}
	var err error
	value, err := ac.GetString(m.ValueFrom.FieldPath)
	if err != nil {
		return "", fmt.Errorf("get valueFrom.fieldPath fail: %w", err)
	}
	return value, nil
}

func checkConditions(conds []v1alpha2.ConditionRequirement, paved *fieldpath.Paved, val *string, ac *fieldpath.Paved) (bool, string) {
	for _, m := range conds {
		checkVal, err := getCheckVal(m, paved, val)
		if err != nil {
			return false, fmt.Sprintf("can't get value to check %v", err)
		}
		m.Value, err = getExpectVal(m, ac)
		if err != nil {
			return false, err.Error()
		}

		switch m.Operator {
		case v1alpha2.ConditionEqual:
			if m.Value != checkVal {
				return false, fmt.Sprintf("got(%v) expected to be %v", checkVal, m.Value)
			}
		case v1alpha2.ConditionNotEqual:
			if m.Value == checkVal {
				return false, fmt.Sprintf("got(%v) expected not to be %v", checkVal, m.Value)
			}
		case v1alpha2.ConditionNotEmpty:
			if checkVal == "" {
				return false, "value should not be empty"
			}
		}
	}
	return true, ""
}

// GetTraitName return trait name
func getTraitName(ac *v1alpha2.ApplicationConfiguration, componentName string,
	ct *v1alpha2.ComponentTrait, t *unstructured.Unstructured, traitDef *v1alpha2.TraitDefinition) string {
	var traitName, apiVersion, kind string
	// we forbid the trait name in the template if the applicationConfiguration is generated by application
	if len(t.GetName()) > 0 && !isControlledByApp(ac) {
		return t.GetName()
	}

	apiVersion = t.GetAPIVersion()
	kind = t.GetKind()

	traitType := traitDef.Name
	if strings.Contains(traitType, ".") {
		traitType = strings.Split(traitType, ".")[0]
	}

	for _, w := range ac.Status.Workloads {
		if w.ComponentName != componentName {
			continue
		}
		for _, trait := range w.Traits {
			if trait.Reference.APIVersion == apiVersion && trait.Reference.Kind == kind {
				traitName = trait.Reference.Name
			}
		}
	}

	if len(traitName) == 0 {
		traitName = util.GenTraitName(componentName, ct.DeepCopy(), traitType)
	}

	return traitName
}

// getExistingWorkload tries to retrieve the currently running workload
func (r *components) getExistingWorkload(ctx context.Context, ac *v1alpha2.ApplicationConfiguration, c *v1alpha2.Component, w *unstructured.Unstructured) (*unstructured.Unstructured, error) {
	var workloadName string
	existingWorkload := &unstructured.Unstructured{}
	for _, component := range ac.Status.Workloads {
		if component.ComponentName != c.GetName() {
			continue
		}
		workloadName = component.Reference.Name
	}
	if workloadName != "" {
		objectKey := client.ObjectKey{Namespace: ac.GetNamespace(), Name: workloadName}
		existingWorkload.SetAPIVersion(w.GetAPIVersion())
		existingWorkload.SetKind(w.GetKind())
		err := r.client.Get(ctx, objectKey, existingWorkload)
		if err != nil {
			return nil, client.IgnoreNotFound(err)
		}
	}
	return existingWorkload, nil
}

// discoverHelmModuleWorkload will get the workload created by flux/helm-controller
func discoverHelmModuleWorkload(ctx context.Context, c client.Reader, comp *v1alpha2.Component, ns string) (*unstructured.Unstructured, error) {
	if comp == nil || comp.Spec.Helm == nil {
		return nil, errors.New("the component has no valid helm module")
	}

	rls, err := util.RawExtension2Unstructured(&comp.Spec.Helm.Release)
	if err != nil {
		return nil, errors.Wrap(err, "cannot get helm release from component")
	}
	rlsName := rls.GetName()

	chartName, ok, err := unstructured.NestedString(rls.Object, helmapi.HelmChartNamePath...)
	if err != nil || !ok {
		return nil, errors.New("cannot get helm chart name")
	}

	// qualifiedFullName is used as the name of target workload.
	// It strictly follows the convention that Helm generate default full name as below:
	// > We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
	// > If release name contains chart name it will be used as a full name.
	qualifiedWorkloadName := rlsName
	if !strings.Contains(rlsName, chartName) {
		qualifiedWorkloadName = fmt.Sprintf("%s-%s", rlsName, chartName)
		if len(qualifiedWorkloadName) > 63 {
			qualifiedWorkloadName = strings.TrimSuffix(qualifiedWorkloadName[:63], "-")
		}
	}

	wl, err := util.RawExtension2Unstructured(&comp.Spec.Workload)
	if err != nil {
		return nil, errors.Wrap(err, "cannot get workload from component")
	}

	if err := c.Get(ctx, client.ObjectKey{Namespace: ns, Name: qualifiedWorkloadName}, wl); err != nil {
		return nil, err
	}

	// check it's created by helm and match the release info
	annots := wl.GetAnnotations()
	labels := wl.GetLabels()
	if annots == nil || labels == nil ||
		annots["meta.helm.sh/release-name"] != rlsName ||
		annots["meta.helm.sh/release-namespace"] != ns ||
		labels["app.kubernetes.io/managed-by"] != "Helm" {
		err := fmt.Errorf("the workload is found but not match with helm info(meta.helm.sh/release-name: %s, meta.helm.sh/namespace: %s, app.kubernetes.io/managed-by: Helm)",
			rlsName, ns)
		klog.ErrorS(err, "Found a name-matched workload but not managed by Helm", "name", qualifiedWorkloadName,
			"annotations", annots, "labels", labels)
		return nil, err
	}

	return wl, nil
}
