package runtime

import (
	"bytes"
	"errors"

	"github.com/RafayLabs/rcloud-base/pkg/controller/scheme"
	apiv2 "github.com/RafayLabs/rcloud-base/proto/types/controller"
	rbacv1 "k8s.io/api/rbac/v1"
	apixv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"

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

	"github.com/RafayLabs/rcloud-base/pkg/controller/util"
)

var (
	// ErrInvalidObject is returned for invalid object
	ErrInvalidObject = errors.New("object interface not implemented")
)

// FromObject creates step object from runtime object
func FromObject(ro runtime.Object) (*apiv2.StepObject, error) {

	var so apiv2.StepObject
	var err error

	bb := new(bytes.Buffer)
	err = scheme.Serializer.Encode(ro, bb)
	if err != nil {
		return nil, err
	}

	// use step object accessor to get object gvk
	// so.SetGroupVersionKind(ro.GetObjectKind().GroupVersionKind())
	// if mo, ok := ro.(metav1.Object); ok {
	// 	so.Name = mo.GetName()
	// }
	so.Raw = bb.Bytes()

	// so.Raw, err = util.CleanPatch(so.Raw)
	// if err != nil {
	// 	return nil, err
	// }

	return &so, nil
}

// SetNamespace sets namespace for runtime object
func SetNamespace(ro runtime.Object, namespace string) error {

	switch ro.(type) {
	case *apixv1beta1.CustomResourceDefinition:
	case *rbacv1.ClusterRole:
	case *rbacv1.ClusterRoleBinding:
		crb := ro.(*rbacv1.ClusterRoleBinding)
		for i := range crb.Subjects {
			if crb.Subjects[i].Kind == rbacv1.ServiceAccountKind &&
				crb.Subjects[i].Namespace == "" {
				crb.Subjects[i].Namespace = namespace
			}
		}
	case *rbacv1.RoleBinding:
		rb := ro.(*rbacv1.RoleBinding)
		if rb.Namespace == "" {
			rb.Namespace = namespace
		}
		for i := range rb.Subjects {
			if rb.Subjects[i].Kind == rbacv1.ServiceAccountKind &&
				rb.Subjects[i].Namespace == "" {
				rb.Subjects[i].Namespace = namespace
			}
		}
	case *rbacv1.Role:
		rb := ro.(*rbacv1.Role)
		if rb.Namespace == "" {
			rb.Namespace = namespace
		}
	default:
		if mo, ok := ro.(metav1.Object); ok {
			mo.SetNamespace(namespace)
			return nil
		}
		return ErrInvalidObject

	}
	return nil
}

// ToObject converts step object to runtime object
func ToObject(so *apiv2.StepObject) (o runtime.Object, gvk *schema.GroupVersionKind, err error) {

	accessor, err := so.Accessor()
	if err != nil {
		return
	}

	eGVK, err := accessor.GroupVersionKind()
	if err != nil {
		return
	}

	if scheme.Scheme.Recognizes(eGVK) {
		o, gvk, err = scheme.Serializer.Decode(so.Raw, nil, nil)
	} else {
		o, gvk, err = scheme.Serializer.Decode(so.Raw, nil, &unstructured.Unstructured{})
	}

	return o, gvk, err
}

// ToStructuredObject converts unstructured object to structured object
func ToStructuredObject(obj runtime.Object) runtime.Object {

	if _, ok := obj.(*unstructured.Unstructured); ok {
		bb := new(bytes.Buffer)
		err := scheme.Serializer.Encode(obj, bb)
		if err != nil {
			return obj
		}

		o, _, err := scheme.Serializer.Decode(bb.Bytes(), nil, nil)
		if err != nil {
			return obj
		}
		return o

	}

	return obj
}

// ToUnstructuredObject converts step object to unstructured object,
// this is useful for preserving original json serialized input in step object.
// Note: while patching k8s resources, we should preserve the user input, as
// patching can remove fields; which are represented as null values in the patch
func ToUnstructuredObject(so *apiv2.StepObject) (*unstructured.Unstructured, *schema.GroupVersionKind, error) {
	var o runtime.Object
	var err error
	var gvk *schema.GroupVersionKind

	o, gvk, err = scheme.Serializer.Decode(so.Raw, nil, &unstructured.Unstructured{})

	return o.(*unstructured.Unstructured), gvk, err
}

// PatchMeta is the metadata to be added while patching
type PatchMeta struct {
	Annotations map[string]string
}

// PatchOption is the functional patch option
type PatchOption func(*PatchMeta)

// AddAnnotations adds annotations to object
func AddAnnotations(annotations map[string]string) PatchOption {
	return func(m *PatchMeta) {
		m.Annotations = annotations
	}
}

// Patch patches the step object with give step object
func Patch(input *apiv2.StepObject, with *apiv2.StepObject, opts ...PatchOption) error {
	pm := &PatchMeta{}
	for _, opt := range opts {
		opt(pm)
	}
	accessor, err := input.Accessor()
	if err != nil {
		return err
	}
	gvk, err := accessor.GroupVersionKind()
	if err != nil {
		return err
	}

	if util.IsStrategicMergePatch(gvk) {
		pb, err := util.CreateStrategicMergePatch(gvk, nil, input.Raw, with.Raw)
		if err != nil {
			return err
		}

		fb, err := util.ApplyStrategicMergePatch(gvk, input.Raw, pb)
		if err != nil {
			return err
		}

		input.Raw = fb
	} else {
		pb, err := util.CreateJSONMergePatch(nil, input.Raw, with.Raw)
		if err != nil {
			return err
		}

		fb, err := util.ApplyJSONMergePatch(input.Raw, pb)
		if err != nil {
			return err
		}

		input.Raw = fb

	}

	if pm.Annotations != nil {
		accessor, err := input.Accessor()
		if err != nil {
			return err
		}

		accessor.SetAnnotations(pm.Annotations)
		input.Raw = accessor.Bytes()
	}

	return nil
}
