package k8s

import (
	"context"
	"fmt"

	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/client-go/discovery"
	"k8s.io/client-go/kubernetes"
	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/clientcmd"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

var (
	baseSchemes = []func(s *runtime.Scheme) error{
		clientgoscheme.AddToScheme,
	}
)

type MutateConfig func(*rest.Config)

type UnionClient struct {
	Ctrl client.Client
	Kube kubernetes.Interface
}

// NewUnionClientFromKubeConfig creates a new UnionClient from the given kubeconfig data.
func NewUnionClientFromKubeConfig(data []byte, mutate MutateConfig) (*UnionClient, error) {
	scheme := runtime.NewScheme()
	for _, fn := range baseSchemes {
		if err := fn(scheme); err != nil {
			return nil, fmt.Errorf("add scheme failed: %w", err)
		}
	}

	restConfig, err := clientcmd.RESTConfigFromKubeConfig(data)
	if err != nil {
		return nil, fmt.Errorf("build rest config: %w", err)
	}
	if mutate != nil {
		mutate(restConfig)
	}

	// TODO: add client option
	ctrl, err := client.New(restConfig, client.Options{
		Scheme: scheme,
	})
	if err != nil {
		return nil, fmt.Errorf("build ctrl client: %w", err)
	}
	kube, err := kubernetes.NewForConfig(restConfig)
	if err != nil {
		return nil, fmt.Errorf("build kube client: %w", err)
	}

	return &UnionClient{
		Ctrl: ctrl,
		Kube: kube,
	}, nil
}

func NewUnionClientFromRest(restConfig *rest.Config, mutate MutateConfig) (*UnionClient, error) {
	scheme := runtime.NewScheme()
	for _, fn := range baseSchemes {
		if err := fn(scheme); err != nil {
			return nil, fmt.Errorf("add scheme failed: %w", err)
		}
	}

	if mutate != nil {
		mutate(restConfig)
	}

	// TODO: add client option
	ctrl, err := client.New(restConfig, client.Options{
		Scheme: scheme,
	})
	if err != nil {
		return nil, fmt.Errorf("build ctrl client: %w", err)
	}
	kube, err := kubernetes.NewForConfig(restConfig)
	if err != nil {
		return nil, fmt.Errorf("build kube client: %w", err)
	}

	return &UnionClient{
		Ctrl: ctrl,
		Kube: kube,
	}, nil
}

// NewUnionClientInCluster creates a new UnionClient when running in the Kubernetes cluster.
func NewUnionClientInCluster(mutate MutateConfig) (*UnionClient, error) {
	scheme := runtime.NewScheme()
	for _, fn := range baseSchemes {
		if err := fn(scheme); err != nil {
			return nil, fmt.Errorf("add scheme failed: %w", err)
		}
	}

	restConfig, err := rest.InClusterConfig()
	if err != nil {
		return nil, fmt.Errorf("build rest config: %w", err)
	}
	if mutate != nil {
		mutate(restConfig)
	}

	// TODO: add client option
	ctrl, err := client.New(restConfig, client.Options{
		Scheme: scheme,
	})
	if err != nil {
		return nil, fmt.Errorf("build ctrl client: %w", err)
	}
	kube, err := kubernetes.NewForConfig(restConfig)
	if err != nil {
		return nil, fmt.Errorf("build kube client: %w", err)
	}

	return &UnionClient{
		Ctrl: ctrl,
		Kube: kube,
	}, nil
}

// GetOrCreate gets the object from the Kubernetes cluster if it exists, or creates it if it does not.
//
// Unlike `controllerutil.CreateOrUpdate`, it does not update objects.
func GetOrCreate(ctx context.Context, cli client.Client, obj client.Object, create func() error) (created bool, err error) {
	key := client.ObjectKeyFromObject(obj)
	err = cli.Get(ctx, key, obj)
	if err != nil {
		if client.IgnoreNotFound(err) != nil {
			return false, err
		}

		err := create()
		if err != nil {
			return false, err
		}

		err = cli.Create(ctx, obj)
		if err != nil {
			return false, err
		}
		return true, nil
	}

	return false, nil
}

// IsAPIGroupVersionSupported checks if given groupVersion is supported by the cluster.
func IsAPIGroupVersionSupported(discoveryCli discovery.DiscoveryInterface, groupVersion string) (bool, error) {
	gv, err := schema.ParseGroupVersion(groupVersion)
	if err != nil {
		return false, err
	}
	apiGroupList, err := discoveryCli.ServerGroups()
	if err != nil {
		return false, err
	}
	for _, apiGroup := range apiGroupList.Groups {
		if apiGroup.Name != gv.Group {
			continue
		}
		for _, version := range apiGroup.Versions {
			if version.GroupVersion == gv.String() {
				return true, nil
			}
		}
	}
	return false, nil
}

// IsAPIGroupSupported checks if given group is supported by the cluster.
func IsAPIGroupSupported(discoveryCli discovery.DiscoveryInterface, group string) (bool, error) {
	apiGroupList, err := discoveryCli.ServerGroups()
	if err != nil {
		return false, err
	}
	for _, apiGroup := range apiGroupList.Groups {
		if apiGroup.Name == group {
			return true, nil
		}
	}
	return false, nil
}

// IsAPIGroupVersionResourceSupported checks if given groupVersion and resource is supported by the cluster.
//
// you can exec `kubectl api-resources` to find groupVersion and resource.
func IsAPIGroupVersionResourceSupported(discoveryCli discovery.DiscoveryInterface, groupVersion string, resource string) (bool, error) {
	apiResourceList, err := discoveryCli.ServerResourcesForGroupVersion(groupVersion)
	if err != nil {
		if errors.IsNotFound(err) {
			return false, nil
		}
		return false, err
	}

	for _, apiResource := range apiResourceList.APIResources {
		if resource == apiResource.Name {
			return true, nil
		}
	}
	return false, nil
}
