package k8s

import (
	"context"
	"fmt"

	// debug modified contents, discomment it when you need
	// "github.com/google/go-cmp/cmp"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/serializer/json"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apimachinery/pkg/util/jsonmergepatch"
	"k8s.io/apimachinery/pkg/util/mergepatch"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/ticdc-service-ng/pkg/consts"
	"github.com/tidbcloud/ticdc-service-ng/pkg/scheme"
	"github.com/tidbcloud/ticdc-service-ng/pkg/util"
)

var (
	serializer = json.NewSerializerWithOptions(
		json.DefaultMetaFactory,
		scheme.Scheme,
		scheme.Scheme,
		json.SerializerOptions{
			Yaml:   false,
			Pretty: false,
			Strict: true,
		})
)

func GetOriginalConfiguration(obj client.Object) []byte {
	annots := obj.GetAnnotations()

	original, ok := annots[consts.LastAppliedConfigAnnotation]
	if !ok {
		return nil
	}

	return []byte(original)
}

// GetModifiedConfiguration returns the modified configuration of the object.
// It first serializes the object without the last applied annotation to prevent recursion,
// then add the annotation back after serializing.
//
// Arg `annotate` means whether the modified config contains the last applied annotation.
// If annotate is true, the last applied config will be record in annotation, and the return value
// is used for generating three way json merge patch.
// If annotate is false, return value is the expected last applied config
func GetModifiedConfiguration(obj client.Object, gv runtime.GroupVersioner, annotate bool) ([]byte, error) {
	encoder := scheme.Codecs.EncoderForVersion(serializer, gv)
	annots := obj.GetAnnotations()
	if annots == nil {
		annots = map[string]string{}
	}

	original := annots[consts.LastAppliedConfigAnnotation]
	delete(annots, consts.LastAppliedConfigAnnotation)
	obj.SetAnnotations(annots)
	// del resourceVersion from modified
	resourceVersion := obj.GetResourceVersion()
	obj.SetResourceVersion("")

	modified, err := runtime.Encode(encoder, obj)
	if err != nil {
		return nil, err
	}

	if annotate {
		annots[consts.LastAppliedConfigAnnotation] = string(modified)
		obj.SetAnnotations(annots)
		// restore resourceVersion into modified
		obj.SetResourceVersion(resourceVersion)

		modified, err = runtime.Encode(encoder, obj)
		if err != nil {
			return nil, err
		}
	}

	// Restore the object to its original condition.
	annots[consts.LastAppliedConfigAnnotation] = original
	obj.SetAnnotations(annots)

	return modified, nil
}

type ApplyResult string

const (
	ApplyResultCreated   ApplyResult = "created"
	ApplyResultUnchanged ApplyResult = "unchanged"
	ApplyResultPatched   ApplyResult = "patched"
	ApplyResultError     ApplyResult = "error"
)

// ApplyWithResult creates or updates the given object via client-side apply, just like `kubectl apply`.
// It first attempts to retrieve the object from the cluster. If the object does not exist,
// it will set the last applied annotation and create it. If the object does exist, it
// compares the modification with the last applied annotation. If the modification is the same,
// it does nothing. Otherwise, it will patch the object and update the last applied annotation.
//
// Note:
//   - Make sure the order of list fields is stable, otherwise it will be considered as modified.
func ApplyWithResult(ctx context.Context, c client.Client, obj client.Object) (ApplyResult, error) {
	if err := checkObject(obj); err != nil {
		return ApplyResultError, fmt.Errorf("the given obj is invalid: %w", err)
	}

	gvks, _, err := scheme.Scheme.ObjectKinds(obj)
	if err != nil {
		return ApplyResultError, fmt.Errorf("can't get gvk of object %T: %w", obj, err)
	}

	expectedObj := obj.DeepCopyObject()

	expected, ok := expectedObj.(client.Object)
	if !ok {
		return ApplyResultError, fmt.Errorf("expected object %T doest not implement client.Object", expectedObj)
	}

	modified, err := GetModifiedConfiguration(expected, gvks[0].GroupVersion(), false)
	if err != nil {
		return ApplyResultError, fmt.Errorf("failed to get modified configuration: %w", err)
	}

	if err := c.Get(ctx, client.ObjectKeyFromObject(obj), obj); err != nil {
		if !apierrors.IsNotFound(err) {
			return ApplyResultError, err
		}

		if len(modified) < 1 {
			return ApplyResultError, fmt.Errorf("the given obj is invalid, whose modification is empty")
		}

		util.SetAnnotation(obj, consts.LastAppliedConfigAnnotation, string(modified))

		if err := c.Create(ctx, obj); err != nil {
			return ApplyResultError, err
		}
		return ApplyResultCreated, nil
	}

	original := GetOriginalConfiguration(obj)

	// Compared with the last applied, the object is not modified.
	if string(original) == string(modified) {
		return ApplyResultUnchanged, nil
	}
	// debug modified contents, discomment it when you need
	// fmt.Printf("modified, original:%s,\nmodified:%s\n diff: %s", string(original), string(modified), cmp.Diff(string(original), string(modified)))

	return ApplyResultPatched, c.Patch(ctx, obj, &threeWayMergePatch{expected: expected, gv: gvks[0].GroupVersion()})
}

func Apply(ctx context.Context, c client.Client, obj client.Object) error {
	_, err := ApplyWithResult(ctx, c, obj)
	return err
}

type threeWayMergePatch struct {
	expected client.Object
	gv       runtime.GroupVersioner
}

func (p *threeWayMergePatch) Type() types.PatchType {
	return types.MergePatchType
}

func (p *threeWayMergePatch) Data(obj client.Object) ([]byte, error) {
	original := GetOriginalConfiguration(obj)

	// set resourceVersion of current in modified
	version := obj.GetResourceVersion()
	obj.SetResourceVersion("")
	p.expected.SetResourceVersion(version)

	modified, err := GetModifiedConfiguration(p.expected, p.gv, true)
	if err != nil {
		return nil, err
	}

	encoder := scheme.Codecs.EncoderForVersion(serializer, p.gv)
	current, err := runtime.Encode(encoder, obj)
	if err != nil {
		return nil, err
	}

	preconditions := []mergepatch.PreconditionFunc{mergepatch.RequireKeyUnchanged("apiVersion"),
		mergepatch.RequireKeyUnchanged("kind"), mergepatch.RequireMetadataKeyUnchanged("name")}
	patch, err := jsonmergepatch.CreateThreeWayJSONMergePatch(original, modified, current, preconditions...)
	if err != nil {
		if mergepatch.IsPreconditionFailed(err) {
			return nil, fmt.Errorf("%s", "At least one of apiVersion, kind and name was changed")
		}
		return nil, err
	}

	return patch, nil
}

// checkObject checks if the given object is valid.
func checkObject(obj client.Object) error {
	if obj == nil {
		return fmt.Errorf("the given obj is nil")
	}

	if obj.GetName() == "" {
		return fmt.Errorf("the given obj's name is empty")
	}

	return nil
}
