package k8s

import (
	"context"
	"fmt"
	"net/url"
	"os"
	"path"

	xpmeta "github.com/crossplane/crossplane-runtime/pkg/meta"
	"gomodules.xyz/jsonpatch/v2"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/rest"
	clientcmd "k8s.io/client-go/tools/clientcmd"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/log"
)

func LoadKubeconfig(path string, scheme *runtime.Scheme) (client.Client, error) {
	cfg, err := GetKubeConfig(path)
	if err != nil {
		return nil, err
	}
	return client.New(cfg, client.Options{Scheme: scheme})
}

func GetKubeConfig(path string) (*rest.Config, error) {
	var cfg *rest.Config
	var err error
	if path == "" {
		// if no kubeconfig file is passed, read from service account token
		cfg, err = rest.InClusterConfig()
		if err != nil {
			return nil, err
		}
	} else {
		cfgBytes, err := os.ReadFile(path)
		if err != nil {
			return nil, err
		}
		cfg, err = clientcmd.RESTConfigFromKubeConfig(cfgBytes)
		if err != nil {
			return nil, err
		}
	}
	return cfg, 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 {
		obj := obj
		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 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
}

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
		} else {
			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
}

func GetNamespacedName(obj client.Object) types.NamespacedName {
	return types.NamespacedName{
		Namespace: obj.GetNamespace(),
		Name:      obj.GetName(),
	}
}

func URLJoint(base string, apiPath string) (string, error) {
	u, err := url.Parse(base)

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

	u.Path = path.Join(u.Path, apiPath)
	taskURL := u.String()
	return taskURL, nil
}

func IsCrossplaneCRPaused(obj client.Object) bool {
	annotations := obj.GetAnnotations()
	return annotations != nil && annotations[xpmeta.AnnotationKeyReconciliationPaused] == "true"
}

func PauseCrossplaneCR(ctx context.Context, cli client.Client, obj client.Object) error {
	logger := log.FromContext(ctx)
	annotations := obj.GetAnnotations()
	if annotations == nil {
		annotations = map[string]string{}
	}
	if annotations[xpmeta.AnnotationKeyReconciliationPaused] == "true" {
		return nil
	}

	logger.Info("pause crossplane cr", "name", obj.GetName(), "namespace", obj.GetNamespace())
	annotations[xpmeta.AnnotationKeyReconciliationPaused] = "true"
	obj.SetAnnotations(annotations)
	if err := cli.Update(ctx, obj); err != nil {
		return fmt.Errorf("failed to pause object %s/%s: %w", obj.GetNamespace(), obj.GetName(), err)
	}
	return nil
}

func HookAutoUnpauseCrossplaneCR(ctx context.Context, cli client.Client, obj client.Object) error {
	logger := log.FromContext(ctx)
	annotations := obj.GetAnnotations()
	if _, ok := annotations[xpmeta.AnnotationKeyReconciliationPaused]; !ok {
		return nil
	}

	logger.Info("auto unpause crossplane cr", "name", obj.GetName(), "namespace", obj.GetNamespace())
	delete(annotations, xpmeta.AnnotationKeyReconciliationPaused)
	obj.SetAnnotations(annotations)
	if err := cli.Update(ctx, obj); err != nil {
		return fmt.Errorf("failed to update object %s/%s: %w", obj.GetNamespace(), obj.GetName(), err)
	}
	return nil
}
