package k8s

import (
	"bytes"
	"context"
	stdjson "encoding/json"
	"fmt"
	"strings"

	// 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/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/scheme"
	"github.com/tidbcloud/infra-provider/pkg/util"
)

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

type applyOptions struct {
	normalizeLastAppliedConfiguration        bool
	recordNormalizedLastAppliedConfiguration bool
}

// ApplyOption configures how ApplyWithResult calculates and records last-applied configuration.
type ApplyOption func(*applyOptions)

// WithNormalizeLastAppliedConfiguration omits dynamic Kubernetes fields, such as status and
// managedFields, when comparing and recording last-applied configuration.
func WithNormalizeLastAppliedConfiguration() ApplyOption {
	return func(opts *applyOptions) {
		opts.normalizeLastAppliedConfiguration = true
		opts.recordNormalizedLastAppliedConfiguration = true
	}
}

func newApplyOptions(obj client.Object, opts ...ApplyOption) applyOptions {
	normalize := normalizeLastAppliedConfigurationEnabled(obj)
	applyOpts := applyOptions{
		normalizeLastAppliedConfiguration:        normalize,
		recordNormalizedLastAppliedConfiguration: normalize,
	}
	for _, opt := range opts {
		opt(&applyOpts)
	}
	return applyOpts
}

func normalizeLastAppliedConfigurationEnabled(obj client.Object) bool {
	if obj == nil {
		return false
	}
	return strings.EqualFold(obj.GetAnnotations()[consts.NormalizeLastAppliedConfigAnnotation], "true")
}

// NormalizeLastAppliedConfigurationAnnotation rewrites the last-applied annotation on obj
// in-place with a normalized payload. It is a no-op when the annotation is absent or
// cannot be parsed, matching Apply's best-effort handling for legacy annotations.
func NormalizeLastAppliedConfigurationAnnotation(obj client.Object) bool {
	if obj == nil {
		return false
	}

	original := obj.GetAnnotations()[consts.LastAppliedConfigAnnotation]
	if original == "" {
		return false
	}

	normalized, err := normalizeConfiguration([]byte(original), false)
	if err != nil || string(normalized) == original {
		return false
	}

	util.SetAnnotation(obj, consts.LastAppliedConfigAnnotation, string(normalized))
	return true
}

func GetOriginalConfiguration(obj client.Object) []byte {
	return getOriginalConfiguration(obj, applyOptions{})
}

func getOriginalConfiguration(obj client.Object, opts applyOptions) []byte {
	annots := obj.GetAnnotations()

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

	if !opts.normalizeLastAppliedConfiguration {
		return []byte(original)
	}

	normalized, err := normalizeConfiguration([]byte(original), false)
	if err != nil {
		return []byte(original)
	}

	return normalized
}

// 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) {
	return getModifiedConfiguration(obj, gv, annotate, applyOptions{})
}

func getModifiedConfiguration(obj client.Object, gv runtime.GroupVersioner, annotate bool, opts applyOptions) ([]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("")

	rawModified, err := runtime.Encode(encoder, obj)
	if err != nil {
		return nil, err
	}
	modified := rawModified
	if opts.normalizeLastAppliedConfiguration {
		modified, err = normalizeConfiguration(rawModified, false)
		if err != nil {
			return nil, err
		}
	}

	if annotate {
		lastApplied := rawModified
		if opts.recordNormalizedLastAppliedConfiguration {
			lastApplied = modified
		}
		annots[consts.LastAppliedConfigAnnotation] = string(lastApplied)
		obj.SetAnnotations(annots)
		// restore resourceVersion into modified
		obj.SetResourceVersion(resourceVersion)

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

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

	return modified, nil
}

func normalizeConfiguration(data []byte, keepLastAppliedAnnotation bool) ([]byte, error) {
	if len(data) == 0 {
		return data, nil
	}

	var obj map[string]any
	decoder := stdjson.NewDecoder(bytes.NewReader(data))
	decoder.UseNumber()
	if err := decoder.Decode(&obj); err != nil {
		return nil, err
	}

	delete(obj, "status")

	metadata, ok := obj["metadata"].(map[string]any)
	if ok {
		for _, key := range []string{"creationTimestamp", "generation", "managedFields", "resourceVersion", "selfLink", "uid"} {
			delete(metadata, key)
		}
		if !keepLastAppliedAnnotation {
			if annotations, ok := metadata["annotations"].(map[string]any); ok {
				delete(annotations, consts.LastAppliedConfigAnnotation)
				if len(annotations) == 0 {
					delete(metadata, "annotations")
				}
			}
		}
	}

	return stdjson.Marshal(obj)
}

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, opts ...ApplyOption) (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)
	}

	applyOpts := newApplyOptions(expected, opts...)
	modified, err := getModifiedConfiguration(expected, gvks[0].GroupVersion(), false, applyOpts)
	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
	}

	// If the live object is still marked, keep comparing and recording normalized forms.
	// This handles the rollback path where the desired object no longer has the marker
	// but the live object still needs one patch to remove it without expanding last-applied.
	if !applyOpts.normalizeLastAppliedConfiguration && normalizeLastAppliedConfigurationEnabled(obj) {
		applyOpts.normalizeLastAppliedConfiguration = true
		applyOpts.recordNormalizedLastAppliedConfiguration = true
		modified, err = getModifiedConfiguration(expected, gvks[0].GroupVersion(), false, applyOpts)
		if err != nil {
			return ApplyResultError, fmt.Errorf("failed to get modified configuration: %w", err)
		}
	}

	original := getOriginalConfiguration(obj, applyOpts)

	// 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(), opts: applyOpts})
}

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

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

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

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

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

	modified, err := getModifiedConfiguration(p.expected, p.gv, true, p.opts)
	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
	}
	if p.opts.normalizeLastAppliedConfiguration {
		current, err = normalizeConfiguration(current, true)
		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
}
