package k8s

import (
	"context"
	"fmt"
	"sync"
	"time"

	"golang.org/x/sync/singleflight"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/cli-runtime/pkg/genericclioptions"
	"k8s.io/client-go/dynamic"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/client-go/tools/clientcmd/api"
	"k8s.io/utils/pointer"

	"github.com/pingcap/test-infra/sdk/pkg/tidbcloud"
	"github.com/pingcap/test-infra/sdk/pkg/util"
)

type TiDBCloudKubeClientSet interface {
	getBaseKubeClient() (*KubeClientSet, error)
	GardenKubeClient() *KubeClientSet
	GetShootKubeConfig(ctx context.Context, projectID string, cloudProvider string, region string) (kubeConfig []byte, err error)
}

var (
	_ TiDBCloudKubeClientSet = &tidbCloudKubeClient{}
	_ TiDBCloudKubeClientSet = &fakeTiDBCloudClient{}
)

type tidbCloudEKSConfiguration struct {
	region                   string
	baseEKSClusterName       string
	baseEKSAPIServerEndpoint string
	baseEKSAssumeRole        string
	shootKubeConfigSecretNS  string
}

const baseKubeClientCacheKey = "base"

type tidbCloudKubeClient struct {
	config *tidbCloudEKSConfiguration

	singleflight                singleflight.Group
	baseEKSKubeClientExpiration time.Time

	baseKubeClient   *KubeClientSet
	gardenKubeClient *KubeClientSet

	shootKubeConfigCache sync.Map
}

func NewTiDBCloudKubeClientSet(config *tidbcloud.TiDBCloudConfiguration) (TiDBCloudKubeClientSet, error) {
	baseKubeClient, baseExpiration, err := newTiDBCloudBaseKubeClient(config.Region, config.BaseEKSClusterName, config.BaseEKSAPIServerEndpoint, config.BaseEKSAssumeRole)
	if err != nil {
		return nil, err
	}
	gardenKubeClient, err := newTiDBCloudGardenerKubeClient(*baseKubeClient)
	if err != nil {
		return nil, err
	}

	return &tidbCloudKubeClient{
		config: &tidbCloudEKSConfiguration{
			region:                   config.Region,
			baseEKSClusterName:       config.BaseEKSClusterName,
			baseEKSAPIServerEndpoint: config.BaseEKSAPIServerEndpoint,
			baseEKSAssumeRole:        config.BaseEKSAssumeRole,
			shootKubeConfigSecretNS:  config.ShootKubeConfigSecretNS,
		},
		baseEKSKubeClientExpiration: baseExpiration,
		baseKubeClient:              baseKubeClient,
		gardenKubeClient:            gardenKubeClient,
	}, nil
}

func (t *tidbCloudKubeClient) getBaseKubeClient() (*KubeClientSet, error) {
	baseKubeClient, err, _ := t.singleflight.Do(baseKubeClientCacheKey, func() (interface{}, error) {
		if t.baseEKSKubeClientExpiration.After(time.Now()) {
			return t.baseKubeClient, nil
		}

		baseKubeClient, baseExpiration, err := newTiDBCloudBaseKubeClient(
			t.config.region,
			t.config.baseEKSClusterName,
			t.config.baseEKSAPIServerEndpoint,
			t.config.baseEKSAssumeRole,
		)
		if err != nil {
			return nil, err
		}

		t.baseEKSKubeClientExpiration = baseExpiration
		t.baseKubeClient = baseKubeClient
		return t.baseKubeClient, err
	})

	return baseKubeClient.(*KubeClientSet), err
}

func (t *tidbCloudKubeClient) GardenKubeClient() *KubeClientSet {
	return t.gardenKubeClient
}

func (t *tidbCloudKubeClient) GetShootKubeConfig(ctx context.Context, projectID string, cloudProvider string, region string) (kubeConfig []byte, err error) {
	shootName := util.TiDBCloudShootName(projectID, cloudProvider, region)
	shootKubeConfig, ok := t.shootKubeConfigCache.Load(shootName)
	if ok {
		return shootKubeConfig.([]byte), nil
	}

	// get shoot kubeconfig from secret
	secretName := fmt.Sprintf("%s.kubeconfig", shootName)
	result, err, _ := t.singleflight.Do(shootName, func() (interface{}, error) {
		secret, innerErr := t.GardenKubeClient().ClientSet.CoreV1().Secrets(t.config.shootKubeConfigSecretNS).Get(ctx, secretName, metav1.GetOptions{})
		if innerErr != nil {
			return nil, innerErr
		}

		t.shootKubeConfigCache.Store(shootName, secret.Data["kubeconfig"])
		return secret.Data["kubeconfig"], nil
	})
	if err != nil {
		return nil, err
	}

	return result.([]byte), nil
}

func newTiDBCloudBaseKubeClient(region string, clusterName string, apiServerEndpoint string, assumeRoleARN string) (kubeClient *KubeClientSet, expiration time.Time, err error) {
	// code copied from: https://github.com/tidbcloud/eks-provider/blob/d6b30aa1e7e8b711d9f79e6eabba19c0e1013b03/pkg/k8s/eks_client_mngr.go#L57
	kubeconfig := genericclioptions.NewConfigFlags(false)
	kubeconfig.Insecure = pointer.Bool(true)

	kubeconfig.APIServer = &apiServerEndpoint
	// Construct arguments for the authenticator
	args := []string{
		"token",
		"--region", region,
		"--cluster-id", clusterName,
		"--role", assumeRoleARN,
	}
	// fill the exec plugin info
	kubeconfig.WrapConfigFn = func(c *rest.Config) *rest.Config {
		c.ExecProvider = &api.ExecConfig{
			APIVersion:      kubectlExecConfigAPIVersion,
			InteractiveMode: api.NeverExecInteractiveMode,
			Command:         "aws-iam-authenticator",
			Args:            args,
		}
		return c
	}

	var k = &KubeClientSet{}

	k.clientConfig, err = kubeconfig.ToRESTConfig()
	if err != nil {
		return nil, time.Time{}, err
	}

	if k.ClientSet, err = kubernetes.NewForConfig(k.clientConfig); err != nil {
		return nil, time.Time{}, err
	}
	if k.DynamicClient, err = dynamic.NewForConfig(k.clientConfig); err != nil {
		return nil, time.Time{}, err
	}

	return k, time.Now().Add(baseKubeClientLease), err
}

func newTiDBCloudGardenerKubeClient(baseCli KubeClientSet) (*KubeClientSet, error) {
	var k = &KubeClientSet{}

	// see: https://github.com/tidbcloud/config-service/blob/master/gardener/util.go#L25
	const (
		gardenNamespace          = "garden"
		gardenKubeconfigForAdmin = "garden-kubeconfig-for-admin"
	)

	kubeConfigSecret, err := baseCli.ClientSet.CoreV1().Secrets(gardenNamespace).Get(context.TODO(), gardenKubeconfigForAdmin, metav1.GetOptions{})
	if err != nil {
		return nil, err
	}

	k.clientConfig, err = clientcmd.RESTConfigFromKubeConfig(kubeConfigSecret.Data["kubeconfig"])
	if err != nil {
		return nil, err
	}

	if k.ClientSet, err = kubernetes.NewForConfig(k.clientConfig); err != nil {
		return nil, err
	}
	if k.DynamicClient, err = dynamic.NewForConfig(k.clientConfig); err != nil {
		return nil, err
	}

	return k, nil
}

type fakeTiDBCloudClient struct{}

func NewFakeTiDBCloudClient() TiDBCloudKubeClientSet {
	return &fakeTiDBCloudClient{}
}

func (f *fakeTiDBCloudClient) getBaseKubeClient() (*KubeClientSet, error) {
	return nil, fmt.Errorf("invalid call to fakeTiDBCloudClient")
}

func (f *fakeTiDBCloudClient) GardenKubeClient() *KubeClientSet {
	return nil
}

func (f *fakeTiDBCloudClient) GetShootKubeConfig(_ context.Context, _ string, _ string, _ string) (kubeConfig []byte, err error) {
	return nil, fmt.Errorf("invalid call to fakeTiDBCloudClient")
}
