package cluster

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"math"
	"net/http"
	"net/url"
	"reflect"
	"slices"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/pingcap/errors"
	"github.com/pingcap/log"
	promcli "github.com/prometheus/client_golang/api/prometheus/v1"
	"github.com/tidbcloud/core-svc/idl/pbgen/proto/core"
	"go.uber.org/zap"
	"gorm.io/gorm"

	"github.com/pingcap-inc/clinic/pkg/activityhub_client"
	"github.com/pingcap-inc/clinic/pkg/api/common"
	error2 "github.com/pingcap-inc/clinic/pkg/api/error"
	"github.com/pingcap-inc/clinic/pkg/auth/session"
	"github.com/pingcap-inc/clinic/pkg/config"
	_const "github.com/pingcap-inc/clinic/pkg/const"
	"github.com/pingcap-inc/clinic/pkg/datasource"
	"github.com/pingcap-inc/clinic/pkg/datasource/clustersearch"
	"github.com/pingcap-inc/clinic/pkg/db"
	model2 "github.com/pingcap-inc/clinic/pkg/db/model"
	"github.com/pingcap-inc/clinic/pkg/es"
	log2 "github.com/pingcap-inc/clinic/pkg/importer/log"
	"github.com/pingcap-inc/clinic/pkg/o11y"
	"github.com/pingcap-inc/clinic/pkg/o11y/urls"
	"github.com/pingcap-inc/clinic/pkg/proxy"
	"github.com/pingcap-inc/clinic/pkg/rbac"
	"github.com/pingcap-inc/clinic/pkg/resource_limit"
	"github.com/pingcap-inc/clinic/pkg/slow_query"
	"github.com/pingcap-inc/clinic/pkg/stream"
	"github.com/pingcap-inc/clinic/pkg/task_manager"
	"github.com/pingcap-inc/clinic/pkg/tsdb"
	"github.com/pingcap-inc/clinic/pkg/upload_file/manager"
)

type ClusterService struct {
	rbac           *rbac.AuthCasbin
	cfg            *config.Config
	esCli          *es.ESClient
	manager        *manager.FileStoreageManager
	taskManagerCli *task_manager.TaskManagerClient
	quotalimiter   resource_limit.IQuotaLimiter
	telemetryCli   *gorm.DB

	handler      *proxy.TiDBClusterMetricHandler
	hubClient    *activityhub_client.ActivityHubClient
	o11yResolver *urls.ClusterURLsResolver

	once sync.Once
}

func NewClusterService(
	cfg *config.Config,
	rbac *rbac.AuthCasbin,
	esCli *es.ESClient,
	manager *manager.FileStoreageManager,
	taskManagerCli *task_manager.TaskManagerClient,
	quotalimiter resource_limit.IQuotaLimiter,
	handler *proxy.TiDBClusterMetricHandler,
) *ClusterService {
	// Initialize o11y resolver if enabled
	var o11yResolver *urls.ClusterURLsResolver
	if cfg.EnableO11yCanary {
		o11ySharedOpts := []o11y.Option{
			o11y.TimeoutOption(40 * time.Second),
			o11y.MaxIdleConnOption(10),
		}
		o11yCli, err := o11y.New(
			cfg.CentralO11yConfig.GSEndpoint,
			cfg.CentralO11yConfig.DSEndpointTPL,
			cfg.CentralO11yConfig.ClientId,
			cfg.CentralO11yConfig.ClientSecret,
			cfg.CentralO11yConfig.Env,
			append(o11ySharedOpts, o11y.ByocDSEndpointOption(cfg.CentralO11yConfig.ByocDSEndpoint))...,
		)
		if err != nil {
			log.Error("failed to create o11y client for ClusterService", zap.Error(err))
		} else {
			o11yResolver = urls.NewClusterURLsResolver(o11yCli)
		}
	}

	return &ClusterService{
		cfg:            cfg,
		rbac:           rbac,
		esCli:          esCli,
		handler:        handler,
		manager:        manager,
		taskManagerCli: taskManagerCli,
		quotalimiter:   quotalimiter,
		o11yResolver:   o11yResolver,
	}
}

func (c *ClusterService) lazyLoad() {
	c.once.Do(func() {
		var (
			telemetryCli *gorm.DB
			err          error
		)

		if c.cfg.ExternalDatasource.TelemetryDB != nil {
			telemetryCli, err = db.Connect(c.cfg.ExternalDatasource.TelemetryDB)
			if err != nil {
				log.Error("init telementry failed", zap.Error(err))
			}
			c.telemetryCli = telemetryCli
		}

		if c.cfg.ActivityManagerConfig.ActivityHubAddr != "" {
			c.hubClient = activityhub_client.NewClient(
				c.cfg.ActivityManagerConfig.ActivityHubAddr,
				c.cfg.ActivityManagerConfig.ActivityHubSource,
				c.cfg.ActivityManagerConfig.ActivityHubKey)
		}
	})
}

// @Summary		List clusters
// @Tags			Cluster
// @Description	list cluster of the specified organization
// @Produce		json
// @Param			x-csrf-token	header		string	true	"get value from login.ValidationResp response"
// @Param			oid				path		string	true	"organization id"
// @Param			limit			query		int		false	"the count per page"
// @Param			query			query		string	false	"the query of cluster"
// @Param			page			query		int		false	"the page index"
// @Param			pid				query		string	false	"project id"
// @Param			show_deleted	query		bool	false	"show deleted cluster"
// @Success		200				{object}	ListClustersResp
// @Router			/orgs/{oid}/clusters [get]
func (s *ClusterService) ListCluster(c *gin.Context) {
	u, org, projectID, ok, err := s.checkListClusterPermission(c)
	if err != nil {
		_ = c.Error(error2.ErrNotFound.New("%s", err.Error()))
		return
	}
	if !ok {
		_ = c.Error(error2.ErrForbidden.New("permission deny"))
		return
	}

	pq := common.ParsePaginationQuery(c.Request.URL.RequestURI())
	clusters, total, err := datasource.ListClusters(u,
		c.Query("query"), org.ID, projectID,
		pq.Page, pq.Limit, !u.Internal, c.Query("show_deleted") != "true")
	if err != nil {
		if err == clustersearch.ErrTextSearchPolicyUnavailable {
			_ = c.Error(error2.ErrServiceUnavailable.New("%s", err.Error()))
			return
		}
		log.Error("list clusters failed", zap.Uint("org", org.ID), zap.Error(err))
		_ = c.Error(error2.ErrInternal.New("list cluster failed"))
		return
	}

	clustersRest := make([]Cluster, 0)
	for _, c := range clusters {
		clustersRest = append(clustersRest, Cluster{
			ID:             c.ClusterID,
			Name:           c.Name,
			Version:        c.Version,
			StartTime:      c.StartTime,
			EndTime:        c.EndTime,
			LastestUpdated: uint64(c.UpdatedAt.Unix()),
			Created:        uint64(c.CreatedAt.Unix()),
			Topology:       c.Topology,
			ClusterType:    c.ClusterType,
			Status:         c.Status,
			DeployType:     c.DeployType,
			DeployTypeV2:   c.DeployTypeV2,
			ParentID:       c.ParentID,
		})
	}

	c.JSON(http.StatusOK, ListClustersResp{
		Total: total,
		Items: clustersRest,
	})
}

func (s *ClusterService) checkListClusterPermission(c *gin.Context) (*session.CtxUser, *db.Organization, uint, bool, error) {
	u, org, err := common.GetOrgFromAPIPath(c)
	if err != nil {
		return nil, nil, 0, false, err
	}
	// project id is required for cloud org
	if org.OrgType == "cloud" && c.Query("pid") == "" {
		return nil, nil, 0, false, error2.ErrBadRequest.New("project id is required")
	}

	projectID := uint(0)
	if c.Query("pid") != "" {
		projectID64, err := strconv.ParseUint(c.Query("pid"), 10, 64)
		if err != nil {
			return nil, nil, 0, false, error2.ErrBadRequest.New("bad parameter: pid, err: %v", err)
		}
		projectID = uint(projectID64)
	}

	// check external user access cloud org
	if !u.Internal && org.OrgType == "cloud" {
		if org.SupportPlan != "Enterprise" && org.SupportPlan != "Premium" {
			openToExternal := s.cfg.ExternalFeatureGates.FeatureEnabled(config.FeatureGateOrgs, uint64(org.DbaasOrgID))
			if !openToExternal {
				return nil, nil, 0, false, fmt.Errorf("cloud org %d not open to external", org.ID)
			}
		}
		ok, err := datasource.HasPermissionForCloudUser(u, org.DbaasOrgID, projectID)
		if err != nil {
			return nil, nil, 0, false, fmt.Errorf("access cluster resource failed: %v, user: %d, orgID: %d", err, u.UserID, org.ID)
		}
		return u, org, projectID, ok, nil
	}

	ok, err := s.rbac.Auth(u.UserID, u.Internal, rbac.Cluster(), rbac.ReadActing(), org.ID)
	if err != nil {
		return nil, nil, 0, false, fmt.Errorf("access cluster resource failed: %v, user: %d, orgID: %d", err, u.UserID, org.ID)
	}
	return u, org, projectID, ok, nil
}

// @Summary		Add cluster
// @Tags			Cluster
// @Description	add a cluster
// @Produce		json
// @Param			x-csrf-token	header		string	true	"get value from login.ValidationResp response"
// @Param			oid				path		string	true	"organization id"
// @Success		200				{object}	common.SuccessResp
// @Router			/orgs/{oid}/clusters [post]
func (s *ClusterService) AddCluster(c *gin.Context) {
	u, org, err := common.GetOrgFromAPIPath(c)
	if err != nil {
		_ = c.Error(err)
		return
	}

	if !u.Internal {
		_ = c.Error(error2.ErrForbidden.New("permission deny"))
		return
	}

	req := AddClusterReq{}
	if err := c.BindJSON(&req); err != nil {
		_ = c.Error(error2.ErrBadRequest.New("bad parameter"))
		return
	}
	if req.DeployType != o11y.ByocType || req.DeployTypeV2 != o11y.DeployTypeV2Byoc {
		_ = c.Error(error2.ErrBadRequest.New("bad parameter"))
		return
	}

	createAt := time.Unix(int64(req.CreatedAt), 0)
	newCluster := &db.Cluster{
		Model: gorm.Model{
			CreatedAt: createAt,
			UpdatedAt: createAt,
		},

		Name:               req.Name,
		OrgID:              org.ID,
		Desc:               req.Description,
		CreationFinishedAt: &createAt,
		ClusterType:        req.ClusterType,
		DeployType:         req.DeployType,
		DeployTypeV2:       req.DeployTypeV2,
		ClusterID:          req.ClusterID,
		ProjectID:          req.ProjectID,
		TenantID:           uint64(org.DbaasOrgID),
		Region:             req.Region,
		Provider:           req.Provider,
	}
	cluster, err := model2.AddCluster(newCluster)
	if err != nil {
		log.Error("create cluster failed", zap.String("cluster", cluster.ClusterID), zap.Error(err))
		_ = c.Error(error2.ErrInternal.New("create cluster failed"))
		return
	}

	c.JSON(http.StatusOK, common.SuccessResponse())
}

// @Summary		List info of the specified cluster
// @Tags			Cluster
// @Description	list packages of the specified cluster
// @Produce		json
// @Param			x-csrf-token	header		string	true	"get value from login.ValidationResp response"
// @Param			oid				path		string	true	"organization id"
// @Param			cid				path		string	true	"cluster id"
// @Success		200				{object}	Cluster
// @Router			/orgs/{oid}/clusters/{cid} [get]
func (s *ClusterService) GetCluster(c *gin.Context) {
	_, org, cluster, ok, err := common.CanReadCluster(c, s.rbac, c.Param("cid"))
	if err != nil {
		_ = c.Error(error2.ErrNotFound.New("%s", err.Error()))
		return
	}
	if !ok {
		_ = c.Error(error2.ErrForbidden.New("access cluster failed"))
		return
	}

	pkgs, _, err := model2.FetchPackageByCusterID(cluster.ClusterID)
	if err != nil {
		log.Error("fetch packages failed", zap.String("clusterID", cluster.ClusterID), zap.Error(err))
		_ = c.Error(error2.ErrInternal.New("fetch oncall count failed"))
		return
	}
	totalOncall, err := countJiraIssue(pkgs)
	if err != nil {
		log.Error("fetch oncall failed", zap.String("clusterID", cluster.ClusterID), zap.Error(err))
	}
	// fetch cluster data size should not fail this request
	dataSize, err := model2.FetchTotalClusterDataSize(cluster.ClusterID)
	if err != nil {
		log.Error("fetch data size failed", zap.String("clusterID", cluster.ClusterID), zap.Error(err))
	}

	dataSizeLimit := uint64(_const.MaxFileSizePreClusterInBytes)
	rl, err := s.quotalimiter.GetResourceLimiter(resource_limit.DataSizeQuota)
	if err != nil {
		log.Error("get resource limiter failed",
			zap.String("clusterID", cluster.ClusterID),
			zap.Stringer("resource", resource_limit.DataSizeQuota), zap.Error(err))
	} else {
		dataSizeLimit = rl.GetQuota(org.ID)
	}
	result := Cluster{
		ID:             cluster.ClusterID,
		Name:           cluster.Name,
		Version:        cluster.Version,
		StartTime:      cluster.StartTime,
		EndTime:        cluster.EndTime,
		LastestUpdated: uint64(cluster.UpdatedAt.Unix()),
		Created:        uint64(cluster.CreatedAt.Unix()),
		Topology:       cluster.Topology,
		TotalOncall:    totalOncall,
		ClusterType:    cluster.ClusterType,
		Note:           cluster.Desc,
		Status:         cluster.Status,
		DataSizeUsed:   dataSize,
		DataSizeLimit:  dataSizeLimit,
		DeployType:     cluster.DeployType,
		DeployTypeV2:   cluster.DeployTypeV2,
		Components:     cluster.ClusterComponent,
		RuVersion:      cluster.RuVersion,
	}

	if org.DbaasOrgID != 0 || cluster.DeployTypeV2 == o11y.DeployTypeV2EssentialV2ResourcePool {
		result.Provider = cluster.Provider
		result.Region = cluster.Region
		if cluster.DeployType == o11y.SharedType || cluster.DeployType == o11y.ServerlessType {
			serverlessK8s, err := realtimeGetServerlessK8s(c, cluster.ClusterID)
			if err != nil {
				log.Error("failed to realtime get serverless k8s", zap.Error(err))
			}
			result.ShootName = serverlessK8s
		} else {
			result.ShootName = cluster.ShootName
		}
		result.DataPlaneName = cluster.DataPlaneName
		result.ControlPlaneArch = cluster.ControlPlaneArch
		result.K8sNamespace = cluster.K8sNamespace
		result.NetworkName = cluster.NetworkName
		result.ProjectID = cluster.ProjectID
		result.Branches = cluster.Branches
		result.ParentID = cluster.ParentID
		result.MinRCU = cluster.MinRCU
		result.MaxRCU = cluster.MaxRCU
		result.KeyspaceID = cluster.KeyspaceID
		result.KeyspaceName = cluster.KeyspaceName
		result.SpendLimit = cluster.SpendLimit
		result.FeatureGates = ClusterFeatureGates{
			LogsEnabled:              s.cfg.ExternalFeatureGates.FeatureEnabled(config.FeatureGateLogs, uint64(org.DbaasOrgID)),
			MaterializedViewEnabled:  s.cfg.ExternalFeatureGates.FeatureEnabled(config.FeatureGateMaterializedView, uint64(org.DbaasOrgID)),
			SlowQueryEnabled:         s.cfg.ExternalFeatureGates.FeatureEnabled(config.FeatureGateSlowQuery, uint64(org.DbaasOrgID)) || IsExecutionPlanRedact(cluster.Version),
			SlowQueryVisualEnabled:   s.cfg.ExternalFeatureGates.FeatureEnabled(config.FeatureGateSlowQueryVisual, uint64(org.DbaasOrgID)),
			TopSQLEnabled:            s.cfg.ExternalFeatureGates.FeatureEnabled(config.FeatureGateTopSQL, uint64(org.DbaasOrgID)) || IsExecutionPlanRedact(cluster.Version),
			ConProfEnabled:           s.cfg.ExternalFeatureGates.FeatureEnabled(config.FeatureGateConProf, uint64(org.DbaasOrgID)),
			BenchmarkReportEnabled:   s.cfg.ExternalFeatureGates.FeatureEnabled(config.FeatureGateBenchmarkReport, uint64(org.DbaasOrgID)),
			ComparisonReportEnabled:  s.cfg.ExternalFeatureGates.FeatureEnabled(config.FeatureGateComparisonReport, uint64(org.DbaasOrgID)),
			SystemCheckReportEnabled: s.cfg.ExternalFeatureGates.FeatureEnabled(config.FeatureGateSystemCheckReport, uint64(org.DbaasOrgID)),
		}

		if cluster.DeployType == "byoc" {
			result.FeatureGates.SlowQueryEnabled = false
			result.FeatureGates.SlowQueryVisualEnabled = false
			result.FeatureGates.TopSQLEnabled = false
			result.FeatureGates.ConProfEnabled = false
			result.FeatureGates.BenchmarkReportEnabled = false
			result.FeatureGates.ComparisonReportEnabled = false
			result.FeatureGates.SystemCheckReportEnabled = false
			result.FeatureGates.MaterializedViewEnabled = false
		}
	}

	c.JSON(http.StatusOK, result)
}

func ClusterBelongToOrg(ctx *session.CtxUser, clusterID string, orgID uint) (*db.Cluster, bool) {
	cluster, err := datasource.GetCluster(ctx, clusterID, orgID)
	if err != nil {
		log.Error("get cluster failed", zap.String("cluster", clusterID), zap.Uint("org", orgID), zap.Error(err))
		return nil, false
	}

	if cluster.OrgID != orgID {
		log.Error(fmt.Sprintf("cluster %s not belong to org %d", clusterID, orgID))
		return nil, false
	}
	return cluster, true
}

// todo@toto redirect to taskmanager

// @Summary		rebuild data for the specified package
// @Tags			Cluster
// @Description	rebuild data for the specified package
// @Acce			json
// @Param			x-csrf-token	header		string			true	"get value from login.ValidationResp response"
// @Param			oid				path		string			true	"organization id"
// @Param			cid				path		string			true	"cluster id"
// @Param			rebuild			body		common.Rebuild	true	"rebuild"
// @Success		200				{object}	common.SuccessResp
// @Router			/orgs/{oid}/clusters/{cid}/rebuild [put]
func (s *ClusterService) Rebuild(c *gin.Context) {
	u, org, ok, err := s.canRebuildCluster(c)
	if err != nil {
		_ = c.Error(error2.ErrNotFound.New("%s", err.Error()))
		return
	}

	if !ok {
		_ = c.Error(error2.ErrForbidden.New("permission deny"))
		return
	}

	byteBody, err := io.ReadAll(c.Request.Body)
	if err != nil {
		_ = c.Error(error2.ErrBadRequest.New("bad parameter"))
		return
	}
	var val common.Rebuild
	err = json.Unmarshal(byteBody, &val)
	if err != nil {
		_ = c.Error(error2.ErrBadRequest.New("bad parameter"))
		return
	}

	if val.ItemID == "" {
		_ = c.Error(error2.ErrForbidden.New("bad parameter"))
		return
	}

	apiType := common.APIDataType(val.DataType)
	dtype := common.ConverToTaskType(apiType)
	if !dtype.Validate() {
		_ = c.Error(error2.ErrBadRequest.New("bad parameter"))
		return
	}

	file, err := model2.QueryUploadFile(val.ItemID)
	if err != nil || file == nil {
		log.Error("query file failed", zap.String("file", val.ItemID), zap.Error(err))
		_ = c.Error(error2.ErrBadRequest.New("item is not existed"))
		return
	}

	if file.ClusterID != c.Param("cid") {
		_ = c.Error(error2.ErrBadRequest.New("invalid cluster"))
		return
	}

	if file.OrgID != org.ID {
		_ = c.Error(error2.ErrBadRequest.New("invalid data"))
		return
	}

	status := common.GetTaskStatusByDataType(dtype, file)
	if status == db.StatusSuccess {
		c.JSON(http.StatusOK, common.SuccessResponse())
		return
	}

	c.Request.Body = io.NopCloser(bytes.NewReader(byteBody))
	if err := common.Proxy(c, fmt.Sprintf("%s:%d", s.cfg.TaskManagerConfig.Host, s.cfg.TaskManagerConfig.Port), &common.ApiUserContext{
		UserID: u.UserID,
		OrgID:  org.ID,
	}, _const.ImporterRebuild); err != nil {
		log.Error("proxy request failed", zap.Uint("user", u.UserID), zap.Uint("org", org.ID), zap.Error(err))
		_ = c.Error(error2.ErrInternal.New("internal error"))
	}
}

// @Summary		delete cluster and data
// @Tags			Cluster
// @Description	delete cluster and data
// @Acce			json
// @Param			x-csrf-token	header		string	true	"get value from login.ValidationResp response"
// @Param			oid				path		string	true	"organization id"
// @Param			cid				path		string	true	"cluster id"
// @Success		200				{string}	success
// @Router			/orgs/{oid}/clusters/{cid} [delete]
func (s *ClusterService) Delete(c *gin.Context) {
	u, org, ok, err := s.canDeleteCluster(c)
	if err != nil {
		_ = c.Error(error2.ErrNotFound.New("%s", err.Error()))
		return
	}
	if !ok {
		_ = c.Error(error2.ErrForbidden.New("permission deny"))
		return
	}
	cluster, ok := ClusterBelongToOrg(u, c.Param("cid"), org.ID)
	if !ok {
		_ = c.Error(error2.ErrBadRequest.New("invalid cluster"))
		return
	}
	go func() {
		files, err := model2.DeleteCluster(cluster.ClusterID)
		if err != nil {
			_ = c.Error(error2.ErrInternal.New("%s", err.Error()))
			return
		}
		logIndex := fmt.Sprintf("org%d_log", cluster.OrgID)
		if err := es.DeleteByCluster(logIndex, cluster.ClusterID, s.cfg.SinkConfig.LogConfig); err != nil {
			_ = c.Error(error2.ErrInternal.New("%s", err.Error()))
			return
		}
		slowlogIndex := fmt.Sprintf("org%d_slowlog", cluster.OrgID)
		if err := es.DeleteByCluster(slowlogIndex, cluster.ClusterID, s.cfg.SinkConfig.LogConfig); err != nil {
			_ = c.Error(error2.ErrInternal.New("%s", err.Error()))
			return
		}
		if err := tsdb.CleanVm(files, cluster.OrgID, s.cfg.SinkConfig.MetricConfig); err != nil {
			_ = c.Error(error2.ErrInternal.New("%s", err.Error()))
			return
		}
	}()
	c.JSON(http.StatusAccepted, common.SuccessResponse())
}

// @Summary		list available dashboard version
// @Tags			Cluster
// @Description	list available dashboard version
// @Produce		json
// @Param			x-csrf-token	header	string	true	"get value from login.ValidationResp response"
// @Param			oid				path	string	true	"organization id"
// @Param			cid				path	string	true	"cluster id"
// @Success		200				{array}	string
// @Router			/orgs/{oid}/clusters/{cid}/dashboard-versions [get]
func (s *ClusterService) ListDashboardVersions(c *gin.Context) {
	u := session.GetUser(c)
	if u == nil {
		_ = c.Error(error2.ErrBadRequest.New("the user is not found"))
		return
	}
	cluster, err := model2.GetAnyCluster(c.Param("cid"))
	if err != nil {
		_ = c.Error(error2.ErrNotFound.New("get cluster failed"))
		return
	}
	if cluster.DeployType == o11y.NgHostType || cluster.DeployType == o11y.PremiumType || cluster.DeployType == o11y.ByocType || cluster.DeployType == o11y.ByocHostType {
		c.JSON(http.StatusOK, s.cfg.SinkConfig.MetricConfig.SortedNextGenDashboardVersion)
		return
	}
	c.JSON(http.StatusOK, s.cfg.SinkConfig.MetricConfig.SortedDashboardVersion)
}

// @Summary		list dashboard user can access
// @Tags			Cluster
// @Description	list dashboard user can access
// @Produce		json
// @Param			x-csrf-token	header	string	true	"get value from login.ValidationResp response"
// @Param			oid				path	string	true	"organization id"
// @Param			cid				path	string	true	"cluster id"
// @Param			startTime		query	int		false	"startTime of dashboard show"
// @Param			endTime			query	int		false	"endTime of dashboard show"
// @Param			version			query	string	false	"tidb version"
// @Success		200				{array}	DataUrl
// @Router			/orgs/{oid}/clusters/{cid}/dashboard [get]
func (s *ClusterService) ListDashboards(c *gin.Context) {
	u, org, cluster, ok, err := common.CanReadCluster(c, s.rbac, c.Param("cid"))
	if err != nil {
		_ = c.Error(error2.ErrNotFound.New("%s", err.Error()))
		return
	}
	if !ok {
		_ = c.Error(error2.ErrForbidden.New("permission deny"))
		return
	}

	startTime, err := strconv.ParseUint(c.Query("startTime"), 10, 64)
	if err != nil {
		_ = c.Error(error2.ErrBadRequest.New("%s", err.Error()))
		return
	}
	endTime, err := strconv.ParseUint(c.Query("endTime"), 10, 64)
	if err != nil {
		_ = c.Error(error2.ErrBadRequest.New("%s", err.Error()))
		return
	}

	version := c.Query("version")
	if version == "" {
		version = cluster.Version
	}
	dashboardCtx := s.resolveDashboardContext(org, cluster, cluster.ClusterID, startTime, endTime)
	matchVersion := ""
	if dashboardCtx.useCloudDashboard && (cluster.DeployType == o11y.NgHostType || cluster.DeployType == o11y.PremiumType || cluster.DeployType == o11y.ByocType || cluster.DeployType == o11y.ByocHostType) {
		matchVersion = common.GetVersion(version, s.cfg.SinkConfig.MetricConfig.SortedNextGenDashboardVersion)
	} else {
		matchVersion = common.GetVersion(version, s.cfg.SinkConfig.MetricConfig.SortedDashboardVersion)
	}

	// suffix in clinic-grafana
	deployTypeSuffix := dashboardCtx.deployTypeSuffix
	if deployTypeSuffix == "cluster" &&
		!s.hasMetricDashboardMetas(deployTypeSuffix, matchVersion) &&
		s.hasMetricDashboardMetas("operator", matchVersion) {
		deployTypeSuffix = "operator"
	}
	var metas []*config.MetricDashboardMeta
	if s.cfg.SinkConfig.MetricConfig.EnableInternalGrafana && u.Internal {
		metas = s.cfg.SinkConfig.MetricConfig.GrafanaDashboardMetas[fmt.Sprintf("clinic-private-%s-%s-internal", deployTypeSuffix, matchVersion)]
		metas = append(metas, s.cfg.SinkConfig.MetricConfig.GrafanaDashboardMetas[fmt.Sprintf("clinic-private-%s-shared-internal", deployTypeSuffix)]...)
	} else {
		metas = s.cfg.SinkConfig.MetricConfig.GrafanaDashboardMetas[fmt.Sprintf("clinic-private-%s-%s-external", deployTypeSuffix, matchVersion)]
		metas = append(metas, s.cfg.SinkConfig.MetricConfig.GrafanaDashboardMetas[fmt.Sprintf("clinic-private-%s-shared-external", deployTypeSuffix)]...)
	}
	metas = applyTemporaryTiCIDashboardScope(metas, cluster, deployTypeSuffix, matchVersion)

	orgID := org.ID
	parentOrgID := uint(0)
	if cluster.ParentID != "" {
		parentCluster, err := model2.GetAnyCluster(cluster.ParentID)
		if err != nil {
			log.Warn("get parent cluster for dashboard org failed",
				zap.String("parentID", cluster.ParentID),
				zap.Error(err))
		} else {
			parentOrgID = parentCluster.OrgID
		}
	}
	results := make([]*DataUrl, 0)
	stream.OfSlice(metas).Map(func(meta *config.MetricDashboardMeta) *stream.MapEntry {
		return &stream.MapEntry{
			Key:   reflect.ValueOf(meta.UID),
			Value: reflect.ValueOf(meta.Name),
		}
	}).Map(func(entry *stream.MapEntry) *DataUrl {
		dashboardId := entry.Key.String()
		dashboardName := entry.Value.String()

		if dashboardName == "" || dashboardId == "" {
			return nil
		}

		orgType := dashboardCtx.orgType
		startTime := dashboardCtx.startTime
		endTime := dashboardCtx.endTime
		k8sCluster := dashboardCtx.k8sCluster
		sharedResourceScope := sharedResourceDashboardScopeVariable(dashboardId, dashboardName)

		// serverless dashboard
		if dashboardCtx.useCloudDashboard && (cluster.DeployType == o11y.SharedType || cluster.DeployType == o11y.ServerlessType) {
			// use tidbcloud dashboard
			if !strings.HasPrefix(dashboardName, "TiDB") &&
				!strings.HasPrefix(dashboardName, "PD") &&
				!strings.HasPrefix(dashboardName, "TiFlash") &&
				!strings.HasPrefix(dashboardName, "TiKV") {
				return nil
			}
		}

		if !u.Internal && dashboardCtx.useCloudDashboard &&
			slices.Contains([]string{o11y.DedicatedType, o11y.ByocType}, cluster.DeployType) {
			if !strings.HasPrefix(dashboardName, "TiDB-Cluster") &&
				!strings.HasPrefix(dashboardName, "TiFlash-Summary") &&
				!strings.HasPrefix(dashboardName, "TiKV-Details") &&
				!strings.HasPrefix(dashboardName, "TiCDC") &&
				!strings.HasPrefix(dashboardName, "Prometheus-TiCDC-New-Arch") &&
				!strings.HasPrefix(dashboardName, "Performance-Overview") &&
				!strings.HasPrefix(dashboardName, "Backup") &&
				!strings.HasPrefix(dashboardName, "Lightning") &&
				!strings.HasPrefix(dashboardName, "TiDB-Resource-Control") &&
				!strings.HasPrefix(dashboardName, "DM-Professional") &&
				!strings.HasPrefix(dashboardName, "DM-Standard") &&
				!strings.HasPrefix(dashboardName, "TiProxy-Summary") &&
				!strings.HasPrefix(dashboardName, "User-Node-Info") {
				return nil
			}
		}

		clusterID := cluster.ClusterID
		if dashboardCtx.useCloudDashboard &&
			(cluster.DeployType == o11y.PremiumType || cluster.DeployType == o11y.ByocType) &&
			!strings.Contains(strings.ToLower(dashboardName), "userscope") &&
			(!strings.Contains(strings.ToLower(dashboardName), "tiproxy") || cluster.DeployTypeV2 == o11y.DeployTypeV2EssentialV2) {
			clusterID = cluster.ParentID
			if parentOrgID > 0 {
				orgID = parentOrgID
			}
		}
		if dashboardCtx.useCloudDashboard && strings.HasPrefix(dashboardName, "Tiflow") {
			return &DataUrl{
				URL:  fmt.Sprintf("/grafana/d/%s/%s?orgId=1&var-tidb_cluster=%s/datamigration&var-store=All&from=%s&to=%s&var-tidb_version=%s&var-org=%d&refresh=15m%s", dashboardId, dashboardName, clusterID, startTime, endTime, matchVersion, orgID, orgType),
				Name: dashboardName,
			}
		}

		if dashboardName == "PD-Cluster-Clinic" || dashboardName == "Overview-Cluster-Clinic" {
			return &DataUrl{
				URL:  fmt.Sprintf("/grafana/d/%s/%s?orgId=1%s&var-tidb_cluster=%s&var-store=All&from=%s&to=%s&var-tidb_version=%s&var-org=%d&refresh=15m%s", dashboardId, dashboardName, k8sCluster, clusterID, startTime, endTime, matchVersion, orgID, orgType),
				Name: dashboardName,
			}
		}

		if dashboardCtx.useCloudDashboard && strings.HasPrefix(dashboardName, "DM") {
			return &DataUrl{
				URL:  fmt.Sprintf("/grafana/d/%s/%s?orgId=1&var-tidb_cluster=%s&var-control_plane_info=seed/%s-%s/seed&var-project_id=%s&from=%s&to=%s&var-tidb_version=%s&var-org=%d&refresh=15m%s", dashboardId, dashboardName, clusterID, cluster.Provider, cluster.Region, cluster.ProjectID, startTime, endTime, matchVersion, orgID, orgType),
				Name: dashboardName,
			}
		}

		if dashboardName == "User-Node-Info" {
			return &DataUrl{
				URL:  fmt.Sprintf("/grafana/d/%s/%s?orgId=1%s&var-tidb_cluster=%s&var-namespace=tidb%s&from=%s&to=%s&var-org=%d&refresh=15m%s", dashboardId, dashboardName, k8sCluster, clusterID, clusterID, startTime, endTime, orgID, orgType),
				Name: dashboardName,
			}
		}

		return &DataUrl{
			URL:  fmt.Sprintf("/grafana/d/%s/%s?orgId=1%s&var-tidb_cluster=%s&var-instance=All&var-store=All&from=%s&to=%s&var-tidb_version=%s&var-org=%d&refresh=15m%s%s", dashboardId, dashboardName, k8sCluster, clusterID, startTime, endTime, matchVersion, orgID, orgType, sharedResourceScope),
			Name: dashboardName,
		}
	}).Filter(func(url *DataUrl) bool {
		if url == nil {
			return false
		}

		// TODO: wait higher grafana version
		if strings.Contains(url.Name, "Screen") {
			return false
		}

		if org.OrgType != "cloud" {
			return true
		}

		if strings.Contains(url.Name, "Node_exporter") || strings.Contains(url.Name, "Kafka-Overview") || strings.Contains(url.Name, "Blackbox") {
			return false
		}
		return true
	}).CollectEmptySlice(&results)

	c.JSON(http.StatusOK, results)
}

func sharedResourceDashboardScopeVariable(dashboardID, dashboardName string) string {
	if dashboardID != proxy.SharedResourceDashboardUID &&
		!strings.HasPrefix(dashboardName, "[SharedScope]Shared Resource One Page") {
		return ""
	}
	return fmt.Sprintf("&%s=%s", proxy.SharedResourceScopeParam, proxy.SharedResourceScopeValue)
}

const temporaryTiCIDashboardClusterID = "7626449809175364811"

func applyTemporaryTiCIDashboardScope(metas []*config.MetricDashboardMeta, cluster *db.Cluster, deployTypeSuffix, matchVersion string) []*config.MetricDashboardMeta {
	metas = filterTiCIDashboards(metas)
	if cluster == nil || cluster.ClusterID != temporaryTiCIDashboardClusterID {
		return metas
	}
	if deployTypeSuffix != "operator" {
		return metas
	}

	versionSuffix := strings.ReplaceAll(matchVersion, ".", "_")
	switch matchVersion {
	case "v8.5.0", "v8.5.x":
	default:
		return metas
	}

	return append(metas,
		&config.MetricDashboardMeta{Name: "TiCI-Meta-Cluster-Clinic", UID: "2eebaf958b2af718operator" + versionSuffix},
		&config.MetricDashboardMeta{Name: "TiCI-Reader-Cluster-Clinic", UID: "5a90bf7441e54762operator" + versionSuffix},
		&config.MetricDashboardMeta{Name: "TiCI-Worker-Cluster-Clinic", UID: "e0213ee534a1b0ddoperator" + versionSuffix},
	)
}

func filterTiCIDashboards(metas []*config.MetricDashboardMeta) []*config.MetricDashboardMeta {
	filtered := make([]*config.MetricDashboardMeta, 0, len(metas))
	for _, meta := range metas {
		if meta == nil || strings.HasPrefix(meta.Name, "TiCI-") {
			continue
		}
		filtered = append(filtered, meta)
	}
	return filtered
}

type dashboardRenderContext struct {
	deployTypeSuffix  string
	useCloudDashboard bool
	orgType           string
	k8sCluster        string
	startTime         string
	endTime           string
}

func (s *ClusterService) resolveDashboardContext(org *db.Organization, cluster *db.Cluster, clusterID string, startTime, endTime uint64) dashboardRenderContext {
	ctx := dashboardRenderContext{
		deployTypeSuffix: "cluster",
		startTime:        fmt.Sprintf("%d", startTime*1000),
		endTime:          fmt.Sprintf("%d", endTime*1000),
	}

	if org.OrgType != "cloud" {
		if cluster.DeployType == "tidb-operator" {
			ctx.deployTypeSuffix = "operator"
			// Only Operator uploads use Kubernetes names; TiUP keeps its Cluster context.
			var file *db.UploadFile
			if startTime > 0 && endTime > 0 {
				file, _ = s.findUploadedMetricPackage(org.ID, clusterID, startTime, endTime)
			}
			ctx.k8sCluster = uploadedMetricK8sClusterVariable(file, cluster)
		}
		return ctx
	}

	if startTime > 0 && endTime > 0 {
		file, ok := s.findUploadedMetricPackage(org.ID, clusterID, startTime, endTime)
		if ok {
			ctx.deployTypeSuffix = "operator"
			ctx.k8sCluster = uploadedMetricK8sClusterVariable(file, cluster)
			return ctx
		}
	}

	ctx.deployTypeSuffix = "tidbcloud"
	if cluster.DeployType == o11y.PremiumType || cluster.DeployType == o11y.NgHostType || cluster.DeployType == o11y.ByocType || cluster.DeployType == o11y.ByocHostType {
		ctx.deployTypeSuffix = "nextgen"
	}
	ctx.useCloudDashboard = true
	ctx.orgType = "&var-org_type=cloud"
	ctx.startTime = "now-2h"
	ctx.endTime = "now"
	return ctx
}

func (s *ClusterService) hasMetricDashboardMetas(deployTypeSuffix, matchVersion string) bool {
	return len(s.cfg.SinkConfig.MetricConfig.GrafanaDashboardMetas[fmt.Sprintf("clinic-private-%s-%s-external", deployTypeSuffix, matchVersion)]) > 0 ||
		len(s.cfg.SinkConfig.MetricConfig.GrafanaDashboardMetas[fmt.Sprintf("clinic-private-%s-%s-internal", deployTypeSuffix, matchVersion)]) > 0 ||
		len(s.cfg.SinkConfig.MetricConfig.GrafanaDashboardMetas[fmt.Sprintf("clinic-private-%s-shared-external", deployTypeSuffix)]) > 0 ||
		len(s.cfg.SinkConfig.MetricConfig.GrafanaDashboardMetas[fmt.Sprintf("clinic-private-%s-shared-internal", deployTypeSuffix)]) > 0
}

func (s *ClusterService) findUploadedMetricPackage(orgID uint, clusterID string, startTime, endTime uint64) (*db.UploadFile, bool) {
	files, _, err := model2.ListUploadFiles(orgID, clusterID, 0, 0, int64(startTime), int64(endTime))
	if err != nil {
		log.Warn("list upload files for dashboard failed", zap.Uint("org", orgID), zap.String("cluster", clusterID), zap.Uint64("startTime", startTime), zap.Uint64("endTime", endTime), zap.Error(err))
		return nil, false
	}
	if len(files) == 0 {
		return nil, false
	}
	return &files[0], true
}

func dashboardK8sClusterVariable(clusterName string) string {
	clusterName = strings.TrimSpace(clusterName)
	if clusterName == "" {
		return ""
	}
	parts := strings.Split(clusterName, "/")
	k8sCluster := strings.TrimSpace(parts[len(parts)-1])
	if k8sCluster == "" {
		return ""
	}
	return fmt.Sprintf("&var-k8s_cluster=%s", url.QueryEscape(k8sCluster))
}

func uploadedMetricK8sClusterVariable(file *db.UploadFile, cluster *db.Cluster) string {
	if file != nil {
		if k8sCluster := dashboardK8sClusterVariable(file.ClusterName); k8sCluster != "" {
			return k8sCluster
		}
	}
	if cluster != nil {
		return dashboardK8sClusterVariable(cluster.Name)
	}
	return ""
}

func (s *ClusterService) canRebuildCluster(c *gin.Context) (*session.CtxUser, *db.Organization, bool, error) {
	u, org, err := common.GetOrgFromAPIPath(c)
	if err != nil {
		return nil, nil, false, err
	}

	ok, err := s.rbac.Auth(u.UserID, u.Internal, rbac.Cluster(), rbac.RebuildActing(), org.ID)
	if err != nil {
		log.Error("access cluster resource failed", zap.Uint("user", u.UserID), zap.Uint("org", org.ID))
		return nil, nil, false, errors.New("permission deny")
	}

	return u, org, ok, nil
}

func (s *ClusterService) canWriteCluster(c *gin.Context) (*session.CtxUser, *db.Organization, bool, error) {
	u, org, err := common.GetOrgFromAPIPath(c)
	if err != nil {
		return nil, nil, false, err
	}

	ok, err := s.rbac.Auth(u.UserID, u.Internal, rbac.Cluster(), rbac.WriteActing(), org.ID)
	if err != nil {
		log.Error("access cluster resource failed", zap.Uint("user", u.UserID), zap.Uint("org", org.ID))
		return nil, nil, false, errors.New("permission deny")
	}

	return u, org, ok, nil
}

func (s *ClusterService) canDeleteCluster(c *gin.Context) (*session.CtxUser, *db.Organization, bool, error) {
	u, org, err := common.GetOrgFromAPIPath(c)
	if err != nil {
		return nil, nil, false, err
	}

	ok, err := s.rbac.Auth(u.UserID, u.Internal, rbac.Cluster(), rbac.DeleteActing(), org.ID)
	if err != nil {
		log.Error("access cluster resource failed", zap.Uint("user", u.UserID), zap.Uint("org", org.ID))
		return nil, nil, false, errors.New("access cluster list failed")
	}

	return u, org, ok, nil
}

func countJiraIssue(files []uint64) (int64, error) {
	num := 0
	for _, fID := range files {
		assocs, err := model2.ListAssociateBySink(db.AssociationEntityClinicFile, uint(fID))
		if err != nil {
			log.Warn("error on list association", zap.Error(err))
			return 0, err
		}
		ids := []uint{}
		for _, assoc := range assocs {
			if assoc.SourceNodeEntity == db.AssociationEntityJiraIssue {
				ids = append(ids, assoc.SourceNodeID)
			}
		}
		issues, err := model2.ListJiraIssueByIds(ids)
		if err != nil {
			if err == gorm.ErrRecordNotFound {
				log.Warn("issue not found", zap.Error(err))
				return 0, err
			}
			return 0, err
		}
		num += len(issues)
	}
	return int64(num), nil
}

// @Summary		get data status of the cluster
// @Tags			Cluster
// @Description	get data status of the specified cluster
// @Produce		json
// @Param			x-csrf-token	header		string	true	"get value from login.ValidationResp response"
// @Param			oid				path		string	true	"organization id"
// @Param			cid				path		string	true	"cluster id"
// @Param			startTime		query		int		true	"startTime"
// @Param			endTime			query		int		true	"endTime"
// @Param			data_type		query		int		true	"int enums"	Enums(1,2,3,4)
// @Success		200				{object}	common.TaskStatusResp
// @Router			/orgs/{oid}/clusters/{cid}/data_status [get]
func (s *ClusterService) GetClusterDataStatus(c *gin.Context) {
	u, org, _, ok, err := common.CanReadCluster(c, s.rbac, c.Param("cid"))
	if err != nil {
		_ = c.Error(error2.ErrNotFound.New("%s", err.Error()))
		return
	}
	if !ok {
		_ = c.Error(error2.ErrForbidden.New("permission deny"))
		return
	}

	pairs := make([]common.Pair, 0)
	pairs = append(pairs, common.Pair{
		Key:   "cid",
		Value: c.Param("cid"),
	})
	pairs = append(pairs, common.Pair{
		Key:   "oid",
		Value: fmt.Sprintf("%d", org.ID),
	})

	err = common.Proxy(c, fmt.Sprintf("%s:%d", s.cfg.TaskManagerConfig.Host, s.cfg.TaskManagerConfig.Port), &common.ApiUserContext{
		UserID: u.UserID,
		OrgID:  org.ID,
	}, _const.ImporterTaskStatus, pairs...)
	if err != nil {
		log.Error("proxy request failed", zap.Uint("user", u.UserID), zap.Uint("org", org.ID), zap.Error(err))
		_ = c.Error(error2.ErrInternal.New("internal error"))
		return
	}
}

// @Summary		update cluster information
// @Tags			Cluster
// @Description	update cluster information
// @Acce			json
// @Param			x-csrf-token	header		string				true	"get value from login.ValidationResp response"
// @Param			oid				path		string				true	"organization id"
// @Param			cid				path		string				true	"cluster id"
// @Param			cluster			body		UpdateClusterReq	true	"cluster information"
// @Success		200				{object}	common.SuccessResp
// @Router			/orgs/{oid}/clusters/{cid} [put]
func (s *ClusterService) UpdateCluster(c *gin.Context) {
	u, org, ok, err := s.canWriteCluster(c)
	if err != nil {
		_ = c.Error(error2.ErrNotFound.New("%s", err.Error()))
		return
	}

	if !ok {
		_ = c.Error(error2.ErrForbidden.New("permission deny"))
		return
	}

	var val UpdateClusterReq
	if err := c.BindJSON(&val); err != nil {
		_ = c.Error(error2.ErrBadRequest.New("bad parameter"))
		return
	}

	cluster, ok := ClusterBelongToOrg(u, c.Param("cid"), org.ID)
	if !ok {
		_ = c.Error(error2.ErrBadRequest.New("invalid cluster"))
		return
	}

	if len(val.Name) > 100 {
		_ = c.Error(error2.ErrBadRequest.New("the name is too long"))
		return
	}
	if len(val.Description) > 100 {
		_ = c.Error(error2.ErrBadRequest.New("the description is too long"))
		return
	}

	if err := model2.UpdateCluster(cluster.ClusterID, getClusterName(cluster, val.Name), getClusterDesc(cluster, val.Description), cluster.Status); err != nil {
		log.Error("update cluster failed", zap.String("cluster", cluster.ClusterID), zap.Error(err))
		_ = c.Error(error2.ErrInternal.New("update cluster failed"))
		return
	}

	c.JSON(http.StatusOK, common.SuccessResponse())
}

// @Summary		update cluster status
// @Tags			Cluster
// @Description	update cluster status
// @Acce			json
// @Param			x-csrf-token	header		string				true	"get value from login.ValidationResp response"
// @Param			oid				path		string				true	"organization id"
// @Param			cid				path		string				true	"cluster id"
// @Param			cluster			body		UpdateClusterReq	true	"cluster information"
// @Success		200				{object}	common.SuccessResp
// @Router			/orgs/{oid}/clusters/{cid}/update_status [post]
func (s *ClusterService) UpdateClusterStatus(c *gin.Context) {
	u, org, err := common.GetOrgFromAPIPath(c)
	if err != nil {
		_ = c.Error(err)
		return
	}

	if !u.Internal {
		_ = c.Error(error2.ErrForbidden.New("permission deny"))
		return
	}

	var val UpdateClusterStatusReq
	if err := c.BindJSON(&val); err != nil {
		_ = c.Error(error2.ErrBadRequest.New("bad parameter"))
		return
	}

	cluster, ok := ClusterBelongToOrg(u, c.Param("cid"), org.ID)
	if !ok {
		_ = c.Error(error2.ErrBadRequest.New("invalid cluster"))
		return
	}

	if err := model2.UpdateCluster(cluster.ClusterID, cluster.Name, cluster.Desc, val.Status); err != nil {
		log.Error("update cluster status failed", zap.String("cluster", cluster.ClusterID), zap.Error(err))
		_ = c.Error(error2.ErrInternal.New("update cluster status failed"))
		return
	}

	c.JSON(http.StatusOK, common.SuccessResponse())
}

func getClusterName(cluster *db.Cluster, clusterName string) string {
	if len(strings.TrimSpace(clusterName)) == 0 {
		return cluster.Name
	}

	return clusterName
}

func getClusterDesc(cluster *db.Cluster, desc string) string {
	if len(strings.TrimSpace(desc)) == 0 {
		return cluster.Desc
	}

	return desc
}

// @Summary		get the detail information of the specified timerange in cluster
// @Tags			Cluster
// @Description	get the detail information of the specified timerange in cluster
// @Produce		json
// @Param			x-csrf-token	header		string	true	"get value from login.ValidationResp response"
// @Param			oid				path		string	true	"organization id"
// @Param			cid				path		string	true	"cluster id"
// @Param			startTime		query		int64	false	"start time"
// @Param			endTime			query		int64	false	"end time"
// @Param			limit			query		int		false	"the count per page"
// @Param			page			query		int		false	"the page index"
// @Success		200				{object}	DataSummaryResponse
// @Router			/orgs/{oid}/clusters/{cid}/data [get]
func (s *ClusterService) GetDataSummary(c *gin.Context) {
	_, org, cluster, ok, err := common.CanReadCluster(c, s.rbac, c.Param("cid"))
	if err != nil {
		_ = c.Error(error2.ErrNotFound.New("%s", err.Error()))
		return
	}
	if !ok {
		_ = c.Error(error2.ErrForbidden.New("access cluster failed"))
		return
	}

	var (
		startTime int64
		endTime   int64
		parseErr  error
	)

	if str := c.Query("startTime"); str != "" {
		startTime, parseErr = strconv.ParseInt(str, 10, 64)
		if parseErr != nil {
			_ = c.Error(error2.ErrBadRequest.New("invalid startTime parameter"))
			return
		}
	}

	if str := c.Query("endTime"); str != "" {
		endTime, parseErr = strconv.ParseInt(str, 10, 64)
		if parseErr != nil {
			_ = c.Error(error2.ErrBadRequest.New("invalid endTime parameter"))
			return
		}
	}

	if startTime > 0 || endTime > 0 {
		if endTime-startTime > s.cfg.GetAllowMaxDataRange() {
			_ = c.Error(error2.ErrBadRequest.New("the timerange is too large"))
			return
		}
	}

	var (
		limit int
		page  int
	)

	pq := common.ParsePaginationQuery(c.Request.URL.RequestURI())
	if pq.RawLimit {
		limit = pq.Limit
		page = pq.Page
	}

	files, total, err := model2.ListUploadFiles(org.ID, cluster.ClusterID, page, limit, startTime, endTime)
	if err != nil {
		_ = c.Error(error2.ErrInternal.New("%s", err.Error()))
		return
	}

	infos := make([]*DataInfo, 0)
	stream.OfSlice(files).Map(func(f db.UploadFile) *DataInfo {
		dataInfo := &DataInfo{
			StartTime:  getTime(f.BeginTime),
			EndTime:    getTime(f.EndTime),
			ItemID:     f.FileUUID,
			FileName:   f.FileOriginalName,
			Collectors: f.Collectors,
		}
		for _, v := range f.Collectors {
			if v == "monitor" || v == "monitor.metric" {
				dataInfo.HaveMetric = true
			}
			if strings.Contains(v, "log") && v != "log.ops" {
				dataInfo.HaveLog = true
			}
			if strings.Contains(v, "config") {
				dataInfo.HaveConfig = true
			}
		}
		return dataInfo
	}).CollectEmptySlice(&infos)

	c.JSON(http.StatusOK, DataSummaryResponse{
		Total:     total,
		DataInfos: infos,
	})
}

func getTime(t *time.Time) int64 {
	if t == nil {
		return 0
	}

	return t.Unix()
}

// @Summary		get slow log list
// @Tags			Cluster
// @Description	get slow log list in cluster
// @Produce		json
// @Param			x-csrf-token	header	string		true	"get value from login.ValidationResp response"
// @Param			oid				path	string		true	"organization id"
// @Param			itemID			query	string		true	"package id"
// @Param			begin_time		query	int			false	"start time"
// @Param			end_time		query	int			false	"end time"
// @Param			db				query	[]string	false	"db list"
// @Param			limit			query	int			false	"limit"
// @Param			text			query	string		false	"text"
// @Param			orderBy			query	string		false	"orderBy"
// @Param			desc			query	bool		false	"desc"
// @Param			plans			query	[]string	false	"plans"
// @Param			digest			query	string		false	"digest"
// @Param			cid				path	string		true	"cluster id"
// @Success		200				{array}	SlowQueryProfile
// @Router			/orgs/{oid}/clusters/{cid}/slowqueries [get]
func (s *ClusterService) ListSlowQueries(c *gin.Context) {
	_, org, cluster, ok, err := common.CanReadCluster(c, s.rbac, c.Param("cid"))
	if err != nil {
		_ = c.Error(error2.ErrNotFound.New("%s", err.Error()))
		return
	}
	if !ok {
		_ = c.Error(error2.ErrForbidden.New("access cluster failed"))
		return
	}

	var req SlowQueryListRequest
	if err := c.BindQuery(&req); err != nil {
		_ = c.Error(error2.ErrBadRequest.New("invalid parameter"))
		return
	}

	file, err := model2.QueryUploadFile(req.ItemID)
	if err != nil {
		_ = c.Error(error2.ErrBadRequest.New("invalid item"))
		return
	}
	if file == nil {
		_ = c.Error(error2.ErrNotFound.New("not found the resource"))
		return
	}
	if file.ClusterID != cluster.ClusterID {
		_ = c.Error(error2.ErrBadRequest.New("invalid cluster id"))
		return
	}
	if file.RebuildLogStatus != string(db.StatusSuccess) {
		_ = c.Error(error2.ErrBadRequest.New("the log is processing"))
		return
	}

	queryBuilder := es.NewBaseQuery()
	//nolint:misspell
	queryBuilder.And(es.NewTermQuery("packageid", file.FileUUID))
	if req.BeginTime != 0 && req.EndTime != 0 {
		queryBuilder.And(es.NewRangeQuery("timestamp", int64(req.BeginTime)*1000, int64(req.EndTime)*1000))
	}

	opt := &es.QueryBuildOption{}

	if req.Limit <= 0 {
		req.Limit = 100
	}

	if req.Limit > 5000 {
		req.Limit = 5000
	}

	opt.Size = req.Limit
	opt.Sort = map[string]es.SortType{
		getSortField(req.OrderBy): func() es.SortType {
			if req.IsDesc {
				return es.Desc
			}

			return es.Asc
		}(),
	}

	if req.Text != "" {
		lowerStr := strings.ToLower(req.Text)
		arr := strings.Fields(lowerStr)

		for _, v := range arr {
			v = fmt.Sprintf("*%s*", v)
			queryBuilder.Should(es.NewWildCardQuery("query", v))
		}
	}
	ctx, cancel := context.WithTimeout(c.Request.Context(), time.Second*30)
	defer cancel()

	resp, queryErr := s.esCli.Do(ctx, es.NewQueryBuilder(es.GetSlowLogIndexName(org.ID), queryBuilder, nil, opt))
	if queryErr != nil {
		log.Error("query es failed", zap.Error(queryErr))
		_ = c.Error(error2.ErrInternal.New("query slowquery failed"))
		return
	}

	results := make([]map[string]any, 0)
	if resp != nil {
		results = resp.Results
	}
	c.JSON(http.StatusOK, results)
}

// @Summary		List metric information of the specified cluster
// @Tags			Cluster
// @Description	list overview metrics of the specified cluster
// @Produce		json
// @Param			x-csrf-token	header		string	true	"get value from login.ValidationResp response"
// @Param			oid				path		string	true	"organization id"
// @Param			cid				path		string	true	"cluster id"
// @Success		200				{object}	ClusterMetrics
// @Router			/orgs/{oid}/clusters/{cid}/metrics [get]
func (s *ClusterService) Metrics(c *gin.Context) {
	u, _, cluster, ok, err := common.CanReadCluster(c, s.rbac, c.Param("cid"))
	if err != nil {
		_ = c.Error(error2.ErrNotFound.New("%s", err.Error()))
		return
	}
	if !ok {
		_ = c.Error(error2.ErrForbidden.New("permission deny"))
		return
	}

	// cluster status
	reports, err := model2.LatestReportCount("health2", cluster.ClusterID, time.Now().Add(-3*24*time.Hour))
	if err != nil {
		log.Error("get reports count failed", zap.String("cluster", cluster.ClusterID), zap.Error(err))
		reports = 0
	}

	s.lazyLoad()

	activitiesCount, err := s.hubClient.GetActivitiesCountByTimeRange(cluster.ClusterID, time.Now().Add(-3*24*time.Hour).Unix(), time.Now().Unix(), 0)
	if err != nil {
		log.Error("get activities failed", zap.String("cluster", cluster.ClusterID), zap.Error(err))
	}

	cli, err := proxy.NewO11PromClient(cluster.OrgID, cluster.ClusterID, u, s.handler)
	if err != nil {
		log.Error("get o11y client failed", zap.String("cluster", cluster.ClusterID), zap.Error(err))
	}
	slaValue := "NaN"
	if u.Internal {
		slaValue = fmt.Sprintf("%f", s.GetSLA(cluster.ClusterID))
	}
	if cli == nil {
		c.JSON(http.StatusOK, ClusterMetrics{
			SLA: slaValue,
			MaxMemory: MetricValue{
				ValueUI: uaDouble(math.NaN()).String(),
			},
			MinUpTime: MetricValue{
				ValueUI: uaDouble(math.NaN()).String(),
			},
			MaxCpu: MetricValue{
				ValueUI: uaDouble(math.NaN()).String(),
			},
			ReportsNumber: reports,
			EventsNumber:  activitiesCount,
		})

		return
	}

	promAPI := promcli.NewAPI(cli)
	// CPU
	cputMetric := getMaxCpuComponent(c.Request.Context(), promAPI, cluster, !u.Internal)

	// memory
	memoryMetric := getMaxMemoryComponent(c.Request.Context(), promAPI, cluster, !u.Internal)
	// uptime
	uptimeMetric := getMinUptimeComponent(c.Request.Context(), promAPI, cluster.ClusterID, !u.Internal)

	// storage
	//nolint:ineffassign, staticcheck
	storageSize := math.NaN()

	if common.CompareVersion(cluster.Version, "5.4.1") > 0 {
		storageSize = getStorageSize(c.Request.Context(), promAPI, NewStorageSizeSQL, cluster.ClusterID)
	} else {
		storageSize = getStorageSize(c.Request.Context(), promAPI, OldStorageSizeSQL, cluster.ClusterID)
	}

	c.JSON(http.StatusOK, ClusterMetrics{
		SLA: slaValue,
		MaxCpu: MetricValue{
			ValueUI:   cputMetric.Value.String(),
			Component: cputMetric.Component,
		},
		MaxMemory: MetricValue{
			Component: memoryMetric.Component,
			ValueUI:   memoryMetric.Value.String(),
		},
		MinUpTime: MetricValue{
			Component: uptimeMetric.Component,
			ValueUI:   uptimeMetric.Value.String(),
		},
		StorageSize:   uaDouble(storageSize).String(),
		ReportsNumber: reports,
		EventsNumber:  activitiesCount,
	})
}

var componentMap = map[string]core.ComponentType{
	"tidb":    core.ComponentType_COMPONENT_TYPE_TIDB,
	"pd":      core.ComponentType_COMPONENT_TYPE_PD,
	"tikv":    core.ComponentType_COMPONENT_TYPE_TIKV,
	"tiflash": core.ComponentType_COMPONENT_TYPE_TIFLASH,
}

func getComponent(comp string) core.ComponentType {
	ct, ok := componentMap[comp]
	if ok {
		return ct
	}

	log.Warn("do not find the component typ", zap.String("component", comp))
	return core.ComponentType_COMPONENT_TYPE_UNSPECIFIED
}

func getMaxCpuComponent(ctx context.Context, promAPI promcli.API, cluster *db.Cluster, excludePD bool) MetricValue {
	now := time.Now()
	query := CPUPromSQLExceptTiflash
	if excludePD {
		query = CPUPromSQLExceptTiflashAndPd
	}
	avgComponentCpu, err := avgTiDBClusterMetricByInstance(ctx, promAPI, fmt.Sprintf(query, cluster.ClusterID), promcli.Range{
		Start: now.Add(-3600 * time.Second),
		End:   now,
		Step:  time.Second * 30,
	})
	if err != nil {
		log.Error("get cpu metric failed", zap.String("metric", fmt.Sprintf(query, cluster.ClusterID)), zap.Error(err))
	}

	avgTiflashCpu, err := avgTiDBClusterMetricByInstance(ctx, promAPI, fmt.Sprintf(TiflashCPUPromSQL, cluster.ClusterID), promcli.Range{
		Start: now.Add(-3600 * time.Second),
		End:   now,
		Step:  time.Second * 30,
	})
	if err != nil {
		log.Error("get tiflash cpu metric failed", zap.String("metric", fmt.Sprintf(TiflashCPUPromSQL, cluster.ClusterID)), zap.Error(err))
	}

	results := make([]MetricValue, 0)
	for _, cpu := range append(avgComponentCpu, avgTiflashCpu...) {
		results = append(results, MetricValue{
			Component: cpu.Component,
			Value:     Division(float64(cpu.Value), int64(cluster.ClusterComponent[getComponent(cpu.Component).String()].CpuNum)),
		})
	}

	return GetMaxValue(results)
}

func getMaxMemoryComponent(ctx context.Context, promAPI promcli.API, cluster *db.Cluster, excludePD bool) MetricValue {
	query := MaxMemoryPromSQLExceptTiflash
	if excludePD {
		query = MaxMemoryPromSQLExceptTiflashAndPd
	}
	componentMem, err := metricGroupByComponent(ctx, promAPI, fmt.Sprintf(query, cluster.ClusterID))
	if err != nil {
		log.Error("get memory metric failed", zap.String("metric", fmt.Sprintf(query, cluster.ClusterID)), zap.Error(err))
	}

	tiflashMem, err := metricGroupByComponent(ctx, promAPI, fmt.Sprintf(MaxTiflashMemoryPromSQL, cluster.ClusterID))
	if err != nil {
		log.Error("get tiflash memory metric failed", zap.String("metric", fmt.Sprintf(MaxTiflashMemoryPromSQL, cluster.ClusterID)), zap.Error(err))
	}

	results := make([]MetricValue, 0)
	for _, mem := range append(componentMem, tiflashMem...) {
		results = append(results, MetricValue{
			Component: mem.Component,
			Value:     Division(float64(mem.Value), cluster.ClusterComponent[getComponent(mem.Component).String()].MemoryGi*1024*1024*1024),
		})
	}

	return GetMaxValue(results)
}

func getMinUptimeComponent(ctx context.Context, promAPI promcli.API, clusterID string, excludePD bool) MetricValue {
	query := MinUpTimeExceptTiflash
	if excludePD {
		query = MinUpTimeExceptTiflashAndPd
	}
	componentUptime, err := metricGroupByComponent(ctx, promAPI, fmt.Sprintf(query, clusterID))
	if err != nil {
		log.Error("get uptime metric failed", zap.String("metric", fmt.Sprintf(query, clusterID)), zap.Error(err))
	}

	tiflashUptime, err := metricGroupByComponent(ctx, promAPI, fmt.Sprintf(MinTiflashUpTime, clusterID))
	if err != nil {
		log.Error("get tiflash uptime metric failed", zap.String("metric", fmt.Sprintf(MinTiflashUpTime, clusterID)), zap.Error(err))
	}

	return GetMinValue(append(componentUptime, tiflashUptime...))
}

func getStorageSize(ctx context.Context, promAPI promcli.API, sql, clusterID string) float64 {
	storage, err := aggMetric(ctx, promAPI, fmt.Sprintf(sql, clusterID, clusterID))
	if err != nil {
		log.Error("get storage metric failed", zap.String("metric", fmt.Sprintf(sql, clusterID, clusterID)), zap.Error(err))
		return math.NaN()
	}

	return storage
}

var (
	CPUPromSQLExceptTiflash      = `irate(process_cpu_seconds_total{cluster_id=~".*%s", component=~"tidb|pd|tikv"}[2m])`
	CPUPromSQLExceptTiflashAndPd = `irate(process_cpu_seconds_total{cluster_id=~".*%s", component=~"tidb|tikv"}[2m])`
	TiflashCPUPromSQL            = `rate(tiflash_proxy_process_cpu_seconds_total{cluster_id=~".*%s", component="tiflash"}[2m])`

	MaxMemoryPromSQLExceptTiflash      = `max(avg_over_time(process_resident_memory_bytes{cluster_id=~".*%s", component=~"tidb|pd|tikv"}[1h])) by (component)`
	MaxMemoryPromSQLExceptTiflashAndPd = `max(avg_over_time(process_resident_memory_bytes{cluster_id=~".*%s", component=~"tidb|tikv"}[1h])) by (component)`
	MaxTiflashMemoryPromSQL            = `max(avg_over_time(tiflash_proxy_process_resident_memory_bytes{cluster_id=~".*%s", component=~"tiflash"}[1h])) by (component)`

	MinUpTimeExceptTiflash      = `min((time() - process_start_time_seconds{cluster_id=~".*%s", component=~"tidb|pd|tikv"})) by (component)`
	MinUpTimeExceptTiflashAndPd = `min((time() - process_start_time_seconds{cluster_id=~".*%s", component=~"tidb|tikv"})) by (component)`
	MinTiflashUpTime            = `min(tiflash_system_asynchronous_metric_Uptime{cluster_id=~".*%s", component=~"tiflash"}) by (component)`

	NewStorageSizeSQL = `(sum(tikv_store_size_bytes{type="used",cluster_id=~".*%s"}) or vector(0))+ (sum(tiflash_system_current_metric_StoreSizeUsed{cluster_id=~".*%s"}) or vector(0))`
	OldStorageSizeSQL = `(sum(tikv_engine_size_bytes{cluster_id=~".*%s"}) or vector(0))+ (sum(tiflash_system_current_metric_StoreSizeUsed{cluster_id=~".*%s"}) or vector(0))`
)

// @Summary		get slow log detail
// @Tags			Cluster
// @Description	get slow log detail in cluster
// @Produce		json
// @Param			x-csrf-token	header		string	true	"get value from login.ValidationResp response"
// @Param			oid				path		string	true	"organization id"
// @Param			itemID			query		string	true	"package id"
// @Param			cid				path		string	true	"cluster id"
// @Param			queryid			path		string	true	"log id"
// @Success		200				{object}	SlowQueryProfile
// @Router			/orgs/{oid}/clusters/{cid}/slowqueries/{queryid} [get]
func (s *ClusterService) SlowQueryDetail(c *gin.Context) {
	id := c.Param("queryid")
	if id == "" {
		log.Warn("empty query id parameter")
		_ = c.Error(error2.ErrNotFound.New("bad parameter"))
		return
	}

	_, org, cluster, ok, err := common.CanReadCluster(c, s.rbac, c.Param("cid"))
	if err != nil {
		_ = c.Error(error2.ErrNotFound.New("%s", err.Error()))
		return
	}
	if !ok {
		_ = c.Error(error2.ErrForbidden.New("permission deny"))
		return
	}

	file, err := model2.QueryUploadFile(c.Query("itemID"))
	if err != nil {
		_ = c.Error(error2.ErrBadRequest.New("invalid item"))
		return
	}

	if file == nil {
		_ = c.Error(error2.ErrNotFound.New("not found the resource"))
		return
	}

	if file.ClusterID != cluster.ClusterID {
		_ = c.Error(error2.ErrBadRequest.New("invalid cluster id"))
		return
	}
	//
	if file.RebuildLogStatus != string(db.StatusSuccess) {
		_ = c.Error(error2.ErrBadRequest.New("the log is processing"))
		return
	}

	builder := es.NewQueryBuilder(es.GetSlowLogIndexName(org.ID),
		es.NewBaseQuery().And(es.NewTermQuery("_id", id)), nil, &es.QueryBuildOption{Size: 1})

	ctx, cancel := context.WithTimeout(c.Request.Context(), time.Second*30)
	defer cancel()

	resp, esErr := s.esCli.Do(ctx, builder)
	if esErr != nil {
		log.Error("query es failed", zap.String("id", id), zap.Error(esErr))
		_ = c.Error(error2.ErrInternal.New("query slow log failed"))
		return
	}

	if resp == nil || len(resp.Results) == 0 {
		_ = c.Error(error2.ErrNotFound.New("not found the slow log"))
		return
	}

	r := resp.Results[0]
	decodePlan(id, r)

	c.JSON(http.StatusOK, r)
}

func decodePlan(id string, r map[string]any) {
	binaryPlan, ok := r["binary_plan"]
	if ok {
		binaryPlanEncoded, ok := binaryPlan.(string)
		if !ok {
			log.Warn("binary plan is not string type", zap.String("id", id), zap.Any("binary_plan", binaryPlan))
			r["binary_plan"] = ""
			return
		}
		binaryPlanEncoded = strings.TrimSuffix(strings.TrimPrefix(binaryPlanEncoded, "tidb_decode_binary_plan('"), "')")
		binaryPlanText, decodeErr := slow_query.DecodeBinaryPlan(binaryPlanEncoded)
		if decodeErr != nil {
			log.Error("decode plan failed", zap.String("id", id), zap.Error(decodeErr))
			binaryPlanText = fmt.Sprintf("Raw binary plan is: %s. Decode binary plan failed: %v", binaryPlanEncoded, decodeErr.Error())
			// UI will parse the json plan
			// if decode to text failed, set binary plan to empty string
			r["binary_plan"] = ""
		} else {
			planJosn, decodeErr := slow_query.DecodeBinaryPlanToJson(binaryPlanEncoded)
			if decodeErr != nil {
				// UI will parse the json plan
				// if decode to text failed, set binary plan to empty string
				r["binary_plan"] = ""
				log.Error("decode binary plan to json failed", zap.String("id", id), zap.Error(decodeErr))
			} else {
				r["binary_plan"] = planJosn
			}
		}
		r["plan"] = binaryPlanText
		return
	}

	planEncoded, ok := r["plan"].(string)
	if !ok {
		log.Warn("plan is not string type", zap.String("id", id), zap.Any("plan", r["plan"]))
		return
	}

	planEncoded = strings.TrimSuffix(strings.TrimPrefix(planEncoded, "tidb_decode_plan('"), "')")
	planText, decodeErr := slow_query.DecodePlan(planEncoded)
	if decodeErr != nil {
		log.Error("decode plan failed", zap.String("id", id), zap.Error(decodeErr))
		planText = fmt.Sprintf("Raw plan is: %s. Decode plan failed: %v", planEncoded, decodeErr.Error())
	}
	r["plan"] = planText
}

func getSortField(orderBy string) string {
	if orderBy == "" || orderBy == "timestamp" {
		return "timestamp"
	}

	var columns map[string]any
	err := json.NewDecoder(bytes.NewReader([]byte(log2.SlowQueryLogMapping))).Decode(&columns)
	if err != nil {
		log.Error("get columns failed", zap.Error(err))

		return "timestamp"
	}

	fileds := columns["mappings"].(map[string]any)["properties"].(map[string]any)
	fieldType, ok := fileds[strings.ToLower(orderBy)].(map[string]any)
	if !ok {
		return "timestamp"
	}

	t := fieldType["type"].(string)
	if t != "text" {
		return strings.ToLower(orderBy)
	}

	keyword, ok := fieldType["fields"].(map[string]any)
	if !ok {
		return "timestamp"
	}

	if _, ok := keyword["keyword"]; ok {
		return fmt.Sprintf("%s.keyword", strings.ToLower(orderBy))
	}

	return "timestamp"
}

// @Summary		List default value of current version
// @Tags			Cluster
// @Description	list default values of the specified cluster version
// @Produce		json
// @Param			x-csrf-token	header		string	true	"get value from login.ValidationResp response"
// @Param			oid				path		string	true	"organization id"
// @Param			cid				path		string	true	"cluster id"
// @Success		200				{object}	DefaultConfiguration
// @Router			/orgs/{oid}/clusters/{cid}/defaultconf [get]
func (s *ClusterService) DefaultConfValue(c *gin.Context) {
	_, _, cluster, ok, err := common.CanReadCluster(c, s.rbac, c.Param("cid"))
	if err != nil {
		_ = c.Error(error2.ErrNotFound.New("%s", err.Error()))
		return
	}
	if !ok {
		_ = c.Error(error2.ErrForbidden.New("access cluster failed"))
		return
	}

	items, err := model2.GetDefaultConfigurations(strings.TrimPrefix(cluster.Version, "v"))
	if err != nil {
		log.Error("get configuration failed", zap.String("cluster", cluster.ClusterID), zap.String("version", cluster.Version), zap.Error(err))
		_ = c.Error(error2.ErrBadRequest.New("get configuration failed"))
		return
	}

	c.JSON(http.StatusOK, DefaultConfiguration{
		Items: items,
	})
}

func realtimeGetServerlessK8s(c *gin.Context, clusterIDStr string) (string, error) {
	user := session.GetUser(c)
	if user == nil {
		return "", errors.New("the user is not found")
	}
	clusterID, err := strconv.ParseUint(clusterIDStr, 10, 64)
	if err != nil {
		return "", errors.Wrap(err, "invalid cluster id")
	}
	mgmtCluster, err := datasource.RealtimeGetMgmtCluster(user, clusterID)
	if err != nil {
		return "", errors.Wrap(err, "failed to get mgmt cluster")
	}
	serverlessCluster := mgmtCluster.GetServerlessCluster()
	if serverlessCluster == nil || serverlessCluster.Cluster == nil {
		return "", nil
	}
	serverlessK8s := serverlessCluster.Cluster.Kubernetes
	if strings.HasPrefix(serverlessK8s, "prod-") {
		serverlessK8s = strings.TrimPrefix(serverlessK8s, "prod-")
	} else if strings.HasPrefix(serverlessK8s, "staging-") {
		serverlessK8s = strings.TrimPrefix(serverlessK8s, "staging-")
	} else if strings.HasPrefix(serverlessK8s, "dev-") {
		serverlessK8s = strings.TrimPrefix(serverlessK8s, "dev-")
	}
	return serverlessK8s, nil
}

// GetClusterByID returns cluster basic info by cluster id
// This is a lightweight API that directly queries DB and returns basic cluster info
//
//	@Summary		Get cluster basic info by ID
//	@Tags			Cluster
//	@Description	Get cluster basic information by cluster id (lightweight direct DB lookup)
//	@Produce		json
//	@Param			x-csrf-token	header		string	true	"get value from login.ValidationResp response"
//	@Param			oid				path		string	true	"organization id"
//	@Param			cid				path		string	true	"cluster id"
//	@Success		200				{object}	SimpleClusterResponse
//	@Router			/clusters/{cid} [get]
func (s *ClusterService) GetClusterByID(c *gin.Context) {
	clusterID := c.Param("cid")
	if clusterID == "" {
		_ = c.Error(error2.ErrBadRequest.New("cluster_id is required"))
		return
	}

	// Direct DB lookup by cluster id
	cluster, err := model2.GetAnyCluster(clusterID)
	if err != nil {
		log.Error("failed to get cluster from db", zap.String("cluster_id", clusterID), zap.Error(err))
		_ = c.Error(error2.ErrNotFound.New("cluster not found"))
		return
	}

	// Build simple response
	resp := SimpleClusterResponse{
		ClusterID:    cluster.ClusterID,
		Name:         cluster.Name,
		OrgID:        cluster.OrgID,
		TenantID:     cluster.TenantID,
		ProjectID:    cluster.ProjectID,
		Region:       cluster.Region,
		Provider:     cluster.Provider,
		DeployType:   cluster.DeployType,
		DeployTypeV2: cluster.DeployTypeV2,
		Status:       cluster.Status,
		Version:      cluster.Version,
		CreatedAt:    cluster.CreatedAt.Unix(),
		UpdatedAt:    cluster.UpdatedAt.Unix(),
	}

	c.JSON(http.StatusOK, resp)
}
