package k8scliv2

import (
	"context"
	"encoding/base64"
	"fmt"

	ackv1alpha1 "github.com/crossplane-contrib/provider-upjet-alibabacloud/apis/ack/v1alpha1"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/clientcmd"
	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
	resourceclient "k8s.io/metrics/pkg/client/clientset/versioned/typed/metrics/v1beta1"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

// ackTenantClientManager resolves remote kubeconfig for an ACK cluster by
// discovering a Secret that holds kubeconfig data for the target cluster.
//
// Discovery rules (in order):
//  1. List Secrets labeled with tags.tidbcloud.com/cluster=<clusterName>
//  2. Pick the first one that contains data key "kubeconfig" or "config"
//
// Notes:
// - We intentionally do not assume a fixed namespace to allow flexibility.
// - The kubeconfig content is expected to be a full kubeconfig file in YAML.
type ackTenantClientManager struct {
	cli            client.Client
	option         client.Options
	assumeRoleName string
}

func NewACKTenantClientManager(cli client.Client, opt client.Options, role string) ClientManager {
	return &ackTenantClientManager{cli: cli, option: opt, assumeRoleName: role}
}

func (m *ackTenantClientManager) GetClient(ctx context.Context, cluster client.ObjectKey) (client.Client, error) {
	cfg, err := m.GetConfig(ctx, cluster)
	if err != nil {
		return nil, err
	}
	return client.New(cfg, m.option)
}

func (m *ackTenantClientManager) GetMetricsClient(ctx context.Context, cluster client.ObjectKey) (resourceclient.MetricsV1beta1Interface, error) {
	cfg, err := m.GetConfig(ctx, cluster)
	if err != nil {
		return nil, err
	}
	return resourceclient.NewForConfig(cfg)
}

func (m *ackTenantClientManager) GetConfig(ctx context.Context, ackKey client.ObjectKey) (*rest.Config, error) {
	ack := &ackv1alpha1.ManagedKubernetes{}
	err := m.cli.Get(ctx, ackKey, ack)
	if err != nil {
		return nil, err
	}

	var assumeRoleARN string
	if ack.Spec.ProviderConfigReference != nil && ack.Spec.ProviderConfigReference.Name != "" {
		assumeRoleARN = fmt.Sprintf("acs:ram::%s:role/%s", ack.Spec.ProviderConfigReference.Name, m.assumeRoleName)
	}
	cfg, err := buildACKKubeconfig(ack, assumeRoleARN)
	if err != nil {
		return nil, err
	}
	return clientcmd.NewDefaultClientConfig(*cfg, nil).ClientConfig()
}

// buildACKKubeconfig builds a kubeconfig from the given ACK cluster object.
// If assumeRoleARN is provided, it will be used in the exec command for authentication.
func buildACKKubeconfig(ack *ackv1alpha1.ManagedKubernetes, assumeRoleARN string) (*clientcmdapi.Config, error) {
	// construct cluster info
	cluster := clientcmdapi.NewCluster()
	if cert, ok := ack.Status.AtProvider.CertificateAuthority["cluster_cert"]; ok && cert != nil {
		ca, err := base64.StdEncoding.DecodeString(*cert)
		if err != nil {
			return nil, fmt.Errorf("failed to decode ack ca data: %w", err)
		}
		cluster.CertificateAuthorityData = ca
	} else {
		cluster.InsecureSkipTLSVerify = true
	}

	// only support public access
	if server, ok := ack.Status.AtProvider.Connections["api_server_internet"]; !ok || server == nil {
		return nil, fmt.Errorf("no public endpoint found for ack")
	} else {
		cluster.Server = *server
	}

	// construct auth info
	auth := clientcmdapi.NewAuthInfo()
	if ack.Status.AtProvider.ID == nil {
		return nil, fmt.Errorf("no cluster id found")
	}
	clusterId := *ack.Status.AtProvider.ID
	args := []string{
		"credential-plugin",
		"get-token",
		"--cluster-id", clusterId,
		"--api-version", "v1beta1",
		"--log-level", "error",
	}
	if assumeRoleARN != "" {
		args = append(args, []string{"--role-arn", assumeRoleARN}...)
	}
	auth.Exec = &clientcmdapi.ExecConfig{
		APIVersion:      v1execConfigAPIVersionV1beta1,
		InteractiveMode: clientcmdapi.NeverExecInteractiveMode,
		Command:         "ack-ram-tool",
		Args:            args,
	}

	cfgctx := clientcmdapi.NewContext()
	cfgctx.Cluster = ack.Name
	cfgctx.AuthInfo = ack.Name

	cfg := clientcmdapi.NewConfig()
	cfg.Contexts[ack.Name] = cfgctx
	cfg.Clusters[ack.Name] = cluster
	cfg.AuthInfos[ack.Name] = auth
	cfg.CurrentContext = ack.Name

	return cfg, nil
}

type ackLocalClientManager struct {
	restCfg *rest.Config
	option  client.Options
}

func NewACKLocalClientManager(restCfg *rest.Config, opt client.Options) ClientManager {
	return &ackLocalClientManager{
		restCfg: restCfg,
		option:  opt,
	}
}

func (mgr *ackLocalClientManager) GetConfig(ctx context.Context, _ client.ObjectKey) (*rest.Config, error) {
	return mgr.restCfg, nil
}

func (mgr *ackLocalClientManager) GetClient(ctx context.Context, _ client.ObjectKey) (client.Client, error) {
	return client.New(mgr.restCfg, mgr.option)
}

func (mgr *ackLocalClientManager) GetMetricsClient(ctx context.Context, _ client.ObjectKey) (resourceclient.MetricsV1beta1Interface, error) {
	return resourceclient.NewForConfig(mgr.restCfg)
}
