package internal

import (
	"context"
	"encoding/json"
	"fmt"

	"github.com/openshift/cluster-version-operator/lib/resourceapply"

	apierrors "k8s.io/apimachinery/pkg/api/errors"
	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/util/diff"
	"k8s.io/apimachinery/pkg/util/sets"
	"k8s.io/apimachinery/pkg/util/strategicpatch"
	"k8s.io/client-go/dynamic"
	"k8s.io/klog/v2"

	"github.com/openshift/client-go/config/clientset/versioned/scheme"
	"github.com/openshift/cluster-version-operator/lib/resourcebuilder"
	"github.com/openshift/library-go/pkg/manifest"
)

// readUnstructuredV1OrDie reads operatorstatus object from bytes. Panics on error.
func readUnstructuredV1OrDie(objBytes []byte) *unstructured.Unstructured {
	udi, _, err := scheme.Codecs.UniversalDecoder().Decode(objBytes, nil, &unstructured.Unstructured{})
	if err != nil {
		panic(err)
	}
	return udi.(*unstructured.Unstructured)
}

func applyUnstructured(ctx context.Context, client dynamic.ResourceInterface, required *unstructured.Unstructured) (*unstructured.Unstructured, bool, error) {
	if required.GetName() == "" {
		return nil, false, fmt.Errorf("invalid object: name cannot be empty")
	}
	existing, err := client.Get(ctx, required.GetName(), metav1.GetOptions{})
	if apierrors.IsNotFound(err) {
		klog.V(2).Infof("%s %s/%s not found, creating", required.GetKind(), required.GetNamespace(), required.GetName())
		actual, err := client.Create(ctx, required, metav1.CreateOptions{})
		return actual, true, err
	}
	if err != nil {
		return nil, false, err
	}
	// if we only create this resource, we have no need to continue further
	if resourceapply.IsCreateOnly(required) {
		return nil, false, nil
	}

	skipKeys := sets.NewString("apiVersion", "kind", "metadata", "status")

	// create a copy of required, but copy skipKeys from existing
	// this would copy skipKeys data into expected from existing
	expected := required.DeepCopy()
	for k, v := range existing.Object {
		if skipKeys.Has(k) {
			expected.Object[k] = v
		}
	}

	objDiff := diff.ObjectDiff(expected, existing)
	if objDiff == "" {
		// Skip update, as no changes found
		return existing, false, nil
	}

	// copy all keys from required in existing except skipKeys
	for k, v := range required.Object {
		if skipKeys.Has(k) {
			continue
		}
		existing.Object[k] = v
	}

	existing.SetAnnotations(required.GetAnnotations())
	existing.SetLabels(required.GetLabels())
	existing.SetOwnerReferences(required.GetOwnerReferences())

	klog.V(2).Infof("Updating %s %s/%s due to diff: %v", required.GetKind(), required.GetNamespace(), required.GetName(), objDiff)

	actual, err := client.Update(ctx, existing, metav1.UpdateOptions{})
	if err != nil {
		return nil, false, err
	}
	return actual, existing.GetResourceVersion() != actual.GetResourceVersion(), nil
}

type genericBuilder struct {
	client   dynamic.ResourceInterface
	raw      []byte
	modifier resourcebuilder.MetaV1ObjectModifierFunc
}

// NewGenericBuilder returns an implentation of resourcebuilder.Interface that
// uses dynamic clients for applying.
func NewGenericBuilder(client dynamic.ResourceInterface, m manifest.Manifest) (resourcebuilder.Interface, error) {
	return &genericBuilder{
		client: client,
		raw:    m.Raw,
	}, nil
}

func (b *genericBuilder) WithMode(m resourcebuilder.Mode) resourcebuilder.Interface {
	return b
}

func (b *genericBuilder) WithModifier(f resourcebuilder.MetaV1ObjectModifierFunc) resourcebuilder.Interface {
	b.modifier = f
	return b
}

func (b *genericBuilder) Do(ctx context.Context) error {
	ud := readUnstructuredV1OrDie(b.raw)
	if b.modifier != nil {
		b.modifier(ud)
	}

	_, _, err := applyUnstructured(ctx, b.client, ud)
	return err
}

func createPatch(original, modified runtime.Object) ([]byte, error) {
	originalData, err := json.Marshal(original)
	if err != nil {
		return nil, err
	}
	modifiedData, err := json.Marshal(modified)
	if err != nil {
		return nil, err
	}
	return strategicpatch.CreateTwoWayMergePatch(originalData, modifiedData, original)
}
