package k8s

import (
	"context"
	"database/sql"
	_ "embed"
	"encoding/json"
	"fmt"
	"net/url"
	"time"

	httptransport "github.com/go-openapi/runtime/client"
	"github.com/go-openapi/strfmt"
	"github.com/pingcap/errors"
	prometheus "github.com/prometheus/client_golang/api"
	prometheus_api "github.com/prometheus/client_golang/api/prometheus/v1"
	"github.com/prometheus/common/model"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/util/wait"
	"k8s.io/client-go/util/retry"
	"k8s.io/utils/pointer"

	apiresource "github.com/pingcap/test-infra/common/model/resource"
	"github.com/pingcap/test-infra/sdk/dbaas_central/client"
	"github.com/pingcap/test-infra/sdk/dbaas_central/client/backup_service"
	"github.com/pingcap/test-infra/sdk/dbaas_central/client/cluster_service"
	"github.com/pingcap/test-infra/sdk/dbaas_central/client/import_service"
	"github.com/pingcap/test-infra/sdk/dbaas_central/models"
	"github.com/pingcap/test-infra/sdk/pkg/prom"
	"github.com/pingcap/test-infra/sdk/pkg/util"
	"github.com/pingcap/test-infra/sdk/resource"
)

var (
	clusterConfigGVR = schema.GroupVersionResource{Group: "ops.tidbcloud.com", Version: "v1alpha1", Resource: "clusterconfigs"}
	restoreGVR       = schema.GroupVersionResource{Group: "pingcap.com", Version: "v1alpha1", Resource: "restores"}

	_ resource.TiDBCloudCluster = &TiDBCloudCluster{}

	//go:embed assets/tidbcloud-br-restore.yaml
	tidbcloudClusterBRRestoreSpec        []byte
	defaultTidbcloudClusterBRRestoreSpec map[string]interface{}
)

func init() {
	mustUnmarshalYaml(tidbcloudClusterBRRestoreSpec, &defaultTidbcloudClusterBRRestoreSpec)
}

type TiDBCloudCluster struct {
	KubeClientSet
	tidbCloudClient          *client.DbaasCentral
	tidbCloudShootKubeClient *KubeClientSet
	apiresource.TiDBCloudCluster

	// tidbcloud openapi request needed parameters here, almost of them are unchanged after created except the token
	shootKubeConfig []byte
	tidbCloudURL    string
	clusterID       string
	// FIXME: token is expired after 24h and we need remember refresh it on rms
	token                  string
	accountEmail           string
	orgID                  string
	projectID              string
	region                 string
	rootPassword           string
	prometheusURL          string
	connect                apiresource.TiDBCloudClusterConnect
	tidbCloudConfigSpec    apiresource.TiDBCloudClusterConfigSpec
	tidbCloudClusterConfig *unstructured.Unstructured // ClusterConfig CR object
}

func (t *TiDBCloudCluster) ChaosClient() resource.ChaosClient {
	return &ChaosClientV2{KubeClientSet: *t.tidbCloudShootKubeClient}
}

func (t *TiDBCloudCluster) ChaosSelect(selector resource.TiDBClusterSelectorOpts) ([]resource.ChaosSelector, error) {
	var selectors []resource.ChaosSelector
	if selector.TiDBClusterTiDBSelectorSpec != nil {
		spec := selector.TiDBClusterTiDBSelectorSpec
		switch {
		case spec.Peer != nil:
			pod, err := t.tidbCloudShootKubeClient.ClientSet.CoreV1().Pods(t.tidbCloudShootKubeClient.Namespace).Get(context.Background(), fmt.Sprintf("%s-tidb-%d", t.tcName(), *spec.Peer), metav1.GetOptions{})
			if err != nil {
				return nil, err
			}
			selectors = append(selectors, &PodSelector{pod})
		case spec.All:
			pods, err := t.tidbCloudShootKubeClient.ClientSet.CoreV1().Pods(t.tidbCloudShootKubeClient.Namespace).List(context.TODO(), metav1.ListOptions{
				LabelSelector: labels.Set{"app.kubernetes.io/instance": t.tcName(), "app.kubernetes.io/component": "tidb"}.AsSelector().String(),
			})
			if err != nil {
				return nil, err
			}
			for _, tidb := range pods.Items {
				pod := tidb
				selectors = append(selectors, &PodSelector{&pod})
			}
		}
	}
	if selector.TiDBClusterPDSelectorSpec != nil {
		spec := selector.TiDBClusterPDSelectorSpec
		switch {
		case spec.Peer != nil:
			pod, err := t.tidbCloudShootKubeClient.ClientSet.CoreV1().Pods(t.tidbCloudShootKubeClient.Namespace).Get(context.Background(), fmt.Sprintf("%s-pd-%d", t.tcName(), *spec.Peer), metav1.GetOptions{})
			if err != nil {
				return nil, err
			}
			selectors = append(selectors, &PodSelector{pod})
		case spec.All:
			pods, err := t.tidbCloudShootKubeClient.ClientSet.CoreV1().Pods(t.tidbCloudShootKubeClient.Namespace).List(context.TODO(), metav1.ListOptions{
				LabelSelector: labels.Set{"app.kubernetes.io/instance": t.tcName(), "app.kubernetes.io/component": "pd"}.AsSelector().String(),
			})
			if err != nil {
				return nil, err
			}
			for _, pd := range pods.Items {
				pod := pd
				selectors = append(selectors, &PodSelector{&pod})
			}
		case spec.IsLeader != nil:
			return nil, fmt.Errorf("unimpl")
		}
	}
	if selector.TiDBClusterTiKVSelectorSpec != nil {
		spec := selector.TiDBClusterTiKVSelectorSpec
		switch {
		case spec.Peer != nil:
			pod, err := t.tidbCloudShootKubeClient.ClientSet.CoreV1().Pods(t.tidbCloudShootKubeClient.Namespace).Get(context.Background(), fmt.Sprintf("%s-tikv-%d", t.tcName(), *spec.Peer), metav1.GetOptions{})
			if err != nil {
				return nil, err
			}
			selectors = append(selectors, &PodSelector{pod})
		case spec.StoreLabelSelector != nil:
			selector, err := metav1.LabelSelectorAsSelector(spec.StoreLabelSelector)
			if err != nil {
				return nil, err
			}
			storeLabels, err := t.getStoreLabels()
			if err != nil {
				return nil, err
			}
			pods, err := t.tidbCloudShootKubeClient.ClientSet.CoreV1().Pods(t.tidbCloudShootKubeClient.Namespace).List(context.TODO(), metav1.ListOptions{
				LabelSelector: labels.Set{"app.kubernetes.io/instance": t.tcName(), "app.kubernetes.io/component": "tikv"}.AsSelector().String(),
			})
			if err != nil {
				return nil, err
			}

			for store, labels := range storeLabels {
				if selector.Matches(labels) {
					for _, tikv := range pods.Items {
						pod := tikv
						if pod.Labels["tidb.pingcap.com/store-id"] == store {
							selectors = append(selectors, &PodSelector{&pod})
						}
					}
				}
			}
		case spec.All:
			pods, err := t.tidbCloudShootKubeClient.ClientSet.CoreV1().Pods(t.tidbCloudShootKubeClient.Namespace).List(context.TODO(), metav1.ListOptions{
				LabelSelector: labels.Set{"app.kubernetes.io/instance": t.tcName(), "app.kubernetes.io/component": "tikv"}.AsSelector().String(),
			})
			if err != nil {
				return nil, err
			}
			for _, tikv := range pods.Items {
				pod := tikv
				selectors = append(selectors, &PodSelector{&pod})
			}
		}
	}
	if selector.TiDBClusterTiFlashSelectorSpec != nil {
		spec := selector.TiDBClusterTiFlashSelectorSpec
		switch {
		case spec.Peer != nil:
			pod, err := t.tidbCloudShootKubeClient.ClientSet.CoreV1().Pods(t.tidbCloudShootKubeClient.Namespace).Get(context.Background(), fmt.Sprintf("%s-tiflash-%d", t.tcName(), *spec.Peer), metav1.GetOptions{})
			if err != nil {
				return nil, err
			}
			selectors = append(selectors, &PodSelector{pod})
		case spec.StoreLabelSelector != nil:
			selector, err := metav1.LabelSelectorAsSelector(spec.StoreLabelSelector)
			if err != nil {
				return nil, err
			}
			storeLabels, err := t.getStoreLabels()
			if err != nil {
				return nil, err
			}
			pods, err := t.tidbCloudShootKubeClient.ClientSet.CoreV1().Pods(t.tidbCloudShootKubeClient.Namespace).List(context.TODO(), metav1.ListOptions{
				LabelSelector: labels.Set{"app.kubernetes.io/instance": t.tcName(), "app.kubernetes.io/component": "tiflash"}.AsSelector().String(),
			})
			if err != nil {
				return nil, err
			}
			for store, labels := range storeLabels {
				if selector.Matches(labels) {
					for _, tiflash := range pods.Items {
						pod := tiflash
						if pod.Labels["tidb.pingcap.com/store-id"] == store {
							selectors = append(selectors, &PodSelector{&pod})
						}
					}
				}
			}
		case spec.All:
			pods, err := t.tidbCloudShootKubeClient.ClientSet.CoreV1().Pods(t.tidbCloudShootKubeClient.Namespace).List(context.TODO(), metav1.ListOptions{
				LabelSelector: labels.Set{"app.kubernetes.io/instance": t.tcName(), "app.kubernetes.io/component": "tiflash"}.AsSelector().String(),
			})
			if err != nil {
				return nil, err
			}
			for _, tiflash := range pods.Items {
				pod := tiflash
				selectors = append(selectors, &PodSelector{&pod})
			}
		}
	}
	return selectors, nil
}

func (t *TiDBCloudCluster) getStoreLabels() (map[string]labels.Set, error) {
	dbURL, err := t.ServiceURL(resource.DBAddr)
	if err != nil {
		return nil, err
	}
	db, err := sql.Open("mysql", resource.BuildDSN(dbURL, "INFORMATION_SCHEMA"))
	if err != nil {
		return nil, err
	}
	// different with TiDBCluster.getAddressLabels here
	rows, err := db.Query(`select STORE_ID, LABEL from TIKV_STORE_STATUS`)
	if err != nil {
		return nil, err
	}
	// address to labels
	storeLabels := make(map[string]labels.Set)

	type rowInfo struct {
		Key   string `json:"key"`
		Value string `json:"value"`
	}
	for rows.Next() {
		var store, rawLabel string
		if err := rows.Scan(&store, &rawLabel); err != nil {
			return nil, err
		}
		var r []rowInfo
		if err := json.Unmarshal([]byte(rawLabel), &r); err != nil {
			return nil, err
		}
		if _, ok := storeLabels[store]; !ok {
			storeLabels[store] = map[string]string{}
		}
		for _, item := range r {
			storeLabels[store][item.Key] = item.Value
		}
	}
	return storeLabels, nil
}

func (t *TiDBCloudCluster) updateTrafficFilters(tfs []apiresource.TrafficFilter) error {
	var cidrs []*models.CentralTrafficFilter
	for _, tf := range tfs {
		cidrs = append(cidrs, &models.CentralTrafficFilter{
			Cidr:        pointer.String(tf.CIDR),
			Description: pointer.String(tf.Description),
		})
	}
	_, err := t.tidbCloudClient.ClusterService.UpdateTrafficFilter(&cluster_service.UpdateTrafficFilterParams{
		Body: &models.CentralUpdateTrafficFilterReq{
			ClusterID:      pointer.String(t.clusterID),
			OrgID:          pointer.String(t.orgID),
			ProjectID:      pointer.String(t.projectID),
			TrafficFilters: &models.CentralTrafficFilterList{Cidrs: cidrs},
		},
		ClusterID: t.clusterID,
		OrgID:     t.orgID,
		ProjectID: t.projectID,
		Context:   context.TODO(),
	}, httptransport.BearerToken(t.token))
	if err != nil {
		return err
	}
	return nil
}

func (t *TiDBCloudCluster) Handle() apiresource.ResourceHandle {
	return t.TiDBCloudCluster.ResourceHandle
}

func (t *TiDBCloudCluster) GetDB(dbName string) (*sql.DB, error) {
	endpoint, err := t.getEndpoint()
	if err != nil {
		return nil, err
	}
	db, err := sql.Open("mysql", resource.BuildDSN(endpoint, dbName, "timeout=2m"))
	if err != nil {
		return nil, errors.Trace(err)
	}
	if err := db.Ping(); err != nil {
		return nil, err
	}
	return db, nil
}

func (t *TiDBCloudCluster) ServiceURL(typ resource.TiDBAddrType) (*url.URL, error) {
	switch typ {
	case resource.DBAddr:
		return t.getEndpoint()
	default:
		return nil, errors.New("service is unavailable")
	}
}

func (t *TiDBCloudCluster) pingDB(context context.Context) error {
	endpoint, err := t.getEndpoint()
	if err != nil {
		return err
	}
	db, err := sql.Open("mysql", resource.BuildDSN(endpoint, "", "timeout=2m"))
	if err != nil {
		return err
	}
	return db.PingContext(context)
}

func (t *TiDBCloudCluster) GetSpec() apiresource.TiDBCloudClusterSpec {
	return t.Spec
}

func (t *TiDBCloudCluster) GetStatus() (*apiresource.TiDBCloudClusterStatus, error) {
	if err := t.Sync(); err != nil {
		return nil, err
	}
	return &t.Status, nil
}

func (t *TiDBCloudCluster) PrometheusAPI() (prometheus_api.API, error) {
	client, err := prometheus.NewClient(prometheus.Config{Address: t.prometheusURL})
	if err != nil {
		return nil, err
	}
	return &tidbCloudClusterPromAPI{clusterID: t.clusterID, API: prometheus_api.NewAPI(client)}, nil
}

// Import data to dbaas cluster, only support TiDBCloudImportDataFormatSQL format now.
func (t *TiDBCloudCluster) Import(ctx context.Context, options resource.TiDBCloudImportOptions) error {
	if err := t.Sync(); err != nil {
		return err
	}
	resp, err := t.tidbCloudClient.ImportService.CreateImport(&import_service.CreateImportParams{
		Body: &models.DataflowImportCreateReq{
			AwsRoleArn:     options.AwsRoleArn,
			DataFormat:     pointer.Int64(int64(options.DataFormat)),
			DataSourceType: pointer.Int64(1),
			Filter:         pointer.String(""),
			SourceURL:      pointer.String(options.SourceURL),
			TargetPassword: pointer.String(t.Spec.Password),
			Region:         pointer.String(t.region),
			TargetUsername: pointer.String("root"),
		},
		ClusterID: t.clusterID,
		OrgID:     t.orgID,
		ProjectID: t.projectID,
		Context:   ctx,
	}, httptransport.BearerToken(t.token))
	if err != nil {
		return err
	}
	id := *resp.GetPayload().ID

	err = wait.PollImmediateUntilWithContext(ctx, 30*time.Second, func(ctx context.Context) (done bool, err error) {
		status, err := t.tidbCloudClient.ImportService.GetImport(&import_service.GetImportParams{
			ClusterID: t.clusterID,
			ID:        id,
			OrgID:     t.orgID,
			ProjectID: t.projectID,
			Context:   ctx,
		}, httptransport.BearerToken(t.token))
		if err != nil {
			return false, err
		}
		switch *status.GetPayload().Status {
		case models.DataflowImportGetRespStatusCOMPLETED:
			return true, nil
		case models.DataflowImportGetRespStatusPREPARING, models.DataflowImportGetRespStatusIMPORTING:
			return false, nil
		}
		return false, fmt.Errorf("unexpect import status: %s, got: %s", *status.GetPayload().Status, *status.GetPayload().Message)
	})
	if err != nil {
		return err
	}
	_, err = t.tidbCloudClient.ImportService.ExitImport(&import_service.ExitImportParams{
		ClusterID: t.clusterID,
		ID:        id,
		OrgID:     t.orgID,
		ProjectID: t.projectID,
		Context:   ctx,
	}, httptransport.BearerToken(t.token))
	if err != nil {
		return err
	}
	return nil
}

func (t *TiDBCloudCluster) Backup(ctx context.Context, opts resource.TiDBCloudBackupOptions) (string, error) {
	if err := t.Sync(); err != nil {
		return "", errors.Trace(err)
	}
	backupType := models.DataflowBackupTypeMANUAL
	_, err := t.tidbCloudClient.BackupService.CreateBackupSchedule(&backup_service.CreateBackupScheduleParams{
		Body: &models.DataflowBackupCreateReq{
			BackupType: &backupType,
			Name:       pointer.String(opts.BackupName),
		},
		ClusterID: t.clusterID,
		OrgID:     t.orgID,
		ProjectID: t.projectID,
		Context:   ctx,
	}, httptransport.BearerToken(t.token))
	if err != nil {
		return "", err
	}
	var backupID string
	err = wait.PollImmediateUntilWithContext(ctx, 30*time.Second, func(ctx context.Context) (done bool, err error) {
		status, err := t.tidbCloudClient.BackupService.ListBackups(&backup_service.ListBackupsParams{
			ClusterID: t.clusterID,
			OrgID:     t.orgID,
			Page:      pointer.Int64(1),
			PerPage:   pointer.Int64(100),
			ProjectID: t.projectID,
			Context:   ctx,
		}, httptransport.BearerToken(t.token))
		if err != nil {
			return false, err
		}
		for _, item := range status.GetPayload().Items {
			if *item.Name == opts.BackupName {
				backupID = *item.ID
				switch *item.Status {
				case models.DataflowBackupStatusRUNNING, models.DataflowBackupStatusPENDING:
					return false, nil
				case models.DataflowBackupStatusSUCCESS:
					return true, nil
				}
				return false, fmt.Errorf("unexpect backup status: %s", *item.Status)
			}
		}
		return false, fmt.Errorf("backup %s not found", opts.BackupName)
	})
	if err != nil {
		return "", err
	}
	return backupID, nil
}

// Restore creates a new cluster then restores data into that cluster
// (in contrast, InplaceRestore() will not create a new cluster before restoring data)
func (t *TiDBCloudCluster) Restore(ctx context.Context, opts resource.TiDBCloudRestoreOptions) (resource.TiDBCloudCluster, error) {
	if err := t.Sync(); err != nil {
		return nil, errors.Trace(err)
	}
	dbNodeProfile, err := t.getNodeProfile(models.CentralComponentTypeCOMPONENTTYPETIDB, string(t.Spec.Provider), t.Spec.TiDB.NodeProfileName)
	if err != nil {
		return nil, err
	}
	kvNodeProfile, err := t.getNodeProfile(models.CentralComponentTypeCOMPONENTTYPETIKV, string(t.Spec.Provider), t.Spec.TiDB.NodeProfileName)
	if err != nil {
		return nil, err
	}
	resp, err := t.tidbCloudClient.BackupService.RestoreBackup(&backup_service.RestoreBackupParams{
		Body: &models.DataflowBackupRestoreReq{
			ClusterName: pointer.String(genClusterFullQualifiedName(t.Namespace, opts.ClusterName)),
			Components: []*models.DataflowSimpleClusterComponent{
				{
					ComponentType: models.DataflowComponentTypeCOMPONENTTYPETIDB.Pointer(),
					NodeProfileID: pointer.String(dbNodeProfile.ID),
					Replicas:      pointer.Int32(t.Spec.TiDB.Replicas),
					StorageSizeGi: pointer.Int32(0),
				},
				{
					ComponentType: models.DataflowComponentTypeCOMPONENTTYPETIKV.Pointer(),
					NodeProfileID: pointer.String(kvNodeProfile.ID),
					Replicas:      pointer.Int32(t.Spec.TiKV.Replicas),
					StorageSizeGi: pointer.Int32(t.Spec.TiKV.StorageSizeGi),
				},
			},
			Password: &opts.Password,
			// FIXME: we need support customize port in future,
			Port: pointer.Int32(4000),
		},
		ClusterID: t.clusterID,
		ID:        opts.BackupID,
		OrgID:     t.orgID,
		ProjectID: t.projectID,
		Context:   ctx,
	}, httptransport.BearerToken(t.token))
	if err != nil {
		return nil, err
	}
	clusterID := *resp.GetPayload().ClusterID

	if err = createBookSecret(
		t.ClientSet,
		t.Namespace,
		opts.ClusterName,
		t.shootKubeConfig,
		t.tidbCloudURL,
		t.accountEmail,
		t.orgID,
		t.projectID,
		t.region,
		t.token,
		clusterID,
		opts.Password,
		t.connect,
		t.prometheusURL,
		nil,
		nil,
	); err != nil {
		return nil, err
	}
	cluster := &TiDBCloudCluster{
		KubeClientSet: t.KubeClientSet,
		TiDBCloudCluster: apiresource.TiDBCloudCluster{
			ResourceHandle: apiresource.ResourceHandle{
				Provider: t.TiDBCloudCluster.ResourceHandle.Provider,
				Name:     opts.ClusterName,
				Testbed:  t.TiDBCloudCluster.ResourceHandle.Testbed,
				Type:     t.TiDBCloudCluster.ResourceHandle.Type,
			},
			Spec: apiresource.TiDBCloudClusterSpec{
				Provider: t.TiDBCloudCluster.Spec.Provider,
				Password: opts.Password,
				Connect:  t.TiDBCloudCluster.Spec.Connect,
				TiDB:     t.TiDBCloudCluster.Spec.TiDB,
				TiKV:     t.TiDBCloudCluster.Spec.TiKV,
			},
		},
		tidbCloudClient: t.tidbCloudClient,
	}
	cluster.Provider = t.Provider

	if err := cluster.Sync(); err != nil {
		return nil, err
	}
	return cluster, nil
}

func (t *TiDBCloudCluster) getNodeProfile(componentName models.CentralComponentType, provider, nodeProfileName string) (*models.CentralClusterNodeProfile, error) {
	resp, err := t.tidbCloudClient.ClusterService.CalcNodeProfiles(&cluster_service.CalcNodeProfilesParams{
		Body: &models.CentralCalcNodeProfilesReq{
			IsDevTier: pointer.Bool(false),
			OrgID:     pointer.String(t.orgID),
			Provider:  pointer.String(provider),
			Region:    pointer.String(t.region),
		},
		Context: context.TODO(),
	}, httptransport.BearerToken(t.token))
	if err != nil {
		return nil, err
	}
	if resp.GetPayload().BaseResp.ErrCode != "0" {
		return nil, fmt.Errorf("%s", resp.GetPayload().BaseResp.ErrMsg)
	}
	for _, np := range resp.GetPayload().NodeProfiles {
		if *np.ComponentType != componentName {
			continue
		}
		for _, p := range np.Profiles {
			if p.DisplayName == nodeProfileName {
				return p, nil
			}
		}
		break
	}
	return nil, fmt.Errorf("node profile %s/%s not found", componentName, nodeProfileName)
}

func (t *TiDBCloudCluster) getEndpoint() (*url.URL, error) {
	switch t.Spec.Connect {
	case apiresource.TiDBCloudClusterConnectVPCPeering:
		return &url.URL{
			Scheme: "mysql",
			User:   url.UserPassword("root", t.Spec.Password),
			Host:   fmt.Sprintf("%s:%d", t.Status.PrivateEndpoint.Address, t.Status.PrivateEndpoint.Port),
		}, nil
	case apiresource.TiDBCloudClusterConnectStandard:
		return &url.URL{
			Scheme: "mysql",
			User:   url.UserPassword("root", t.Spec.Password),
			Host:   fmt.Sprintf("%s:%d", t.Status.Endpoint.Address, t.Status.Endpoint.Port),
		}, nil
	}
	return nil, fmt.Errorf("unknown connect config: %s", t.Spec.Connect)
}

// InplaceRestore restores data into current cluster, without creating a new cluster
// (in contrast, Restore() will create a new cluster before restoring data)
func (t *TiDBCloudCluster) InplaceRestore(ctx context.Context, opts resource.TiDBCloudInplaceRestoreOptions) error {
	brRestore := new(unstructured.Unstructured)
	brRestore.Object = runtime.DeepCopyJSON(defaultTidbcloudClusterBRRestoreSpec)

	brRestore.SetNamespace(t.tidbCloudShootKubeClient.Namespace)
	brRestore.SetGenerateName(fmt.Sprintf("%s-restore-", t.Name))
	_ = unstructured.SetNestedField(brRestore.Object, string(opts.BackupType), "spec", "backupType")
	if brImage := getBRRestoreImage(t.tidbCloudClusterConfig); len(brImage) > 0 {
		_ = unstructured.SetNestedField(brRestore.Object, brImage, "spec", "toolImage")
	}
	// set spec.br
	_ = unstructured.SetNestedField(brRestore.Object, t.tidbCloudShootKubeClient.Namespace, "spec", "br", "clusterNamespace")
	_ = unstructured.SetNestedField(brRestore.Object, t.tcName(), "spec", "br", "cluster")
	if opts.DBName != nil {
		_ = unstructured.SetNestedField(brRestore.Object, *opts.DBName, "spec", "br", "db")
	}
	if opts.TableName != nil {
		_ = unstructured.SetNestedField(brRestore.Object, *opts.TableName, "spec", "br", "table")
	}
	// set spec.s3
	_ = unstructured.SetNestedField(brRestore.Object, t.region, "spec", "s3", "region")
	_ = unstructured.SetNestedField(brRestore.Object, opts.BucketName, "spec", "s3", "bucket")
	_ = unstructured.SetNestedField(brRestore.Object, opts.ObjectPrefix, "spec", "s3", "prefix")

	createdBrRestore, err := t.tidbCloudShootKubeClient.DynamicClient.Resource(restoreGVR).
		Namespace(t.tidbCloudShootKubeClient.Namespace).
		Create(ctx, brRestore, metav1.CreateOptions{})
	if err != nil {
		return errors.Annotate(err, "create restore")
	}
	brRestoreName := createdBrRestore.GetName()

	err = wait.PollUntil(30*time.Second, func() (done bool, err error) {
		const (
			// see: https://github.com/pingcap/tidb-operator/blob/c1820e39e50b39212ae7b7a8aa0a15ddf0e721d6/pkg/apis/pingcap/v1alpha1/types.go#L1861
			// RestoreComplete means the Restore has successfully executed and the
			// backup data has been loaded into tidb cluster.
			RestoreComplete = "Complete"
			// RestoreFailed means the Restore has failed.
			RestoreFailed = "Failed"
			// RestoreInvalid means invalid restore CR.
			RestoreInvalid = "Invalid"
		)

		createdBrRestore, err = t.tidbCloudShootKubeClient.DynamicClient.Resource(restoreGVR).
			Namespace(t.tidbCloudShootKubeClient.Namespace).
			Get(ctx, brRestoreName, metav1.GetOptions{})
		if err != nil {
			return false, err
		}

		phase, _, err := unstructured.NestedString(createdBrRestore.Object, "status", "phase")
		if err != nil {
			return false, err
		}

		switch phase {
		case RestoreComplete:
			return true, nil
		case RestoreFailed, RestoreInvalid:
			return false, fmt.Errorf("restore [%s/%s] status [%s]", createdBrRestore.GetNamespace(), createdBrRestore.GetName(), phase)
		default: // running, pending, etc
			return false, nil
		}
	}, ctx.Done())

	// delete Restore whatever restore success or failed;
	// FIXME: this is rather fragile and hard to debug errors, perhaps we should delete ns in controller instead
	_ = t.tidbCloudShootKubeClient.DynamicClient.Resource(restoreGVR).
		Namespace(t.tidbCloudShootKubeClient.Namespace).
		Delete(ctx, brRestoreName, metav1.DeleteOptions{})

	return errors.Trace(err)
}

func getBRRestoreImage(tidbCloudClusterConfig *unstructured.Unstructured) string {
	if tidbCloudClusterConfig == nil {
		return ""
	}

	image, imageFound, imageErr := unstructured.NestedString(tidbCloudClusterConfig.Object, "spec", "restore", "baseImage")
	tag, tagFound, tagErr := unstructured.NestedString(tidbCloudClusterConfig.Object, "spec", "restore", "tag")
	if (len(image) > 0 && imageFound && imageErr == nil) &&
		(len(tag) > 0 && tagFound && tagErr == nil) {
		return fmt.Sprintf("%s:%s", image, tag)
	}

	return ""
}

// ResetCluster deletes all data of components(i.e. rm -rf data of tikv, pd...), then restarts cluster.
// It's to simulate a fresh deployed cluster
func (t *TiDBCloudCluster) ResetCluster() error {
	if err := t.Sync(); err != nil {
		return errors.Trace(err)
	}

	// delete pv, pvc, pods sequentially to clean all data of cluster
	var (
		tcName           = t.tcName()
		pvSelector       = labels.Set{"app.kubernetes.io/namespace": shootNamespace(t.Status.ClusterID), "app.kubernetes.io/instance": tcName}.AsSelector().String()
		podAndPVSelector = labels.Set{"app.kubernetes.io/instance": tcName}.AsSelector().String()
	)
	err := t.tidbCloudShootKubeClient.ClientSet.CoreV1().PersistentVolumes().DeleteCollection(
		context.TODO(), metav1.DeleteOptions{}, metav1.ListOptions{LabelSelector: pvSelector})
	if err != nil {
		return errors.Trace(err)
	}
	err = t.tidbCloudShootKubeClient.ClientSet.CoreV1().PersistentVolumeClaims(t.tidbCloudShootKubeClient.Namespace).DeleteCollection(
		context.TODO(), metav1.DeleteOptions{}, metav1.ListOptions{LabelSelector: podAndPVSelector})
	if err != nil {
		return errors.Trace(err)
	}
	err = t.tidbCloudShootKubeClient.ClientSet.CoreV1().Pods(t.tidbCloudShootKubeClient.Namespace).DeleteCollection(
		context.TODO(), metav1.DeleteOptions{}, metav1.ListOptions{LabelSelector: podAndPVSelector})
	if err != nil {
		return errors.Trace(err)
	}

	// wait until cluster ready
	pollCtx, cancel := context.WithTimeout(context.TODO(), 30*time.Minute)
	defer cancel()
	return wait.PollUntilWithContext(pollCtx, 30*time.Second, func(ctx context.Context) (done bool, err error) {
		// ensure pvc exist, pod can be scheduled eventually
		pods, err := t.tidbCloudShootKubeClient.ClientSet.CoreV1().Pods(t.tidbCloudShootKubeClient.Namespace).List(ctx, metav1.ListOptions{LabelSelector: podAndPVSelector})
		if err != nil {
			return false, err
		}

		for _, pod := range pods.Items {
			// pod not yet deleted, no need to sync early
			if pod.ObjectMeta.DeletionTimestamp != nil {
				return false, nil
			}

			// pod can be blocked in pending, because pingcap/advanced-statefulset doesn't carry this fix: https://github.com/kubernetes/kubernetes/pull/93457
			// so we delete pod here if pvc not exist to trigger pvc creation
			// see: https://github.com/pingcap/advanced-statefulset/blob/master/pkg/controller/statefulset/stateful_pod_control.go#L194-L196
			if pod.Status.Phase != corev1.PodPending {
				continue
			}
			for _, volume := range pod.Spec.Volumes {
				if volume.PersistentVolumeClaim == nil {
					continue
				}

				_, err := t.tidbCloudShootKubeClient.ClientSet.CoreV1().PersistentVolumeClaims(t.tidbCloudShootKubeClient.Namespace).Get(context.Background(), volume.PersistentVolumeClaim.ClaimName, metav1.GetOptions{})
				switch {
				case apierrors.IsNotFound(err):
					err2 := t.tidbCloudShootKubeClient.ClientSet.CoreV1().Pods(t.tidbCloudShootKubeClient.Namespace).Delete(context.Background(), pod.Name, metav1.DeleteOptions{})
					switch {
					case apierrors.IsNotFound(err2):
						sugar.Errorf("delete orphan pod %s: %s", pod.Name, err.Error())
					case err2 != nil:
						return false, err2
					}
				case err != nil:
					return false, err
				}
			}
		}

		err = t.SyncWithoutPing() // pingDB will fail because password reset to empty
		if err != nil {
			return false, err
		}
		if !*t.Ready {
			return false, nil
		}

		// if db looks ready, start trying to reset password to original value
		if err = t.restorePassword(ctx); err != nil {
			return false, err
		}
		return true, nil
	})
}

func (t *TiDBCloudCluster) restorePassword(ctx context.Context) error {
	dbEndpoint, err := t.getEndpoint()
	if err != nil {
		return err
	}

	// pingDB to ensure connectivity, "ready" state reported by tidbcloud can be false positive
	dbEndpoint.User = url.UserPassword("root", "") // ping db with empty password
	err = wait.PollUntilWithContext(ctx, 5*time.Second, func(ctx context.Context) (done bool, innerErr error) {
		db, innerErr := sql.Open("mysql", resource.BuildDSN(dbEndpoint, "", "timeout=2m"))
		if innerErr != nil {
			return false, innerErr
		}

		pingCtx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
		defer cancel()
		innerErr = db.PingContext(pingCtx)
		return innerErr == nil, nil
	})
	if err != nil {
		return err
	}

	select {
	case <-ctx.Done():
		return errors.New("restore password wait timeout")
	case <-time.After(100 * time.Second):
		// even after pingDB succeeded(which means connectivity via ONE of the NLBs is fine),
		// we wait a period because AWS NLB has a health check mechanism(defaults to interval of 30s and threshold of 3).
		// wait extra period to ensure connectivity via ALL of the NLBs are fine.
		// see: https://docs.aws.amazon.com/elasticloadbalancing/latest/network/target-group-health-checks.html
	}

	db, _ := sql.Open("mysql", resource.BuildDSN(dbEndpoint, "", "timeout=2m"))
	_, err = db.Exec(fmt.Sprintf(`set password="%s"`, t.GetSpec().Password))
	return errors.Annotatef(err, "restore password SQL failed")
}

func mergeClusterConfigByComponent(component string, newConfig *apiresource.TiDBCloudConfiguration, clusterConfigOrigin *unstructured.Unstructured) error {
	// field baseImage
	if newConfig.BaseImage != "" {
		if err := unstructured.SetNestedField(clusterConfigOrigin.Object, newConfig.BaseImage, "spec", component, "baseImage"); err != nil {
			return err
		}
	}
	// field tags
	if newConfig.Tag != nil {
		if err := unstructured.SetNestedField(clusterConfigOrigin.Object, *newConfig.Tag, "spec", component, "tag"); err != nil {
			return err
		}
	}
	// field config
	originConfig := apiresource.GenericConfig{MP: make(map[string]interface{})}
	rawOriginConfig, found, err := unstructured.NestedMap(clusterConfigOrigin.Object, "spec", component, "config")
	if err != nil {
		config, _, _ := unstructured.NestedString(clusterConfigOrigin.Object, "spec", component, "config")

		if err := originConfig.UnmarshalTOML([]byte(config)); err != nil {
			return err
		}
	}
	if found {
		originConfig.MP = runtime.DeepCopyJSON(rawOriginConfig)
	}
	mp, _ := mergeMap(originConfig.MP, newConfig.Config.MP)
	config := apiresource.GenericConfig{MP: mp}
	data, err := config.MarshalTOML()
	if err != nil {
		return err
	}
	if err := unstructured.SetNestedField(clusterConfigOrigin.Object, string(data), "spec", component, "config"); err != nil {
		return err
	}

	// field overlays
	rawOriginOverlays, found, err := unstructured.NestedSlice(clusterConfigOrigin.Object, "spec", component, "overlays")
	if err != nil || !found {
		return err
	}
	for _, overlay := range rawOriginOverlays {
		overlayConfig := apiresource.GenericConfig{MP: make(map[string]interface{})}
		rawOverlayConfig, found, err := unstructured.NestedMap(overlay.(map[string]interface{}), "config")
		if err != nil {
			config, _, _ := unstructured.NestedString(overlay.(map[string]interface{}), "config")

			if err := overlayConfig.UnmarshalTOML([]byte(config)); err != nil {
				return err
			}
		}
		if found {
			overlayConfig.MP = runtime.DeepCopyJSON(rawOverlayConfig)
		}
		mp, _ := mergeMap(overlayConfig.MP, newConfig.Config.MP)
		config := apiresource.GenericConfig{MP: mp}
		data, err := config.MarshalTOML()
		if err != nil {
			return err
		}
		if err := unstructured.SetNestedField(overlay.(map[string]interface{}), string(data), "config"); err != nil {
			return err
		}
	}

	if err := unstructured.SetNestedSlice(clusterConfigOrigin.Object, rawOriginOverlays, "spec", component, "overlays"); err != nil {
		return err
	}
	return nil
}

func applyClusterConfig(clusterConfig apiresource.TiDBCloudClusterConfigSpec, clusterConfigOrigin *unstructured.Unstructured) (*unstructured.Unstructured, error) {
	if clusterConfig.PD != nil {
		if err := mergeClusterConfigByComponent("pd", clusterConfig.PD, clusterConfigOrigin); err != nil {
			return clusterConfigOrigin, err
		}
	}
	if clusterConfig.TiDB != nil {
		if err := mergeClusterConfigByComponent("tidb", clusterConfig.TiDB, clusterConfigOrigin); err != nil {
			return clusterConfigOrigin, err
		}
	}
	if clusterConfig.TiKV != nil {
		if err := mergeClusterConfigByComponent("tikv", clusterConfig.TiKV, clusterConfigOrigin); err != nil {
			return clusterConfigOrigin, err
		}
	}
	if clusterConfig.TiFlash != nil {
		if err := mergeClusterConfigByComponent("tiflash", clusterConfig.TiFlash, clusterConfigOrigin); err != nil {
			return clusterConfigOrigin, err
		}
	}
	if clusterConfig.Backup != nil {
		if err := mergeClusterConfigByComponent("backup", clusterConfig.Backup, clusterConfigOrigin); err != nil {
			return clusterConfigOrigin, err
		}
	}
	if clusterConfig.Restore != nil {
		if err := mergeClusterConfigByComponent("restore", clusterConfig.Restore, clusterConfigOrigin); err != nil {
			return clusterConfigOrigin, err
		}
	}

	if util.ValidateVersion(clusterConfig.Version) == nil {
		if err := unstructured.SetNestedField(clusterConfigOrigin.Object, clusterConfig.Version, "spec", "version"); err != nil {
			return nil, err
		}
	}

	return clusterConfigOrigin, nil
}

// tcName returns name of TiDBCluster CR in tidbcloud's kubernetes cluster
func (t *TiDBCloudCluster) tcName() string {
	return "db" // hardcoded name, highly coupled with implementation tidbcloud
}

func (t *TiDBCloudCluster) Sync() error {
	return t.sync(config.TiDBCloudClusterConfig.ReadyCheck.PingDB)
}

func (t *TiDBCloudCluster) SyncWithoutPing() error {
	return t.sync(false)
}

func (t *TiDBCloudCluster) sync(pingDB bool) error {
	bookSecret, err := t.KubeClientSet.ClientSet.CoreV1().Secrets(t.Namespace).Get(context.TODO(), fmt.Sprintf("tidbcloud-%s", t.Name), metav1.GetOptions{})
	if err != nil {
		return errors.Trace(err)
	}

	t.tidbCloudURL = string(bookSecret.Data["tidb_cloud_url"])
	t.shootKubeConfig = bookSecret.Data["shoot_kubeconfig"]
	t.clusterID = string(bookSecret.Data["cluster_id"])
	t.token = string(bookSecret.Data["auth_token"])
	t.accountEmail = string(bookSecret.Data["account_email"])
	t.orgID = string(bookSecret.Data["org_id"])
	t.projectID = string(bookSecret.Data["project_id"])
	t.region = string(bookSecret.Data["region"])
	t.rootPassword = string(bookSecret.Data["cluster_root_password"])
	t.connect = apiresource.TiDBCloudClusterConnect(bookSecret.Data["cluster_connect"])
	t.prometheusURL = string(bookSecret.Data["prometheus_url"])
	t.tidbCloudClusterConfig = new(unstructured.Unstructured)
	if _, ok := bookSecret.Data["cluster_config"]; ok {
		if _, _, err := unstructured.UnstructuredJSONScheme.Decode(bookSecret.Data["cluster_config"], nil, t.tidbCloudClusterConfig); err != nil {
			return err
		}
	}
	if _, ok := bookSecret.Data["cluster_config_spec"]; ok {
		if err = json.Unmarshal(bookSecret.Data["cluster_config_spec"], &t.tidbCloudConfigSpec); err != nil {
			return err
		}
	}

	if t.tidbCloudShootKubeClient == nil {
		shootKubeConfig := bookSecret.Data["shoot_kubeconfig"]
		shootKubeClient, err := NewKubeClientFromKubeConfigData(shootNamespace(t.clusterID), shootKubeConfig)
		if err != nil {
			return errors.Trace(err)
		}
		t.tidbCloudShootKubeClient = shootKubeClient
	}

	if t.tidbCloudClient == nil {
		t.tidbCloudClient = client.NewHTTPClientWithConfig(strfmt.Default, client.DefaultTransportConfig().WithHost(t.tidbCloudURL).WithSchemes([]string{"https"}))
	}

	resp, err := t.tidbCloudClient.ClusterService.GetCluster(&cluster_service.GetClusterParams{
		ID:        t.clusterID,
		OrgID:     t.orgID,
		ProjectID: t.projectID,
		Context:   context.TODO(),
	}, httptransport.BearerToken(t.token))
	if err != nil {
		return errors.Trace(err)
	}
	if resp.GetPayload().BaseResp.ErrCode != "0" {
		return errors.Trace(fmt.Errorf("sync tidbcloud cluster %s error: %+v", t.clusterID, *resp.GetPayload().BaseResp))
	}
	clusterStatus := resp.GetPayload().Cluster

	t.Spec = apiresource.TiDBCloudClusterSpec{
		AccountEmail: t.accountEmail,
		OrgID:        t.orgID,
		ProjectID:    t.projectID,
		Provider:     apiresource.TiDBCloudClusterProvider(*clusterStatus.Provider),
		Password:     t.rootPassword,
		Connect:      t.connect,
		TiDB: apiresource.TiDBCloudClusterTiDBSpec{
			Replicas:        int32(*clusterStatus.ClusterComponents.Tidb.Replicas),
			NodeProfileName: *clusterStatus.ClusterComponents.Tidb.NodeProfile.DisplayName,
		},
		TiKV: apiresource.TiDBCloudClusterTiKVSpec{
			Replicas:        int32(*clusterStatus.ClusterComponents.Tikv.Replicas),
			NodeProfileName: *clusterStatus.ClusterComponents.Tikv.NodeProfile.DisplayName,
			StorageSizeGi:   int32(*clusterStatus.ClusterComponents.Tikv.NodeProfile.StorageGi),
		},
		ClusterConfig: t.tidbCloudConfigSpec,
	}
	t.Status = apiresource.TiDBCloudClusterStatus{
		State:     apiresource.TiDBCloudClusterState(*clusterStatus.Status),
		ClusterID: t.clusterID,
		Version:   *clusterStatus.Version,
		PrivateEndpoint: apiresource.TiDBCloudClusterEndpoint{
			Address: *clusterStatus.PrivateEndpoint.Address,
			Port:    *clusterStatus.PrivateEndpoint.Port,
			Ready:   *clusterStatus.PrivateEndpoint.Ready,
		},
		Endpoint: apiresource.TiDBCloudClusterEndpoint{
			Address: *clusterStatus.Endpoint.Address,
			Port:    *clusterStatus.Endpoint.Port,
			Ready:   *clusterStatus.Endpoint.Ready,
		},
	}

	if t.Status.State == apiresource.TiDBCloudClusterStateNormal && t.connect == apiresource.TiDBCloudClusterConnectStandard {
		if _, ok := bookSecret.Data["connect_cidr"]; !ok {
			if err = t.updateTrafficFilters([]apiresource.TrafficFilter{{
				CIDR:        "0.0.0.0/0",
				Description: "",
			}}); err != nil {
				return err
			}
			err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
				bookSecret.Data["connect_cidr"] = []byte("0.0.0.0/0")
				_, updateErr := t.ClientSet.CoreV1().Secrets(t.Namespace).Update(context.Background(), bookSecret, metav1.UpdateOptions{})
				if updateErr == nil {
					return nil
				}
				if updated, err := t.KubeClientSet.ClientSet.CoreV1().Secrets(t.Namespace).Get(context.TODO(), fmt.Sprintf("tidbcloud-%s", t.Name), metav1.GetOptions{}); err != nil {
					return errors.Annotate(err, "get tidbCloud secret object error")
				} else {
					bookSecret = updated.DeepCopy()
				}
				return updateErr
			})
			if err != nil {
				return err
			}
		}
	}
	if t.Status.State == apiresource.TiDBCloudClusterStateNormal {
		timeout, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		t.Ready = pointer.Bool(!pingDB || t.pingDB(timeout) == nil)
		return nil
	}
	t.Ready = pointer.Bool(false)
	return nil
}

func (t *TiDBCloudCluster) WaitForReady(timeout time.Duration) error {
	interval := 10 * time.Second
	err := wait.PollImmediate(interval, timeout, func() (done bool, err error) {
		if err := t.Sync(); err != nil {
			return false, err
		}
		return t.Ready != nil && *t.Ready, nil
	})
	return errors.Trace(err)
}

func shootNamespace(clusterID string) string {
	return fmt.Sprintf("tidb%s", clusterID)
}

var (
	_ prometheus_api.API = &tidbCloudClusterPromAPI{}
)

type tidbCloudClusterPromAPI struct {
	clusterID string
	prometheus_api.API
}

func (t *tidbCloudClusterPromAPI) Alerts(ctx context.Context) (prometheus_api.AlertsResult, error) {
	panic("unimplemented, please connect to @test-infra for help")
}

func (t *tidbCloudClusterPromAPI) AlertManagers(ctx context.Context) (prometheus_api.AlertManagersResult, error) {
	panic("unimplemented, please connect to @test-infra for help")
}

func (t *tidbCloudClusterPromAPI) CleanTombstones(ctx context.Context) error {
	panic("unimplemented, please connect to @test-infra for help")
}

func (t *tidbCloudClusterPromAPI) Config(ctx context.Context) (prometheus_api.ConfigResult, error) {
	panic("unimplemented, please connect to @test-infra for help")
}

func (t *tidbCloudClusterPromAPI) DeleteSeries(ctx context.Context, matches []string, startTime time.Time, endTime time.Time) error {
	panic("unimplemented, please connect to @test-infra for help")
}

func (t *tidbCloudClusterPromAPI) Flags(ctx context.Context) (prometheus_api.FlagsResult, error) {
	panic("unimplemented, please connect to @test-infra for help")
}

func (t *tidbCloudClusterPromAPI) LabelNames(ctx context.Context, matches []string, startTime time.Time, endTime time.Time) ([]string, prometheus_api.Warnings, error) {
	panic("unimplemented, please connect to @test-infra for help")
}

func (t *tidbCloudClusterPromAPI) LabelValues(ctx context.Context, label string, matches []string, startTime time.Time, endTime time.Time) (model.LabelValues, prometheus_api.Warnings, error) {
	panic("unimplemented, please connect to @test-infra for help")
}

func (t *tidbCloudClusterPromAPI) Query(ctx context.Context, query string, ts time.Time) (model.Value, prometheus_api.Warnings, error) {
	clusteredQuery := prom.InjectLabel(query, "cluster_id", fmt.Sprintf("tidb%s", t.clusterID))
	return t.API.Query(ctx, clusteredQuery, ts)
}

func (t *tidbCloudClusterPromAPI) QueryRange(ctx context.Context, query string, r prometheus_api.Range) (model.Value, prometheus_api.Warnings, error) {
	clusteredQuery := prom.InjectLabel(query, "cluster_id", t.clusterID)
	return t.API.QueryRange(ctx, clusteredQuery, r)
}

func (t *tidbCloudClusterPromAPI) QueryExemplars(ctx context.Context, query string, startTime time.Time, endTime time.Time) ([]prometheus_api.ExemplarQueryResult, error) {
	panic("unimplemented, please connect to @test-infra for help")
}

func (t *tidbCloudClusterPromAPI) Buildinfo(ctx context.Context) (prometheus_api.BuildinfoResult, error) {
	panic("unimplemented, please connect to @test-infra for help")
}

func (t *tidbCloudClusterPromAPI) Runtimeinfo(ctx context.Context) (prometheus_api.RuntimeinfoResult, error) {
	panic("unimplemented, please connect to @test-infra for help")
}

func (t *tidbCloudClusterPromAPI) Series(ctx context.Context, matches []string, startTime time.Time, endTime time.Time) ([]model.LabelSet, prometheus_api.Warnings, error) {
	panic("unimplemented, please connect to @test-infra for help")
}

func (t *tidbCloudClusterPromAPI) Snapshot(ctx context.Context, skipHead bool) (prometheus_api.SnapshotResult, error) {
	panic("unimplemented, please connect to @test-infra for help")
}

func (t *tidbCloudClusterPromAPI) Rules(ctx context.Context) (prometheus_api.RulesResult, error) {
	panic("unimplemented, please connect to @test-infra for help")
}

func (t *tidbCloudClusterPromAPI) Targets(ctx context.Context) (prometheus_api.TargetsResult, error) {
	panic("unimplemented, please connect to @test-infra for help")
}

func (t *tidbCloudClusterPromAPI) TargetsMetadata(ctx context.Context, matchTarget string, metric string, limit string) ([]prometheus_api.MetricMetadata, error) {
	panic("unimplemented, please connect to @test-infra for help")
}

func (t *tidbCloudClusterPromAPI) Metadata(ctx context.Context, metric string, limit string) (map[string][]prometheus_api.Metadata, error) {
	panic("unimplemented, please connect to @test-infra for help")
}

func (t *tidbCloudClusterPromAPI) TSDB(ctx context.Context) (prometheus_api.TSDBResult, error) {
	panic("unimplemented, please connect to @test-infra for help")
}
