package dbhubsdk

import (
	"context"
	"crypto/sha1"
	"dataservices/commons/config"
	"dataservices/commons/metrics"
	"encoding/json"
	"strconv"
	"sync"
	"time"

	"github.com/btcsuite/btcutil/base58"
	"github.com/pingcap/errors"
	"github.com/pingcap/log"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/clientcmd"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/manager"
	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
)

// TagPrefix is the prefix of label that  will be propagated to the underlay resource.
// etc: tags.tidbcloud.com/tenant then the tc created by infra-api will have the "tenant" label.
const TagPrefix = "tags.tidbcloud.com/"

// types for provider type
const (
	ProviderTypeAWS        string = "aws"
	ProviderTypeEKS        string = "eks"
	ProviderTypeGCP        string = "gcp"
	ProviderTypeAWSDevTier string = "aws-free-tier"
)

const (
	defaultQPS     = 500
	defaultBurst   = 1000
	defaultTimeout = 5 * time.Second

	infraAPIClientDefaultTimeout = 30 * time.Second
)

// API is the wrapper level to the infra API.
type InfraAPI struct {
	projectName string
	mgr         manager.Manager
	client      client.Client // we can mock this client for unit tests.
}

var (
	api  *InfraAPI
	once sync.Once
)

type InfraFacade interface {
	GetCluster(ctx context.Context, projectID uint64, clusterID uint64) (c *cloudv1.Cluster, err error)
}

type TidbUserMeta struct {
	Host             string
	Port             int
	Token            string
	ServerlessUser   string
	ServerlessPasswd string
	ExpiresAt        int64
	CreateAt         time.Time
	ClusterId        uint64
	IsDedicate       bool
	Cloud            string
	UseTLS           *bool
}

// NewAPI returns a new API instance.
func NewInfraAPI(projectName string, baseKubeCli *kubernetes.Clientset, infraAPIConfigPath string) (*InfraAPI, error) {
	scheme := runtime.NewScheme()
	err := cloudv1.AddToScheme(scheme)
	if err != nil {
		return nil, errors.Annotate(err, "register infra API cloudv1")
	}

	err = corev1.AddToScheme(scheme)
	if err != nil {
		return nil, errors.Annotate(err, "register infra API corev1")
	}

	var restCfg *rest.Config
	log.Info("NewInfraAPI configPath is " + infraAPIConfigPath)
	if infraAPIConfigPath != "" {
		restCfg, err = clientcmd.BuildConfigFromFlags("", infraAPIConfigPath)
	} else {
		restCfg, err = GetInfraAPIKubeConfigFromBase(baseKubeCli)
	}
	if err != nil {
		return nil, errors.Annotate(err, "retrieve infra API server kubeconfig failed")
	}

	// set timeout for infra api client, ref to https://github.com/kubernetes/client-go/blob/master/rest/config.go
	restCfg.Timeout = infraAPIClientDefaultTimeout

	mgr, err := ctrl.NewManager(
		restCfg, ctrl.Options{
			Scheme:         scheme,
			LeaderElection: false,
			Metrics:        metricsserver.Options{BindAddress: "0"},
		},
	)
	if err != nil {
		return nil, errors.Annotate(err, "create controller-runtime manager failed")
	}

	// we need to start it to use the client.
	// we never stop it now.
	go func() {
		err := mgr.Start(context.TODO())
		if err != nil {
			panic("failed to start mgr: " + err.Error())
		}
	}()

	return &InfraAPI{
		projectName: projectName,
		mgr:         mgr,
		client:      mgr.GetClient(),
	}, nil
}

// GetInfraAPIKubeConfigFromBase get the infra API server kubeconfig from the base cluster.
func GetInfraAPIKubeConfigFromBase(baseCli kubernetes.Interface) (*rest.Config, error) {
	secretNamespace := "infra"
	secretName := "api-server-kubeconfig"
	if config.IsDev() {
		secretName = "gardener-provider-infra-kubeconfig"
	}
	secret, err := baseCli.CoreV1().Secrets(secretNamespace).Get(context.TODO(), secretName, metav1.GetOptions{})
	if err != nil {
		return nil, err
	}
	kubeconfig, err := clientcmd.RESTConfigFromKubeConfig(secret.Data["config"])
	if err != nil {
		return nil, err
	}
	log.Info("Build infra api from base succeed.")
	return kubeconfig, nil
}

func (a *InfraAPI) GetCluster(ctx context.Context, projectID uint64, clusterID uint64) (c *cloudv1.Cluster, err error) {
	c = new(cloudv1.Cluster)
	ns := Namespace(a.projectName, projectID)
	name := ClusterName(clusterID)

	err = a.client.Get(ctx, types.NamespacedName{Namespace: ns, Name: name}, c)
	if err != nil {
		return nil, err
	}
	return
}

func (a *InfraAPI) GetUserMeta(ctx context.Context, projectId, clusterId uint64) (userMeta *TidbUserMeta, err error) {
	start := time.Now()
	defer func() {
		metrics.ClusterInfoCounter.WithLabelValues(strconv.FormatUint(projectId, 10), strconv.FormatUint(clusterId, 10), strconv.FormatBool(err == nil)).Inc()
		metrics.GetUserMetaDuration.Observe(time.Since(start).Seconds())
	}()

	cls, err := a.GetCluster(ctx, projectId, clusterId)
	if err != nil {
		return nil, err
	}
	user := &TidbUserMeta{
		CreateAt:  GetClusterCreatedAt(cls),
		ClusterId: clusterId,
	}
	if cls.Status.SqlEndpoints.PublicEndpoint != nil {
		user.Host = cls.Status.SqlEndpoints.PublicEndpoint.Host
		user.Port = cls.Status.SqlEndpoints.PublicEndpoint.Port
		user.Token = cls.Status.SqlEndpoints.PublicEndpoint.Token
		user.ServerlessUser = GetServerlessUser(cls)
		user.ServerlessPasswd = GetServerlessPassword(cls)
		if user.Host == "" || user.Port <= 0 || user.Token == "" || user.ServerlessPasswd == "" {
			tilog.Error(ctx, "cluster info (public endpoint) not complete",
				zap.String("host", user.Host),
				zap.String("port", user.Host),
				zap.String("token", user.Token),
				zap.String("id", user.ServerlessPasswd))
			return user, errors.New("public endpoint miss auth param")
		}
	} else if cls.Status.SqlEndpoints.PrivateLinkService.AWS != nil {
		user.Port = int(*cls.Status.SqlEndpoints.PrivateLinkService.AWS.Port)
		if user.Port <= 0 {
			tilog.Error(ctx, "cluster info (private endpoint) not complete",
				zap.String("port", user.Host))
			return user, errors.New("private endpoint miss auth param")
		}
	} else {
		return nil, errors.New("cluster miss basic endpoint info")
	}

	return user, nil
}

func createK8sClientset(kubeConfigPath string) (*kubernetes.Clientset, error) {
	var conf *rest.Config
	var err error
	if kubeConfigPath != "" {
		conf, err = clientcmd.BuildConfigFromFlags("", kubeConfigPath)
	} else {
		conf, err = rest.InClusterConfig()
	}
	if err != nil {
		return nil, errors.Annotate(err, "initialize the kubernetes config failed")
	}

	// Overwrite some configurations.
	conf.QPS = defaultQPS
	conf.Burst = defaultBurst
	conf.Timeout = defaultTimeout

	baseKubeCli, err := kubernetes.NewForConfig(conf)
	if err != nil {
		return nil, errors.Annotate(err, "initialize the base kubeClient failed")
	}

	return baseKubeCli, nil
}

func ClusterName(id uint64) string {
	return strconv.FormatUint(id, 10)
}

func NetworkName(providerType string, region string) string {
	return providerType + "-" + region
}

// Namespace return the namespace of infra-api
// projectName is any name of one instance of dbaas deployment can can not be changed.
func Namespace(projectName string, projectID uint64) string {
	return projectName + "-" + strconv.FormatUint(projectID, 10)
}

type ClusterMetadata struct {
	ID          uint64    `json:"id,omitempty"`
	Name        string    `json:"name,omitempty"`
	TenantID    uint64    `json:"tenant-id,omitempty"`
	ProjectID   uint64    `json:"project-id,omitempty"`
	CreatedAt   time.Time `json:"created-at,omitempty"`
	CreatedByID uint64    `json:"created-by-id,omitempty"`
	Salt        []byte    `json:"salt,omitempty"`
}

func GetServerlessUser(cluster *cloudv1.Cluster) string {
	return cluster.Status.SqlEndpoints.PublicEndpoint.Token + ".playground"
}
func GetServerlessPassword(cluster *cloudv1.Cluster) string {
	v := cluster.Annotations["cluster.tidbcloud.com/metadata"]
	if v == "" {
		return ""
	}
	metadata := &ClusterMetadata{}
	if err := json.Unmarshal([]byte(v), metadata); err != nil {
		return ""
	}
	createAt, _ := metadata.CreatedAt.GobEncode()
	h := sha1.New()
	h.Write(metadata.Salt)
	h.Write(createAt)
	return base58.Encode(h.Sum(nil))[:15]
}

func GetClusterCreatedAt(cluster *cloudv1.Cluster) time.Time {
	layout := "2006-01-02T15:04:05Z"
	t, _ := time.Parse(layout, layout)
	v := cluster.Annotations["cluster.tidbcloud.com/metadata"]
	if v == "" {
		return t
	}
	metadata := &ClusterMetadata{}
	if err := json.Unmarshal([]byte(v), metadata); err != nil {
		return t
	}
	return metadata.CreatedAt
}
