package apply

import (
	"fmt"
	"strings"

	"github.com/rancher/wrangler/pkg/gvk"

	"github.com/rancher/wrangler/pkg/kv"

	"github.com/pkg/errors"
	namer "github.com/rancher/wrangler/pkg/name"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/api/meta"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/client-go/dynamic"
	"k8s.io/client-go/tools/cache"
)

var (
	ErrOwnerNotFound = errors.New("owner not found")
)

func notFound(name string, gvk schema.GroupVersionKind) error {
	// this is not proper, but does it really matter that much? If you find this
	// line while researching a bug, then the answer is probably yes.
	resource := namer.GuessPluralName(strings.ToLower(gvk.Kind))
	return apierrors.NewNotFound(schema.GroupResource{
		Group:    gvk.Group,
		Resource: resource,
	}, name)
}

func getGVK(gvkLabel string, gvk *schema.GroupVersionKind) error {
	parts := strings.Split(gvkLabel, ", Kind=")
	if len(parts) != 2 {
		return fmt.Errorf("invalid GVK format: %s", gvkLabel)
	}
	gvk.Group, gvk.Version = kv.Split(parts[0], "/")
	gvk.Kind = parts[1]
	return nil
}

func (o desiredSet) FindOwner(obj runtime.Object) (runtime.Object, error) {
	if obj == nil {
		return nil, ErrOwnerNotFound
	}
	meta, err := meta.Accessor(obj)
	if err != nil {
		return nil, err
	}

	var (
		debugID   = fmt.Sprintf("%s/%s", meta.GetNamespace(), meta.GetName())
		gvkLabel  = meta.GetAnnotations()[LabelGVK]
		namespace = meta.GetAnnotations()[LabelNamespace]
		name      = meta.GetAnnotations()[LabelName]
		gvk       schema.GroupVersionKind
	)

	if gvkLabel == "" {
		return nil, ErrOwnerNotFound
	}

	if err := getGVK(gvkLabel, &gvk); err != nil {
		return nil, err
	}

	cache, client, err := o.getControllerAndClient(debugID, gvk)
	if err != nil {
		return nil, err
	}

	if cache != nil {
		return o.fromCache(cache, namespace, name, gvk)
	}

	return o.fromClient(client, namespace, name, gvk)
}

func (o *desiredSet) fromClient(client dynamic.NamespaceableResourceInterface, namespace, name string, gvk schema.GroupVersionKind) (runtime.Object, error) {
	var (
		err error
		obj interface{}
	)
	if namespace == "" {
		obj, err = client.Get(o.ctx, name, metav1.GetOptions{})
	} else {
		obj, err = client.Namespace(namespace).Get(o.ctx, name, metav1.GetOptions{})
	}
	if err != nil {
		return nil, err
	}
	if ro, ok := obj.(runtime.Object); ok {
		return ro, nil
	}
	return nil, notFound(name, gvk)
}

func (o *desiredSet) fromCache(cache cache.SharedInformer, namespace, name string, gvk schema.GroupVersionKind) (runtime.Object, error) {
	var key string
	if namespace == "" {
		key = name
	} else {
		key = namespace + "/" + name
	}
	item, ok, err := cache.GetStore().GetByKey(key)
	if err != nil {
		return nil, err
	} else if !ok {
		return nil, notFound(name, gvk)
	} else if ro, ok := item.(runtime.Object); ok {
		return ro, nil
	}
	return nil, notFound(name, gvk)
}

func (o desiredSet) PurgeOrphan(obj runtime.Object) error {
	if obj == nil {
		return nil
	}

	meta, err := meta.Accessor(obj)
	if err != nil {
		return err
	}

	if _, err := o.FindOwner(obj); apierrors.IsNotFound(err) {
		gvk, err := gvk.Get(obj)
		if err != nil {
			return err
		}

		o.strictCaching = false
		_, client, err := o.getControllerAndClient(meta.GetName(), gvk)
		if err != nil {
			return err
		}
		if meta.GetNamespace() == "" {
			return client.Delete(o.ctx, meta.GetName(), metav1.DeleteOptions{})
		} else {
			return client.Namespace(meta.GetNamespace()).Delete(o.ctx, meta.GetName(), metav1.DeleteOptions{})
		}
	} else if err == ErrOwnerNotFound {
		return nil
	} else if err != nil {
		return err
	}
	return nil
}
