package k8scliv2

import (
	"context"
	"fmt"
	"log"

	"github.com/pingcap/errors"
	"helm.sh/helm/v3/pkg/action"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/cli-runtime/pkg/genericclioptions"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/clientcmd"
	resourceclient "k8s.io/metrics/pkg/client/clientset/versioned/typed/metrics/v1beta1"
	"k8s.io/utils/pointer"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/observability/agent-control-server/pkg/config"
	"github.com/tidbcloud/observability/agent-control-server/pkg/consts"
)

const (
	HelmNamespace = "helm"
)

type ClientManager interface {
	GetClient(ctx context.Context, cluster client.ObjectKey) (client.Client, error)
	GetMetricsClient(ctx context.Context, cluster client.ObjectKey) (resourceclient.MetricsV1beta1Interface, error)
	GetConfig(ctx context.Context, eksKey client.ObjectKey) (*rest.Config, error)
}

type ClientGetter interface {
	GetClient(context.Context) (client.Client, error)
	GetMetricsClient(context.Context) (resourceclient.MetricsV1beta1Interface, error)
	GetHelmConfig(context.Context) (*action.Configuration, error)
}

type clientGetter struct {
	cli           client.Client
	clientOptions client.Options

	// External information of the EKS cluster
	clusterName        string
	injectedKubeconfig string

	// Created in the construction
	clientManager ClientManager
}

// NetworkClientGetter return the getter to get k8s client.
func NetworkClientGetter(cli client.Client, scheme *runtime.Scheme, provider, clusterName string, rootConfig *config.RootConfig) (ClientGetter, error) {
	var clientOptions client.Options
	if scheme != nil {
		clientOptions = client.Options{
			Scheme: scheme,
		}
	}

	g := &clientGetter{
		cli:           cli,
		clientOptions: clientOptions,
		clusterName:   clusterName,
	}

	serverlessMode := rootConfig.IsServerlessMode()
	restConfig := ctrl.GetConfigOrDie()
	switch provider {
	case consts.ProviderTypeEKS:
		if serverlessMode {
			g.clientManager = NewEKSLocalClientManager(restConfig, clientOptions)
		} else {
			g.clientManager = NewEKSTenantClientManager(cli, clientOptions)
		}
		return g, nil

	case consts.ProviderTypeGKE:
		if serverlessMode {
			g.clientManager = NewGKELocalClientManager(restConfig, clientOptions)
		} else {
			g.clientManager = NewGKETenantClientManager(cli, clientOptions)
		}
		return g, nil

	case consts.ProviderTypeACK:
		if serverlessMode {
			g.clientManager = NewACKLocalClientManager(restConfig, clientOptions)
		} else {
			g.clientManager = NewACKTenantClientManager(cli, clientOptions, rootConfig.DataplaneAssumeRoleName)
		}
		return g, nil

	case consts.ProviderTypeAKS:
		g.clientManager = NewAKSClientManager(cli, clientOptions)
		return g, nil

	default:
		return nil, fmt.Errorf("unknown provider type")
	}
}

func (g *clientGetter) GetClient(ctx context.Context) (client.Client, error) {
	if g.injectedKubeconfig != "" {
		return g.getClientFromKubeconfig()
	}
	return g.clientManager.GetClient(ctx, types.NamespacedName{Name: g.clusterName})
}

func (g *clientGetter) GetMetricsClient(ctx context.Context) (resourceclient.MetricsV1beta1Interface, error) {
	if g.injectedKubeconfig != "" {
		return g.getMetricsClientFromKubeconfig()
	}
	return g.clientManager.GetMetricsClient(ctx, types.NamespacedName{Name: g.clusterName})
}

func (g *clientGetter) getClientFromKubeconfig() (client.Client, error) {
	conf, err := clientcmd.BuildConfigFromFlags("", g.injectedKubeconfig)
	if err != nil {
		return nil, err
	}
	return client.New(conf, g.clientOptions)
}

func (g *clientGetter) getMetricsClientFromKubeconfig() (resourceclient.MetricsV1beta1Interface, error) {
	conf, err := clientcmd.BuildConfigFromFlags("", g.injectedKubeconfig)
	if err != nil {
		return nil, err
	}
	return resourceclient.NewForConfig(conf)
}

func (g *clientGetter) GetHelmConfig(ctx context.Context) (*action.Configuration, error) {
	kubeconfig := genericclioptions.NewConfigFlags(false)
	var err error
	if g.injectedKubeconfig != "" {
		kubeconfig, err = g.getKubeconfigForHelm(ctx)
	} else {
		restCfg, err := g.clientManager.GetConfig(context.Background(), types.NamespacedName{Name: g.clusterName})
		if err != nil {
			return nil, errors.Wrap(err, "failed to get remote kubeconfig")
		}
		// https://github.com/helm/helm/issues/11047
		// helm client can't use rest.Config directly
		// The Manager is constructed from rest.Config and rest.Config is created from configFlags
		// So here we just reverse the process to construct configFlags from rest.Config
		kubeconfig.APIServer = &restCfg.Host
		kubeconfig.Insecure = pointer.Bool(true)
		kubeconfig.WrapConfigFn = func(_ *rest.Config) *rest.Config {
			return restCfg
		}
	}
	if err != nil {
		return nil, errors.Wrap(err, "failed to get remote kubeconfig")
	}
	var c action.Configuration
	err = c.Init(
		kubeconfig,
		HelmNamespace,
		"configmap", // use configmap as the driver
		log.Printf,
	)
	if err != nil {
		return nil, err
	}
	return &c, nil
}

func (g *clientGetter) getKubeconfigForHelm(_ctx context.Context) (*genericclioptions.ConfigFlags, error) {
	kubeconfig := genericclioptions.NewConfigFlags(false)
	kubeconfig.KubeConfig = &g.injectedKubeconfig
	return kubeconfig, nil
}

type CacheClientGetter struct {
	getter ClientGetter

	cli  client.Client
	conf *action.Configuration
}

// NewCacheClientGetter creates a new ClientGetter that caches the client and helm config.
// To void the underlay getter build the client every time.
func NewCacheClientGetter(getter ClientGetter) *CacheClientGetter {
	return &CacheClientGetter{
		getter: getter,
	}
}

// GetClient implements ClientGetter interface.
func (c *CacheClientGetter) GetClient(ctx context.Context) (client.Client, error) {
	if c.cli != nil {
		return c.cli, nil
	}

	cli, err := c.getter.GetClient(ctx)
	if err != nil {
		return nil, err
	}

	c.cli = cli
	return c.cli, nil
}

// GetHelmConfig implements ClientGetter interface.
func (c *CacheClientGetter) GetHelmConfig(ctx context.Context) (*action.Configuration, error) {
	if c.conf != nil {
		return c.conf, nil
	}

	conf, err := c.getter.GetHelmConfig(ctx)
	if err != nil {
		return nil, err
	}

	c.conf = conf
	return c.conf, nil
}
