package k8s

import (
	"context"
	"flag"
	"log"

	"github.com/tidbcloud/eks-provider/api/v1alpha1"
	"github.com/tidbcloud/eks-provider/pkg/util"
	"helm.sh/helm/v3/pkg/action"

	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/cli-runtime/pkg/genericclioptions"
	"k8s.io/client-go/tools/clientcmd"
	"sigs.k8s.io/aws-iam-authenticator/pkg/token"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

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

var (
	// TODO: the injectedKubeconfig is for testing, replace it with tenant's eks client in future.
	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 = &eksGetter{}

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

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

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

type EKSRequest struct {
	ConnectionName   string
	ClusterName      string
	ClusterRegion    string
	AssumeRoleArn    string
	AssumeExternalID string
	ClientOptions    client.Options
}

func NewEKSGetter(ctx context.Context, cli client.Client, req EKSRequest) (*eksGetter, error) {
	g := &eksGetter{
		cli:              cli,
		clientOptions:    req.ClientOptions,
		clusterName:      req.ClusterName,
		clusterRegion:    req.ClusterRegion,
		assumeRoleArn:    req.AssumeRoleArn,
		assumeExternalID: req.AssumeExternalID,
	}
	tokenGen, err := token.NewGenerator(false, false)
	if err != nil {
		return nil, err
	}
	g.tokenGen = tokenGen
	g.clientManager = NewEKSClientManager(g.cli, NewEKSTokenManager(g.cli, g.tokenGen), g.clientOptions)

	// 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
	}
	key := types.NamespacedName{
		Name: req.ConnectionName,
	}
	var c v1alpha1.EKSConnection
	if err := cli.Get(ctx, key, &c); err != nil {
		return nil, util.WrapError("failed to get connection", err)
	}
	var ready bool
	for _, cond := range c.Status.Conditions {
		if cond.Type == v1alpha1.ConditionReady && cond.Status == corev1.ConditionTrue {
			ready = true
			break
		}
	}
	if !ready {
		return nil, util.WrapError("connection is not ready", err)
	}
	return g, nil
}

func (g *eksGetter) GetClient(ctx context.Context) (client.Client, error) {
	if g.injectedKubeconfig != "" {
		return g.getClientFromKubeconfig()
	}
	return g.clientManager.GetClient(ctx, EKSClientOpt{
		EKSObject: types.NamespacedName{Name: g.clusterName},
		EKSTarget: EKSTarget{
			AssumeRole: g.assumeRoleArn,
			ExternalID: g.assumeExternalID,
			Region:     g.clusterRegion,
			Cluster:    g.clusterName,
			// The namespace that been used to store tokens in the infra cluster.
			// TODO: Pass the value once the client manager logic has been modified.
			Namespace: "default",
		},
	})
}

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

func (g *eksGetter) GetHelmConfig(ctx context.Context) (*action.Configuration, error) {
	var kubeconfig *genericclioptions.ConfigFlags
	var err error
	if g.injectedKubeconfig != "" {
		kubeconfig, err = g.getKubeconfigForHelm(ctx)
	} else {
		kubeconfig, err = g.clientManager.GetConfig(context.TODO(), EKSClientOpt{
			EKSObject: types.NamespacedName{Name: g.clusterName},
			EKSTarget: EKSTarget{
				AssumeRole: g.assumeRoleArn,
				ExternalID: g.assumeExternalID,
				Region:     g.clusterRegion,
				Cluster:    g.clusterName,
				// The namespace that been used to store tokens in the infra cluster.
				// TODO: Pass the value once the client manager logic has been modified.
				Namespace: "default",
			},
		})
	}
	if err != nil {
		return nil, util.WrapError("failed to get remote kubeconfig ", err)
	}
	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 *eksGetter) getKubeconfigForHelm(ctx context.Context) (*genericclioptions.ConfigFlags, error) {
	kubeconfig := genericclioptions.NewConfigFlags(false)
	kubeconfig.KubeConfig = &g.injectedKubeconfig
	return kubeconfig, nil
}
