package util

import (
	"fmt"
	"reflect"

	kmeta "k8s.io/kubernetes/pkg/api/meta"
	"k8s.io/kubernetes/pkg/labels"
	"k8s.io/kubernetes/pkg/runtime"

	deployapi "github.com/openshift/origin/pkg/deploy/api"
)

// MergeInto flags
const (
	OverwriteExistingDstKey = 1 << iota
	ErrorOnExistingDstKey
	ErrorOnDifferentDstKeyValue
)

// AddObjectLabels adds new label(s) to a single runtime.Object
func AddObjectLabels(obj runtime.Object, labels labels.Set) error {
	if labels == nil {
		return nil
	}

	accessor, err := kmeta.Accessor(obj)

	if err != nil {
		if _, ok := obj.(*runtime.Unstructured); !ok {
			// error out if it's not possible to get an accessor and it's also not an unstructured object
			return err
		}
	} else {
		metaLabels := accessor.GetLabels()
		if metaLabels == nil {
			metaLabels = make(map[string]string)
		}

		switch objType := obj.(type) {
		case *deployapi.DeploymentConfig:
			if err := addDeploymentConfigNestedLabels(objType, labels); err != nil {
				return fmt.Errorf("unable to add nested labels to %s/%s: %v", obj.GetObjectKind().GroupVersionKind(), accessor.GetName(), err)
			}
		}

		if err := MergeInto(metaLabels, labels, OverwriteExistingDstKey); err != nil {
			return fmt.Errorf("unable to add labels to %s/%s: %v", obj.GetObjectKind().GroupVersionKind(), accessor.GetName(), err)
		}

		accessor.SetLabels(metaLabels)

		return nil
	}

	// handle unstructured object
	// TODO: allow meta.Accessor to handle runtime.Unstructured
	if unstruct, ok := obj.(*runtime.Unstructured); ok && unstruct.Object != nil {
		// the presence of "metadata" is sufficient for us to apply the rules for Kube-like
		// objects.
		// TODO: add swagger detection to allow this to happen more effectively
		if obj, ok := unstruct.Object["metadata"]; ok {
			if m, ok := obj.(map[string]interface{}); ok {

				existing := make(map[string]string)
				if l, ok := m["labels"]; ok {
					if found, ok := interfaceToStringMap(l); ok {
						existing = found
					}
				}
				if err := MergeInto(existing, labels, OverwriteExistingDstKey); err != nil {
					return err
				}
				m["labels"] = mapToGeneric(existing)
			}
			return nil
		}

		// only attempt to set root labels if a root object called labels exists
		// TODO: add swagger detection to allow this to happen more effectively
		if obj, ok := unstruct.Object["labels"]; ok {
			existing := make(map[string]string)
			if found, ok := interfaceToStringMap(obj); ok {
				existing = found
			}
			if err := MergeInto(existing, labels, OverwriteExistingDstKey); err != nil {
				return err
			}
			unstruct.Object["labels"] = mapToGeneric(existing)
			return nil
		}
	}

	return nil
}

// AddObjectAnnotations adds new annotation(s) to a single runtime.Object
func AddObjectAnnotations(obj runtime.Object, annotations map[string]string) error {
	if len(annotations) == 0 {
		return nil
	}

	accessor, err := kmeta.Accessor(obj)

	if err != nil {
		if _, ok := obj.(*runtime.Unstructured); !ok {
			// error out if it's not possible to get an accessor and it's also not an unstructured object
			return err
		}
	} else {
		metaAnnotations := accessor.GetAnnotations()
		if metaAnnotations == nil {
			metaAnnotations = make(map[string]string)
		}

		switch objType := obj.(type) {
		case *deployapi.DeploymentConfig:
			if err := addDeploymentConfigNestedAnnotations(objType, annotations); err != nil {
				return fmt.Errorf("unable to add nested annotations to %s/%s: %v", obj.GetObjectKind().GroupVersionKind(), accessor.GetName(), err)
			}
		}

		MergeInto(metaAnnotations, annotations, OverwriteExistingDstKey)
		accessor.SetAnnotations(metaAnnotations)

		return nil
	}

	// handle unstructured object
	// TODO: allow meta.Accessor to handle runtime.Unstructured
	if unstruct, ok := obj.(*runtime.Unstructured); ok && unstruct.Object != nil {
		// the presence of "metadata" is sufficient for us to apply the rules for Kube-like
		// objects.
		// TODO: add swagger detection to allow this to happen more effectively
		if obj, ok := unstruct.Object["metadata"]; ok {
			if m, ok := obj.(map[string]interface{}); ok {

				existing := make(map[string]string)
				if l, ok := m["annotations"]; ok {
					if found, ok := interfaceToStringMap(l); ok {
						existing = found
					}
				}
				if err := MergeInto(existing, annotations, OverwriteExistingDstKey); err != nil {
					return err
				}
				m["annotations"] = mapToGeneric(existing)
			}
			return nil
		}

		// only attempt to set root annotations if a root object called annotations exists
		// TODO: add swagger detection to allow this to happen more effectively
		if obj, ok := unstruct.Object["annotations"]; ok {
			existing := make(map[string]string)
			if found, ok := interfaceToStringMap(obj); ok {
				existing = found
			}
			if err := MergeInto(existing, annotations, OverwriteExistingDstKey); err != nil {
				return err
			}
			unstruct.Object["annotations"] = mapToGeneric(existing)
			return nil
		}
	}

	return nil
}

// addDeploymentConfigNestedLabels adds new label(s) to a nested labels of a single DeploymentConfig object
func addDeploymentConfigNestedLabels(obj *deployapi.DeploymentConfig, labels labels.Set) error {
	if obj.Spec.Template.Labels == nil {
		obj.Spec.Template.Labels = make(map[string]string)
	}
	if err := MergeInto(obj.Spec.Template.Labels, labels, OverwriteExistingDstKey); err != nil {
		return fmt.Errorf("unable to add labels to Template.DeploymentConfig.Template.ControllerTemplate.Template: %v", err)
	}
	return nil
}

func addDeploymentConfigNestedAnnotations(obj *deployapi.DeploymentConfig, annotations map[string]string) error {
	if obj.Spec.Template == nil {
		return nil
	}

	if obj.Spec.Template.Annotations == nil {
		obj.Spec.Template.Annotations = make(map[string]string)
	}

	if err := MergeInto(obj.Spec.Template.Annotations, annotations, OverwriteExistingDstKey); err != nil {
		return fmt.Errorf("unable to add annotations to Template.DeploymentConfig.Template.ControllerTemplate.Template: %v", err)
	}
	return nil
}

// interfaceToStringMap extracts a map[string]string from a map[string]interface{}
func interfaceToStringMap(obj interface{}) (map[string]string, bool) {
	if obj == nil {
		return nil, false
	}
	lm, ok := obj.(map[string]interface{})
	if !ok {
		return nil, false
	}
	existing := make(map[string]string)
	for k, v := range lm {
		switch t := v.(type) {
		case string:
			existing[k] = t
		}
	}
	return existing, true
}

// mapToGeneric converts a map[string]string into a map[string]interface{}
func mapToGeneric(obj map[string]string) map[string]interface{} {
	if obj == nil {
		return nil
	}
	res := make(map[string]interface{})
	for k, v := range obj {
		res[k] = v
	}
	return res
}

// MergeInto merges items from a src map into a dst map.
// Returns an error when the maps are not of the same type.
// Flags:
// - ErrorOnExistingDstKey
//     When set: Return an error if any of the dst keys is already set.
// - ErrorOnDifferentDstKeyValue
//     When set: Return an error if any of the dst keys is already set
//               to a different value than src key.
// - OverwriteDstKey
//     When set: Overwrite existing dst key value with src key value.
func MergeInto(dst, src interface{}, flags int) error {
	dstVal := reflect.ValueOf(dst)
	srcVal := reflect.ValueOf(src)

	if dstVal.Kind() != reflect.Map {
		return fmt.Errorf("dst is not a valid map: %v", dstVal.Kind())
	}
	if srcVal.Kind() != reflect.Map {
		return fmt.Errorf("src is not a valid map: %v", srcVal.Kind())
	}
	if dstTyp, srcTyp := dstVal.Type(), srcVal.Type(); !dstTyp.AssignableTo(srcTyp) {
		return fmt.Errorf("type mismatch, can't assign '%v' to '%v'", srcTyp, dstTyp)
	}

	if dstVal.IsNil() {
		return fmt.Errorf("dst value is nil")
	}
	if srcVal.IsNil() {
		// Nothing to merge
		return nil
	}

	for _, k := range srcVal.MapKeys() {
		if dstVal.MapIndex(k).IsValid() {
			if flags&ErrorOnExistingDstKey != 0 {
				return fmt.Errorf("dst key already set (ErrorOnExistingDstKey=1), '%v'='%v'", k, dstVal.MapIndex(k))
			}
			if dstVal.MapIndex(k).String() != srcVal.MapIndex(k).String() {
				if flags&ErrorOnDifferentDstKeyValue != 0 {
					return fmt.Errorf("dst key already set to a different value (ErrorOnDifferentDstKeyValue=1), '%v'='%v'", k, dstVal.MapIndex(k))
				}
				if flags&OverwriteExistingDstKey != 0 {
					dstVal.SetMapIndex(k, srcVal.MapIndex(k))
				}
			}
		} else {
			dstVal.SetMapIndex(k, srcVal.MapIndex(k))
		}
	}

	return nil
}
