package resourcemerge

import (
	errorsstdlib "errors"
	"fmt"
	"reflect"
	"strings"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
)

// EnsureObjectMeta writes namespace, name, labels, and annotations.  Don't set other things here.
// TODO finalizer support maybe?
func EnsureObjectMeta(modified *bool, existing *metav1.ObjectMeta, required metav1.ObjectMeta) {
	SetStringIfSet(modified, &existing.Namespace, required.Namespace)
	SetStringIfSet(modified, &existing.Name, required.Name)
	MergeMap(modified, &existing.Labels, required.Labels)
	MergeMap(modified, &existing.Annotations, required.Annotations)
	MergeOwnerRefs(modified, &existing.OwnerReferences, required.OwnerReferences)
}

func EnsureObjectMetaForUnstructured(modified *bool, existing *unstructured.Unstructured, required *unstructured.Unstructured) error {

	// Ensure metadata field is present on the object.
	existingObjectMeta, found, err := unstructured.NestedMap(existing.Object, "metadata")
	if err != nil {
		return err
	}
	if !found {
		return errorsstdlib.New(fmt.Sprintf("metadata not found in the existing object: %s/%s", existing.GetNamespace(), existing.GetName()))
	}
	var requiredObjectMeta map[string]interface{}
	requiredObjectMeta, found, err = unstructured.NestedMap(required.Object, "metadata")
	if err != nil {
		return err
	}
	if !found {
		return errorsstdlib.New(fmt.Sprintf("metadata not found in the required object: %s/%s", required.GetNamespace(), required.GetName()))
	}

	// Cast the metadata to the correct type.
	var existingObjectMetaTyped, requiredObjectMetaTyped metav1.ObjectMeta
	err = runtime.DefaultUnstructuredConverter.FromUnstructured(existingObjectMeta, &existingObjectMetaTyped)
	if err != nil {
		return err
	}
	err = runtime.DefaultUnstructuredConverter.FromUnstructured(requiredObjectMeta, &requiredObjectMetaTyped)
	if err != nil {
		return err
	}

	// Check if the metadata objects differ. This only checks for selective fields (excluding the resource version, among others).
	EnsureObjectMeta(modified, &existingObjectMetaTyped, requiredObjectMetaTyped)
	if *modified {
		existing.Object["metadata"], err = runtime.DefaultUnstructuredConverter.ToUnstructured(&existingObjectMetaTyped)
		if err != nil {
			return err
		}
	}

	return nil
}

// WithCleanLabelsAndAnnotations cleans the metadata off the removal annotations/labels/ownerrefs
// (those that end with trailing "-")
func WithCleanLabelsAndAnnotations(obj metav1.Object) metav1.Object {
	obj.SetAnnotations(cleanRemovalKeys(obj.GetAnnotations()))
	obj.SetLabels(cleanRemovalKeys(obj.GetLabels()))
	obj.SetOwnerReferences(cleanRemovalOwnerRefs(obj.GetOwnerReferences()))
	return obj
}

func cleanRemovalKeys(required map[string]string) map[string]string {
	for k := range required {
		if strings.HasSuffix(k, "-") {
			delete(required, k)
		}
	}
	return required
}

func SetString(modified *bool, existing *string, required string) {
	if required != *existing {
		*existing = required
		*modified = true
	}
}

func SetStringIfSet(modified *bool, existing *string, required string) {
	if len(required) == 0 {
		return
	}
	if required != *existing {
		*existing = required
		*modified = true
	}
}

func SetStringSlice(modified *bool, existing *[]string, required []string) {
	if !reflect.DeepEqual(required, *existing) {
		*existing = required
		*modified = true
	}
}

func SetStringSliceIfSet(modified *bool, existing *[]string, required []string) {
	if required == nil {
		return
	}
	if !reflect.DeepEqual(required, *existing) {
		*existing = required
		*modified = true
	}
}

// Deprecated: Use k8s.io/utils/ptr.To instead.
func BoolPtr(val bool) *bool {
	return &val
}

func SetBool(modified *bool, existing *bool, required bool) {
	if required != *existing {
		*existing = required
		*modified = true
	}
}

func SetInt32(modified *bool, existing *int32, required int32) {
	if required != *existing {
		*existing = required
		*modified = true
	}
}

func SetInt32IfSet(modified *bool, existing *int32, required int32) {
	if required == 0 {
		return
	}

	SetInt32(modified, existing, required)
}

func SetInt64(modified *bool, existing *int64, required int64) {
	if required != *existing {
		*existing = required
		*modified = true
	}
}

func MergeMap(modified *bool, existing *map[string]string, required map[string]string) {
	if *existing == nil {
		*existing = map[string]string{}
	}
	for k, v := range required {
		actualKey := k
		removeKey := false

		// if "required" map contains a key with "-" as suffix, remove that
		// key from the existing map instead of replacing the value
		if strings.HasSuffix(k, "-") {
			removeKey = true
			actualKey = strings.TrimRight(k, "-")
		}

		if existingV, ok := (*existing)[actualKey]; removeKey {
			if !ok {
				continue
			}
			// value found -> it should be removed
			delete(*existing, actualKey)
			*modified = true

		} else if !ok || v != existingV {
			*modified = true
			(*existing)[actualKey] = v
		}
	}
}

func SetMapStringString(modified *bool, existing *map[string]string, required map[string]string) {
	if *existing == nil {
		*existing = map[string]string{}
	}

	if !reflect.DeepEqual(*existing, required) {
		*existing = required
	}
}

func SetMapStringStringIfSet(modified *bool, existing *map[string]string, required map[string]string) {
	if required == nil {
		return
	}
	if *existing == nil {
		*existing = map[string]string{}
	}

	if !reflect.DeepEqual(*existing, required) {
		*existing = required
	}
}

func MergeOwnerRefs(modified *bool, existing *[]metav1.OwnerReference, required []metav1.OwnerReference) {
	if *existing == nil {
		*existing = []metav1.OwnerReference{}
	}

	for _, o := range required {
		removeOwner := false

		// if "required" ownerRefs contain an owner.UID with "-" as suffix, remove that
		// ownerRef from the existing ownerRefs instead of replacing the value
		// NOTE: this is the same format as kubectl annotate and kubectl label
		if strings.HasSuffix(string(o.UID), "-") {
			removeOwner = true
		}

		existedIndex := 0

		for existedIndex < len(*existing) {
			if ownerRefMatched(o, (*existing)[existedIndex]) {
				break
			}
			existedIndex++
		}

		if existedIndex == len(*existing) {
			// There is no matched ownerref found, append the ownerref
			// if it is not to be removed.
			if !removeOwner {
				*existing = append(*existing, o)
				*modified = true
			}
			continue
		}

		if removeOwner {
			*existing = append((*existing)[:existedIndex], (*existing)[existedIndex+1:]...)
			*modified = true
			continue
		}

		if !reflect.DeepEqual(o, (*existing)[existedIndex]) {
			(*existing)[existedIndex] = o
			*modified = true
		}
	}
}

func ownerRefMatched(existing, required metav1.OwnerReference) bool {
	if existing.Name != required.Name {
		return false
	}

	if existing.Kind != required.Kind {
		return false
	}

	existingGV, err := schema.ParseGroupVersion(existing.APIVersion)

	if err != nil {
		return false
	}

	requiredGV, err := schema.ParseGroupVersion(required.APIVersion)

	if err != nil {
		return false
	}

	if existingGV.Group != requiredGV.Group {
		return false
	}

	return true
}

func cleanRemovalOwnerRefs(required []metav1.OwnerReference) []metav1.OwnerReference {
	for k := 0; k < len(required); k++ {
		if strings.HasSuffix(string(required[k].UID), "-") {
			required = append(required[:k], required[k+1:]...)
			k--
		}
	}
	return required
}
