package applier

import (
	"context"
	"fmt"
	"strconv"

	"github.com/go-logr/logr"
	"github.com/mitchellh/hashstructure/v2"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)

// All the below code can be extracted as a common package
const (
	AnnoLastAppliedHash = "tidbcloud.com/last-applied-hash"
)

type Object[T any] interface {
	client.Object
	*T
}

type Appliable[PT Object[T], T any] interface {
	Apply(ctx context.Context) (PT, error)
}

type Applier[PT Object[T], T any] interface {
	Expected(obj PT) Appliable[PT, T]
}

type MutateFn[PT Object[T], T any] func(current, expected PT) error

type appliable[PT Object[T], T any] struct {
	c        client.Client
	current  PT
	expected PT
	fn       MutateFn[PT, T]
}

func Hash(v interface{}) (string, error) {
	hash, err := hashstructure.Hash(v, hashstructure.FormatV2, &hashstructure.HashOptions{
		ZeroNil:         true,
		IgnoreZeroValue: true,
	})
	if err != nil {
		return "", err
	}

	return strconv.FormatUint(hash, 10), nil
}

// It's hard to diff the current and the expected object, because there are some default values
// which are set in server side.
// TODO: Maybe server-side apply is a more appropriate way.
//
// Now we compute the hash of expected object to avoid sending modification to apiserver too many times
// NOTE: Some manually change will not be rewritten by this operator
func (p *appliable[PT, T]) Apply(ctx context.Context) (PT, error) {
	res, err := controllerutil.CreateOrUpdate(ctx, p.c, p.current, func() error {
		hash, err := Hash(p.expected)
		if err != nil {
			return err
		}

		anno := p.current.GetAnnotations()
		curHash, ok := anno[AnnoLastAppliedHash]
		if !ok || curHash != hash {
			if err := p.fn(p.current, p.expected); err != nil {
				return err
			}

			anno := p.current.GetAnnotations()
			if anno == nil {
				anno = map[string]string{}
			}

			anno[AnnoLastAppliedHash] = hash
			p.current.SetAnnotations(anno)
		}

		return nil
	})
	if err != nil {
		return nil, err
	}

	logger := logr.FromContextOrDiscard(ctx)

	logger.Info("apply success",
		"namespace", p.current.GetNamespace(),
		"name", p.current.GetName(),
		"type", fmt.Sprintf("%T", p.current),
		"result", res,
	)

	return p.current, nil
}

func NewObject[PT Object[T], T any](ns, name string) PT {
	var dst PT = new(T)
	dst.SetName(name)
	dst.SetNamespace(ns)

	return dst
}

func NewClusterObject[PT Object[T], T any](name string) PT {
	return NewObject[PT]("", name)
}

func newEmptyObject[PT Object[T], T any](src PT) PT {
	return NewObject[PT](src.GetNamespace(), src.GetName())
}

type applier[PT Object[T], T any] struct {
	c  client.Client
	fn func(current, expected PT) error
}

func NewApplier[PT Object[T], T any](c client.Client, fn MutateFn[PT, T]) Applier[PT, T] {
	return &applier[PT, T]{
		c:  c,
		fn: fn,
	}
}

func (p *applier[PT, T]) Expected(obj PT) Appliable[PT, T] {
	return &appliable[PT, T]{
		c:        p.c,
		current:  newEmptyObject(obj),
		expected: obj,
		fn:       p.fn,
	}
}
