package yaml

import (
	"context"
	"errors"
	"fmt"

	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/api/meta"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/shared-provider/tests/framework/env/types"
)

var errNotManaged = errors.New("resource has exists and is not managed")

// Interface is prepared for dealing with kubernetes resource in yaml data format.
type Interface interface {
	// CreateOrUpdate creates or updates objects in yaml format
	CreateOrUpdate(ctx context.Context, ns string, paths ...string) error

	Cleanup(ctx context.Context) error
}

func New(c client.Client) Interface {
	return &yamlManager{
		c:    c,
		objs: make(map[GVKNN]struct{}),
	}
}

type yamlManager struct {
	c    client.Client
	objs map[GVKNN]struct{}
}

type GVKNN struct {
	schema.GroupVersionKind
	Namespace string
	Name      string
}

func (m *yamlManager) Cleanup(ctx context.Context) error {
	for gvknn := range m.objs {
		obj := &unstructured.Unstructured{}
		obj.SetGroupVersionKind(gvknn.GroupVersionKind)
		obj.SetNamespace(gvknn.Namespace)
		obj.SetName(gvknn.Name)

		if err := m.c.Delete(ctx, obj); err != nil {
			if !apierrors.IsNotFound(err) {
				return fmt.Errorf("delete object %v failed: %w", gvknn, err)
			}
		}
	}

	return nil
}

func (m *yamlManager) CreateOrUpdate(ctx context.Context, ns string, paths ...string) error {
	objs, err := ReadObjects(paths)
	if err != nil {
		return fmt.Errorf("read objects failed: %w", err)
	}

	for _, obj := range objs {
		gvk := obj.GetObjectKind().GroupVersionKind()
		mapping, err := m.c.RESTMapper().RESTMapping(gvk.GroupKind(), gvk.Version)
		if err != nil {
			return fmt.Errorf("get restmapping for gvk %v err: %w", gvk, err)
		}
		gvknn := GVKNN{
			GroupVersionKind: gvk,
			Namespace:        ns,
			Name:             obj.GetName(),
		}
		if mapping.Scope.Name() == meta.RESTScopeNameRoot {
			gvknn.Namespace = ""
		}

		if err := m.createOrUpdateObject(ctx, gvknn, obj); err != nil {
			return err
		}

		m.objs[gvknn] = struct{}{}
	}

	return nil
}

func (m *yamlManager) createOrUpdateObject(ctx context.Context, gvknn GVKNN, obj client.Object) error {
	newObj := &unstructured.Unstructured{}
	newObj.SetGroupVersionKind(gvknn.GroupVersionKind)

	if err := m.c.Get(ctx, client.ObjectKey{
		Namespace: gvknn.Namespace,
		Name:      gvknn.Name,
	}, newObj); err != nil {
		if !apierrors.IsNotFound(err) {
			return fmt.Errorf("get object %v failed: %w", gvknn, err)
		}
		if err := m.c.Create(ctx, obj); err != nil {
			return fmt.Errorf("create object %v err: %w", gvknn, err)
		}

		return nil
	}
	anno := newObj.GetAnnotations()
	if anno == nil {
		return fmt.Errorf("%v: %w", gvknn, errNotManaged)
	}
	if managedBy, ok := anno[types.AnnoKeyManagedBy]; !ok || managedBy != types.AnnoValManagedByProvider {
		return fmt.Errorf("%v: %w", gvknn, errNotManaged)
	}
	obj.SetResourceVersion(newObj.GetResourceVersion())
	if err := m.c.Update(ctx, obj); err != nil {
		return fmt.Errorf("update %v error:%w", gvknn, err)
	}

	return nil
}
