package k8s

import (
	"context"
	"flag"
	"fmt"
	"log"

	eksv1beta1 "github.com/crossplane-contrib/provider-aws/apis/eks/v1beta1"
	gkev1beta2 "github.com/crossplane-contrib/provider-gcp/apis/container/v1beta2"
	cpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"helm.sh/helm/v3/pkg/action"
	corev1 "k8s.io/api/core/v1"
	"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"
	"k8s.io/utils/pointer"
	"sigs.k8s.io/aws-iam-authenticator/pkg/token"
	"sigs.k8s.io/controller-runtime/pkg/client"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	"github.com/tidbcloud/infra-provider/pkg/util"
)

const (
	// Namespace for Helm to store data.
	HelmNamespace = "helm"
)

var (
	injectedKubeconfig = flag.String("injected-kubeconfig", "", "Injected kubeconfig for development")
)

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

var _ ClientGetter = &clientGetter{}

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

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

	// Created in the construction
	clientManager ClientManager
	tokenGen      token.Generator
}

type EmptyGetter struct{}

var _ ClientGetter = &EmptyGetter{}

func (g *EmptyGetter) GetClient(context.Context) (client.Client, error) {
	return nil, fmt.Errorf("empty getter")
}
func (g *EmptyGetter) GetHelmConfig(context.Context) (*action.Configuration, error) {
	return nil, fmt.Errorf("empty getter")
}

type ClusterMetaGetter interface {
	client.Object
	GetClusterName() string
	GetRegion() string
	GetProvider() string
	GetNetwork() *cloudv1.Network
}

type networkWrapper struct {
	*cloudv1.Network
}

func (n *networkWrapper) GetProvider() string {
	return n.Network.Spec.Provider
}

func (n *networkWrapper) GetRegion() string {
	return n.Network.Spec.Region
}

func (n *networkWrapper) GetClusterName() string {
	return namer.LegacyBuildClusterNameByNetwork(n.Network)
}

func (n *networkWrapper) GetNetwork() *cloudv1.Network {
	return n.Network
}

var _ ClusterMetaGetter = &networkWrapper{}

// NetworkClientGetter return the getter to get k8s client.
func NetworkClientGetter(ctx context.Context, cli client.Client, network *cloudv1.Network, scheme *runtime.Scheme) (ClientGetter, error) {
	return ClusterClientGetter(ctx, cli, &networkWrapper{network}, scheme)
}

func ClusterClientGetter(ctx context.Context, cli client.Client, meta ClusterMetaGetter, scheme *runtime.Scheme) (ClientGetter, error) {
	name := namer.LegacyBuildClusterNameByNetwork(meta.GetNetwork())
	var clientOptions client.Options
	if scheme != nil {
		clientOptions = client.Options{
			Scheme: scheme,
		}
	}
	var err error
	var ready bool
	tokenGen, err := token.NewGenerator(false, false)
	if err != nil {
		return nil, err
	}

	g := &clientGetter{
		cli:           cli,
		clientOptions: clientOptions,
		clusterName:   name,
		clusterRegion: meta.GetRegion(),
		tokenGen:      tokenGen,
	}
	// Errors from the client getter have different causations which can be hard to identify.
	// Ensure the connection is ready to separate this type of error.
	if *injectedKubeconfig != "" {
		g.injectedKubeconfig = *injectedKubeconfig
		return g, nil
	}

	switch meta.GetProvider() {
	case consts.ProviderTypeEKS:
		g.clientManager = NewEKSClientManager(cli, clientOptions)
		// wait cluster ready
		ready, err = eksReady(ctx, cli, name)
		if err != nil {
			return nil, err
		}
		if !ready {
			return nil, util.WrapError("connection is not ready", err)
		}
		return g, nil
	case consts.ProviderTypeGKE:
		g.clientManager = NewGKEClientManager(cli, clientOptions)
		// wait cluster ready
		var ready bool
		ready, err = gkeReady(ctx, cli, name)
		if err != nil {
			return nil, err
		}
		if !ready {
			return nil, util.WrapError("connection is not ready", err)
		}
		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) 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) GetHelmConfig(ctx context.Context) (*action.Configuration, error) {
	configFlags := genericclioptions.NewConfigFlags(false)

	if g.injectedKubeconfig != "" {
		configFlags.KubeConfig = &g.injectedKubeconfig
	} else {
		restCfg, err := g.clientManager.GetConfig(context.TODO(), types.NamespacedName{Name: g.clusterName})
		if err != nil {
			return nil, util.WrapError("failed to get remote kubeconfig ", err)
		}
		// 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
		configFlags.APIServer = &restCfg.Host
		configFlags.Insecure = pointer.Bool(true)
		configFlags.WrapConfigFn = func(_ *rest.Config) *rest.Config {
			return restCfg
		}
	}
	var c action.Configuration
	if err := c.Init(
		configFlags,
		HelmNamespace,
		"configmap", // use configmap as the driver
		log.Printf,
	); err != nil {
		return nil, err
	}

	return &c, nil
}

func eksReady(ctx context.Context, cli client.Client, name string) (ready bool, err error) {
	eks := &eksv1beta1.Cluster{}
	err = cli.Get(ctx, types.NamespacedName{Name: name}, eks)
	if err != nil {
		return false, err
	}

	return checkClusterStatus(eks.Status.Conditions)
}

func gkeReady(ctx context.Context, cli client.Client, name string) (ready bool, err error) {
	gke := &gkev1beta2.Cluster{}
	err = cli.Get(ctx, types.NamespacedName{Name: name}, gke)
	if err != nil {
		return false, err
	}

	return checkClusterStatus(gke.Status.Conditions)
}

func checkClusterStatus(Conditions []cpv1.Condition) (ready bool, err error) {
	for _, cond := range Conditions {
		if cond.Type == cpv1.TypeReady && cond.Status == corev1.ConditionTrue {
			return true, nil
		}
	}

	return false, nil
}
