package k8s

import (
	"context"
	"fmt"
	"strings"

	"github.com/Masterminds/semver/v3"
	"gomodules.xyz/jsonpatch/v2"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

func DeleteObject(ctx context.Context, cli client.Writer, obj client.Object) error {
	if err := client.IgnoreNotFound(cli.Delete(ctx, obj)); err != nil {
		return err
	}
	return nil
}

// CompareVersions returns true if the constraint <version1> compared by <operator> to <version2>
// returns true, and false otherwise.
// The comparison is based on semantic versions, i.e. <version1> and <version2> will be converted
// if needed.
func CompareVersions(version1, operator, version2 string) (bool, error) {
	var (
		v1 = normalizeVersion(version1)
		v2 = normalizeVersion(version2)
	)

	return CheckVersionMeetsConstraint(v1, fmt.Sprintf("%s %s", operator, v2))
}

func normalizeVersion(version string) string {
	v := strings.ReplaceAll(version, "v", "")
	idx := strings.IndexAny(v, "-+")
	if idx != -1 {
		v = v[:idx]
	}
	return v
}

// CheckVersionMeetsConstraint returns true if the <version> meets the <constraint>.
func CheckVersionMeetsConstraint(version, constraint string) (bool, error) {
	c, err := semver.NewConstraint(constraint)
	if err != nil {
		return false, err
	}

	v, err := semver.NewVersion(version)
	if err != nil {
		return false, err
	}

	return c.Check(v), nil
}

type ListOpts[T client.Object] struct {
	ObjectList client.ObjectList
	Unpack     func() []T
}

func MapPtr[T any](objs []T) []*T {
	ptrs := make([]*T, 0, len(objs))
	for _, obj := range objs {
		ptrs = append(ptrs, &obj)
	}
	return ptrs
}

func (o *ListOpts[T]) List(ctx context.Context, cli client.Client, opts ...client.ListOption) ([]T, error) {
	var result []T
	if err := cli.List(ctx, o.ObjectList, opts...); err != nil {
		return result, err
	}
	return o.Unpack(), nil
}

func (o *ListOpts[T]) FilterBy(filter func(T) error) []T {
	var result []T
	for _, obj := range o.Unpack() {
		if filter(obj) == nil {
			result = append(result, obj)
		}
	}
	return result
}

func (o *ListOpts[T]) ListFilterBy(ctx context.Context, cli client.Client, filter func(T) bool, opts ...client.ListOption) ([]T, error) {
	var result []T
	if err := cli.List(ctx, o.ObjectList, opts...); err != nil {
		return result, err
	}
	for _, obj := range o.Unpack() {
		if filter(obj) {
			result = append(result, obj)
		}
	}
	return result, nil
}

func (o *ListOpts[T]) DeleteFilterBy(ctx context.Context, cli client.Client, filter func(T) bool, opts ...client.ListOption) error {
	objs, err := o.ListFilterBy(ctx, cli, filter, opts...)
	if err != nil {
		return err
	}
	for _, obj := range objs {
		if err := DeleteObject(ctx, cli, obj); err != nil {
			return err
		}
	}
	return nil
}

func (o *ListOpts[T]) PatchFilterBy(ctx context.Context, cli client.Client, filter func(T) bool, patches []ObjectPatch, opts ...client.ListOption) error {
	objs, err := o.ListFilterBy(ctx, cli, filter, opts...)
	if err != nil {
		return err
	}
	for _, obj := range objs {
		if err := PatchObject(ctx, cli, obj, patches...); err != nil {
			return err
		}
	}
	return nil
}

func (o *ListOpts[T]) AllValid(ctx context.Context, cli client.Client, validate func(T) error, opts ...client.ListOption) (error, error) {
	if err := cli.List(ctx, o.ObjectList, opts...); err != nil {
		return nil, err
	}
	for _, obj := range o.Unpack() {
		if err := validate(obj); err != nil {
			return err, nil
		}
	}
	return nil, nil
}

func CreateIfNotExist(ctx context.Context, cli client.Client, obj client.Object) error {
	err := cli.Get(ctx, client.ObjectKeyFromObject(obj), obj)
	if err != nil {
		if apierrors.IsNotFound(err) {
			return cli.Create(ctx, obj)
		}
		return err
	}

	return nil
}

func DeleteObjectCompleted(ctx context.Context, cli client.Client, obj client.Object) error {
	return DeleteObjectCompletedWithHooks(ctx, cli, obj)
}

type DeletionHook func(ctx context.Context, cli client.Client, obj client.Object) error

func DeleteObjectCompletedWithHooks(ctx context.Context, cli client.Client, obj client.Object, preDeletionHooks ...DeletionHook) error {
	err := cli.Get(ctx, client.ObjectKeyFromObject(obj), obj)
	if err != nil {
		if apierrors.IsNotFound(err) {
			return nil
		}
		return err
	}

	for _, hook := range preDeletionHooks {
		if err := hook(ctx, cli, obj); err != nil {
			return err
		}
	}

	if err := cli.Delete(ctx, obj); err != nil {
		return err
	}
	return fmt.Errorf("object %s %v still exist", obj.GetObjectKind().GroupVersionKind(), obj.GetName())
}

func PatchObject(ctx context.Context, cli client.Writer, obj client.Object, patchers ...ObjectPatch) error {
	ops := make([]*jsonpatch.Operation, 0, len(patchers))
	for _, patcher := range patchers {
		ops = append(ops, patcher(obj))
	}
	patch, err := ConstructPatch(ops...)
	if err != nil {
		return err
	}
	return client.IgnoreNotFound(cli.Patch(ctx, obj, patch))
}

func GetDeletedObject(ctx context.Context, cli client.Reader, key types.NamespacedName, obj client.Object) error {
	err := cli.Get(ctx, key, obj)
	if apierrors.IsNotFound(err) {
		return nil
	}
	if err == nil {
		return fmt.Errorf("resource %s %s is still present", obj.GetObjectKind().GroupVersionKind().Kind, obj.GetName())
	}
	return err
}
