package api

import (
	"bytes"
	"context"
	"crypto/sha256"
	"crypto/tls"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/gtank/cryptopasta"
	"github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/account/idl/pbgen/proto/acl"
	"github.com/tidbcloud/core-svc/idl/pbgen/proto/core"
	centralentity "github.com/tidbcloud/core-svc/model"
	"github.com/tidbcloud/internal-portal/api/types"
	"github.com/tidbcloud/internal-portal/apiserver"
	"github.com/tidbcloud/internal-portal/core-portal/central/db"
	"github.com/tidbcloud/internal-portal/core-portal/central/model/cluster"
	"github.com/tidbcloud/internal-portal/db/entity"
	"github.com/tidbcloud/internal-portal/temporary/grpcli"
	"github.com/tidbcloud/pkgv2/log"
	"github.com/vmihailenco/msgpack"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

const (
	dashboardAuthCodeURI = "/api/user/share/code"

	authCodeStatusNormal    = "normal"
	authCodeStatusRevoke    = "revoked"
	authcodeStatusExpired   = "expired"
	authCodeRecordOpsAuth   = "authorize"
	authCodeRecordOpsQuery  = "query"
	authCodeRecordOpsRevoke = "revoke"

	userDebugItemsPerPageDefault = 10

	featureKeyUserdebugAuth = "userdebug_authorization"
)

// pre-defined variables
var (
	ErrAlreadyAuthorized = fmt.Errorf("cluster is already authorized")
)

func getUserDebugFeatures(c *gin.Context) {
	organizationID := c.Param("organization")
	if organizationID == "" {
		Fail(c, "", fmt.Errorf("organization must be specified"), http.StatusBadRequest)
		return
	}

	orgID, err := strconv.ParseUint(organizationID, 10, 64)
	if err != nil {
		Fail(c, "", err, http.StatusBadRequest)
		return
	}

	// query db to get auth code
	var featureList []entity.UserdebugFeatureConfig
	err = db.Transaction(func(tx *gorm.DB) error {
		return entity.NewUserdebugFeatureConfigQuerySet(tx).
			OrganizationIDEq(entity.ID(orgID)).
			All(&featureList)
	})
	if err != nil && err != gorm.ErrRecordNotFound {
		log.Info(c, "query for feature list failed", zap.String("organization", organizationID), zap.Error(err))
		Fail(c, "failed querying feature list", err, http.StatusInternalServerError)
		return
	}

	// build result
	result := types.UserDebugFeatureList{
		OrgID:    organizationID,
		Features: make([]*types.UserDebugFeatureListFeaturesItems0, 0),
	}
	for _, f := range featureList {
		result.Features = append(result.Features, &types.UserDebugFeatureListFeaturesItems0{
			Key:    f.FeatureKey,
			Status: f.Status,
		})
	}

	c.JSON(http.StatusOK, result)
}

func getUserDebugAuthList(c *gin.Context) {
	organizationID := c.Param("organization")
	if organizationID == "" {
		Fail(c, "", fmt.Errorf("organization must be specified"), http.StatusBadRequest)
		return
	}
	orgID, err := strconv.ParseUint(organizationID, 10, 64)
	if err != nil {
		Fail(c, "", err, http.StatusBadRequest)
		return
	}
	var pageNum int
	pn := c.Query("page")
	if pn != "" {
		pageNum, err = strconv.Atoi(pn)
		if err != nil {
			Fail(c, "", err, http.StatusBadRequest)
			return
		}
	} else {
		pageNum = 1
	}
	var perPage int
	pp := c.Query("per_page")
	if pp != "" {
		perPage, err = strconv.Atoi(pp)
		if err != nil {
			Fail(c, "", err, http.StatusBadRequest)
			return
		}
	} else {
		perPage = userDebugItemsPerPageDefault
	}

	claim, err := apiserver.GetUserFromContext(c)
	if err != nil {
		Fail(c, "", err, http.StatusUnauthorized)
		return
	}

	if claim.Tenant.ID.Uint64() != uint64(orgID) {
		Fail(c, "", fmt.Errorf("tenant id mismatch"), http.StatusBadRequest)
		return
	}

	type clsInfo struct {
		projID   int64
		projName string
		clsName  string
	}

	// list all clusters of the organization
	clusterList := make([]*core.Cluster, 0)
	clusterIDs := make([]entity.ID, 0)
	clusterInfo := make(map[entity.ID]*clsInfo)

	ctx, f := context.WithTimeout(context.Background(), 15*time.Second)
	defer f()
	projects, err := grpcli.ListProjectsByOrgsInUser(ctx, &account.ListProjectsByOrgsInUserRequest{
		UserId: uint64(claim.ID),
		OrgIds: []uint64{uint64(orgID)},
	})
	if err != nil {
		Fail(c, "failed to get projects info", err, http.StatusInternalServerError)
	}

	for _, orgProj := range projects.Projects {
		for _, proj := range orgProj.Projects {
			var clusterResp *core.ListClustersResp
			filter := &core.ClusterFilter{
				TenantIds:  []uint64{uint64(orgID)},
				ProjectIds: []uint64{uint64(proj.Id)},
			}
			ctx, f := context.WithTimeout(context.Background(), 15*time.Second)
			defer f()
			clusterResp, err = grpcli.ListClusters(ctx, &core.ListClustersReq{Filter: filter})
			if err != nil {
				Fail(c, "failed to get cluster info", err, http.StatusInternalServerError)
				return
			}

			for _, cls := range clusterResp.Clusters {
				clusterIDs = append(clusterIDs, entity.ID(cls.Id))
				clusterList = append(clusterList, cls)
				clusterInfo[entity.ID(cls.GetId())] = &clsInfo{
					projID:   int64(proj.Id),
					projName: proj.Name,
					clsName:  cls.GetName(),
				}
			}
		}
	}

	// no cluster found
	result := &types.UserDebugAuthorizationList{
		Items: make([]*types.UserDebugAuthorizationDetail, 0),
	}
	if len(clusterIDs) < 1 {
		c.JSON(http.StatusOK, result)
		return
	}

	// query db to get auth code list
	var authList []entity.UserdebugAuth
	err = db.Transaction(func(tx *gorm.DB) error {
		// query for total number first
		if result.Total, err = entity.
			NewUserdebugAuthQuerySet(tx).
			ClusterIDIn(clusterIDs...).
			Count(); err != nil {
			return err
		}

		// query for needed records
		return entity.NewUserdebugAuthQuerySet(tx).
			ClusterIDIn(clusterIDs...).
			OrderDescByCreatedAt().
			Offset(perPage * (pageNum - 1)).
			Limit(perPage).
			All(&authList)
	})
	if err != nil || authList == nil {
		if err == gorm.ErrRecordNotFound {
			err = fmt.Errorf(
				"no userdebug auth code found for cluster of organization %s",
				organizationID,
			)
		}
		log.Info(c, "query for userdebug auth failed", zap.String("org", organizationID), zap.Error(err))
		Fail(c, "", err, http.StatusNotFound)
		return
	}

	// no need to write auth record as the code itself is not returned

	// build result
	for _, authItem := range authList {
		item := &types.UserDebugAuthorizationDetail{
			ID:          fmt.Sprintf("%d", authItem.GetID()),
			ClusterID:   fmt.Sprintf("%d", authItem.ClusterID),
			ClusterName: clusterInfo[authItem.ClusterID].clsName,
			ProjectID:   fmt.Sprintf("%d", clusterInfo[authItem.ClusterID].projID),
			ProjectName: clusterInfo[authItem.ClusterID].projName,
			Status:      authItem.Status,
			ExpiresAt:   authItem.ExpiresAt.Unix(),
			CreatedAt:   authItem.CreatedAt.Unix(),
			UpdatedAt:   authItem.UpdatedAt.Unix(),
		}
		if authItem.Status == authCodeStatusNormal &&
			authItem.ExpiresAt.Before(time.Now()) {
			item.Status = authcodeStatusExpired
		}
		result.Items = append(result.Items, item)
	}
	c.JSON(http.StatusOK, result)
}

func getUserDebugAuthDetail(c *gin.Context) {
	clusterID := c.Param("cluster")
	organizationID := c.Param("organization")
	authID := c.Param("auth_id")
	if clusterID == "" || organizationID == "" || authID == "" {
		Fail(c, "", fmt.Errorf("organization, cluster and auth_id must be specified"), http.StatusBadRequest)
		return
	}

	clusterInfo, err, code := getClusterByID(c, organizationID, clusterID)
	if err != nil {
		Fail(c, "", err, code)
		return
	}

	authCodeID, err := strconv.ParseUint(authID, 10, 64)
	if err != nil {
		Fail(c, "", err, http.StatusBadRequest)
		return
	}

	// query db to get auth code
	authLine := &entity.UserdebugAuth{}
	err = db.Transaction(func(tx *gorm.DB) error {
		return entity.NewUserdebugAuthQuerySet(tx).
			ClusterIDEq(entity.ID(clusterInfo.cluster.Id)).
			IDEq(entity.ID(authCodeID)).
			One(authLine)
	})
	if err != nil || authLine == nil {
		if err == gorm.ErrRecordNotFound {
			err = fmt.Errorf("userdebug auth code not found with cluster %s", clusterInfo.cluster.Name)
		}
		log.Info(c, "query for userdebug auth failed", zap.String("cluster", clusterID), zap.Error(err))
		Fail(c, "", err, http.StatusNotFound)
		return
	}

	ctx, f := context.WithTimeout(context.Background(), 15*time.Second)
	defer f()
	user, err := grpcli.GetUserByID(ctx, &account.GetUserByIDRequest{UserId: authLine.CreatedBy.Uint64()})

	if err != nil || user.User == nil {
		log.Info(c, "query for user info failed", zap.Uint64("user", uint64(authLine.CreatedBy)), zap.Error(err))
		// continue
	}

	// no need to write auth record as the code itself is not returned

	result := &types.UserDebugAuthorizationDetail{
		ID:          fmt.Sprintf("%d", authLine.GetID()),
		Status:      authLine.Status,
		ClusterID:   fmt.Sprintf("%d", clusterInfo.cluster.GetId()),
		ClusterName: clusterInfo.cluster.Name,
		ProjectID:   fmt.Sprintf("%d", clusterInfo.project.Id),
		ProjectName: clusterInfo.project.Name,
		CreatedAt:   authLine.CreatedAt.Unix(),
		ExpiresAt:   authLine.ExpiresAt.Unix(),
		UpdatedAt:   authLine.UpdatedAt.Unix(),
		AuthorizedBy: &types.UserDebugAuthorizedBy{
			ID: fmt.Sprintf("%d", authLine.CreatedBy),
		},
	}
	if authLine.Status == authCodeStatusNormal &&
		authLine.ExpiresAt.Before(time.Now()) {
		result.Status = authcodeStatusExpired
	}

	result.AuthorizedBy.FirstName = user.User.FirstName
	result.AuthorizedBy.LastName = user.User.LastName
	result.AuthorizedBy.Email = user.User.Email

	c.JSON(http.StatusOK, result)
}

func getUserDebugAuthCode(c *gin.Context) {
	clusterID := c.Param("cluster")
	organizationID := c.Param("organization")
	if clusterID == "" || organizationID == "" {
		Fail(c, "", fmt.Errorf("organization and cluster must be specified"), http.StatusBadRequest)
		return
	}
	clsID, err := strconv.ParseUint(clusterID, 10, 64)
	if err != nil {
		Fail(c, "", err, http.StatusBadRequest)
		return
	}
	orgID, err := strconv.ParseUint(organizationID, 10, 64)
	if err != nil {
		Fail(c, "", err, http.StatusBadRequest)
		return
	}

	userName := c.Query("user")
	if userName == "" {
		Fail(c, "", fmt.Errorf("user must be specified"), http.StatusBadRequest)
		return
	}
	// decline too long usernames
	if len(userName) > 128 {
		Fail(c, "", fmt.Errorf("user name is too long"), http.StatusBadRequest)
		return
	}

	var userResp *account.GetUserByEmailResponse
	if strings.Contains(userName, "@") {
		userResp, err = grpcli.GetUserByEmail(context.Background(), &account.GetUserByEmailRequest{Email: userName})
		if err != nil {
			userResp.User.Id = 0 // default 0
		}
	}

	// query db to get auth code
	authLine := &entity.UserdebugAuth{}
	err = db.Transaction(func(tx *gorm.DB) error {
		return entity.NewUserdebugAuthQuerySet(tx).
			ClusterIDEq(entity.ID(clsID)).
			StatusEq(authCodeStatusNormal).
			ExpiresAtGt(time.Now()).
			OrderDescByExpiresAt().
			One(authLine)
	})
	if err != nil || authLine == nil {
		if err == gorm.ErrRecordNotFound {
			err = fmt.Errorf("userdebug auth code not found for cluster %d", clsID)
		}
		log.Info(c, "query for userdebug auth failed", zap.String("cluster", clusterID), zap.Error(err))
		Fail(c, "query for userdebug auth failed", err, http.StatusNotFound)
		return
	}

	// write auth record
	log.Info(c.Request.Context(), "requested userdebug auth code for cluster",
		zap.String("organization", organizationID),
		zap.String("cluster", clusterID),
		zap.String("user", userName),
	)
	if err := writeUserDebugAuthRecord(
		centralentity.ID(orgID),
		centralentity.ID(clsID),
		centralentity.ID(userResp.User.Id),
		authLine.ID,
		authCodeRecordOpsQuery,
		0,
	); err != nil {
		Fail(c, "failed to write auth record", err, http.StatusInternalServerError)
		return
	}

	c.JSON(http.StatusOK, types.UserDebugAuthorization{
		// not returning auth code as the api is not authenticated
		// Token: authLine.AuthCode,
		Valid: true, // the timestamp is checkd in query, so always valid
	})
}

func deleteUserDebugAuth(c *gin.Context) {
	clusterID := c.Param("cluster")
	organizationID := c.Param("organization")
	if clusterID == "" || organizationID == "" {
		Fail(c, "", fmt.Errorf("organization and cluster must be specified"), http.StatusBadRequest)
		return
	}

	clusterInfo, err, code := getClusterByID(c, organizationID, clusterID)
	if err != nil {
		Fail(c, "", err, code)
		return
	}

	// check for user permission
	if !clusterInfo.isOwner {
		Fail(c, "", fmt.Errorf("authorization is only allowed for organization owner"), http.StatusForbidden)
		return
	}

	// revoke the auth code
	revokedIDs := make([]entity.ID, 0)
	err = db.Transaction(func(tx *gorm.DB) error {
		authCodes := &[]entity.UserdebugAuth{}
		err := entity.NewUserdebugAuthQuerySet(tx).
			ClusterIDEq(entity.ID(clusterInfo.cluster.Id)).
			StatusEq(authCodeStatusNormal).
			ExpiresAtGt(time.Now()).
			OrderDescByCreatedAt().
			All(authCodes)
		if err != nil {
			return err
		}
		if authCodes == nil || len(*authCodes) < 1 {
			return fmt.Errorf("no valid auth code found for cluster")
		}
		for _, authLine := range *authCodes {
			authLine.Status = authCodeStatusRevoke
			if err := authLine.Update(tx, entity.UserdebugAuthDBSchema.Status); err != nil {
				return err
			}
			revokedIDs = append(revokedIDs, authLine.ID)
		}
		return nil
	})
	if err != nil {
		log.Info(c, "query for userdebug auth failed", zap.String("cluster", clusterID), zap.Error(err))
		Fail(c, "", err, http.StatusNotFound)
		return
	}
	// write auth record
	for _, authID := range revokedIDs {
		if err := writeUserDebugAuthRecord(
			*clusterInfo.tenantID,
			centralentity.ID(clusterInfo.cluster.Id),
			*clusterInfo.userID,
			authID,
			authCodeRecordOpsRevoke,
			0,
		); err != nil {
			Fail(c, "failed to write auth record", err, http.StatusInternalServerError)
			return
		}
	}

	Success(c, http.StatusOK, "revoked")
}

// DashboardAuthRequest is the same as ShareRequest in tidb-dashboard
// Ref: https://github.com/pingcap/tidb-dashboard file pkg/apiserver/user/code/router.go
type DashboardAuthRequest struct {
	ExpireInSeconds int64 `json:"expire_in_sec"`
	RevokeWritePriv bool  `json:"revoke_write_priv"`
}

// DashboardAuthResponse is the same as ShareRequest in tidb-dashboard
// Ref: https://github.com/pingcap/tidb-dashboard file pkg/apiserver/user/code/router.go
type DashboardAuthResponse struct {
	Code string `json:"code"`
}

// AuthRequestBody is the acceptable params of the POST request
type AuthRequestBody struct {
	Expires int `json:"expires"`
}

// postUserDebugAuth wraps the gin handler
func postUserDebugAuth(tlsConfig *tls.Config) gin.HandlerFunc {
	return func(ctx *gin.Context) {
		code, msg, err := createUserDebugAuth(ctx, tlsConfig)
		if err != nil || code >= 400 {
			if msg == "" {
				msg = "failed to create userdebug auth"
			}
			Fail(ctx, msg, err, code)
			return
		}
		ctx.Status(code)
	}
}

func createUserDebugAuth(
	c *gin.Context,
	tlsConfig *tls.Config,
) (int, string, error) {
	// parse parameters
	clusterID := c.Param("cluster")
	organizationID := c.Param("organization")
	if clusterID == "" || organizationID == "" {
		return http.StatusBadRequest, "", fmt.Errorf("organization and cluster must be specified")
	}

	clusterInfo, err, code := getClusterByID(c, organizationID, clusterID)
	if err != nil {
		return code, "", err
	}

	// check for user permission
	if !clusterInfo.isOwner {
		return http.StatusForbidden, "", fmt.Errorf("authorization is only allowed for organization owner")
	}

	var reqBody AuthRequestBody
	if err := c.ShouldBindJSON(&reqBody); err != nil {
		return http.StatusBadRequest, "", err
	}

	// check for existing auth code, do not allow creating new if one already
	// exist and still valid
	if err := db.Transaction(func(tx *gorm.DB) error {
		cnt, err := entity.NewUserdebugAuthQuerySet(tx).
			ClusterIDEq(entity.ID(clusterInfo.cluster.Id)).
			StatusEq(authCodeStatusNormal).
			ExpiresAtGt(time.Now()).
			Count()
		if err != nil {
			return err
		}
		if cnt > 0 {
			return ErrAlreadyAuthorized
		}
		return nil
	}); err != nil {
		if err == ErrAlreadyAuthorized {
			return http.StatusBadRequest, err.Error(), err
		}
		log.Error(c, "failed to check for existing auth",
			zap.String("cluster", clusterID),
			zap.Error(err),
		)
		return http.StatusInternalServerError, "failed to check for existing auth", nil
	}

	// the expires time is calculated before actually request dashboard, so it could
	// be slightly earlier than actual, which should not be a problem
	codeExpiresAt := time.Now().Add(time.Hour * time.Duration(reqBody.Expires))

	// get dashboard endpoint
	ctx, f := context.WithTimeout(context.Background(), 15*time.Second)
	defer f()
	baseURL, err := cluster.FindClusterDashboardBaseURL(
		ctx,
		encryptor,
		infraAPI,
		centralentity.ID(clusterInfo.cluster.GetId()),
		*clusterInfo.tenantID,
	)

	if err == nil && baseURL == "" {
		err = fmt.Errorf("get dashboard baseURL failed")
	}

	if err != nil {
		log.Error(c, "failed to get dashboard URL",
			zap.String("cluster", clusterID),
			zap.Error(err),
		)
		return http.StatusInternalServerError, "failed to get dashboard URL", nil
	}

	if !strings.HasPrefix(baseURL, "http://") &&
		!strings.HasPrefix(baseURL, "https://") {
		baseURL = "https://" + baseURL
	}
	u, err := url.Parse(baseURL)
	if err != nil {
		log.Error(c, "dashboard URL invalid",
			zap.String("cluster", clusterID),
			zap.Error(err),
		)
		return http.StatusInternalServerError, "dashboard URL invalid", nil
	}
	targetURL := u.Scheme + "://" + u.Host + dashboardAuthCodeURI + "?" + u.Query().Encode()
	log.Info(c,
		"get cluster dashboard url success",
		zap.String("cluster", clusterID),
		zap.Error(err),
	)
	// request dashboard to get auth code
	authReq := DashboardAuthRequest{
		ExpireInSeconds: int64(reqBody.Expires) * 60 * 60, // convert hours to seconds
	}
	authBody, err := json.Marshal(&authReq)
	if err != nil {
		log.Error(c, "failed to request auth from dashboard",
			zap.String("cluster", clusterID),
			zap.Error(err),
		)
		return http.StatusInternalServerError, "failed to request auth from dashboard", nil
	}

	req, err := http.NewRequest(http.MethodPost, targetURL, bytes.NewBuffer(authBody))
	if err != nil {
		log.Error(c, "failed to request auth from dashboard",
			zap.String("cluster", clusterID),
			zap.Error(err),
		)
		return http.StatusInternalServerError, "failed to request auth from dashboard", nil
	}
	client := &http.Client{
		Timeout:   time.Second * 10,
		Transport: &http.Transport{TLSClientConfig: tlsConfig},
	}
	resp, err := client.Do(req)
	if err != nil {
		log.Error(c, "failed to request auth from dashboard",
			zap.String("cluster", clusterID),
			zap.Error(err),
		)
		if os.IsTimeout(err) {
			return http.StatusGatewayTimeout, "failed to request auth from dashboard", nil
		}
		return http.StatusBadGateway, "failed to request auth from dashboard", nil
	}

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Error(c, "failed to request auth from dashboard",
			zap.String("cluster", clusterID),
			zap.Error(err),
		)
		return http.StatusInternalServerError, "failed to request auth from dashboard", nil
	}
	defer resp.Body.Close()

	var respObj DashboardAuthResponse
	if err := json.Unmarshal(respBody, &respObj); err != nil {
		log.Warn(c, "get dashbard share code failed",
			zap.String("cluster", clusterID),
			zap.String("body", string(respBody)),
			zap.Error(err),
		)
		// return http.StatusInternalServerError, nil, errors.Annotate(err, string(respBody))
		respObj.Code = genFakeSharingCode(
			*clusterInfo.userID,
			centralentity.ID(clusterInfo.cluster.Id),
			u.Query().Get("password"),
			authReq,
		)
	}

	authLine := &entity.UserdebugAuth{}
	err = db.Transaction(func(tx *gorm.DB) error {
		authLine = &entity.UserdebugAuth{
			ClusterID: entity.ID(clusterInfo.cluster.Id),
			AuthCode:  respObj.Code,
			Status:    authCodeStatusNormal,
			ExpiresAt: codeExpiresAt,
			CreatedBy: entity.ID(*clusterInfo.userID),
		}
		return authLine.Create(tx)
	})
	if err != nil {
		log.Error(c, "failed to create auth for cluster",
			zap.String("cluster", clusterID),
			zap.Error(err),
		)
		return http.StatusInternalServerError, "failed to create auth for cluster", nil
	}

	// write auth record
	if err := writeUserDebugAuthRecord(
		*clusterInfo.tenantID,
		centralentity.ID(clusterInfo.cluster.Id),
		*clusterInfo.userID,
		authLine.ID,
		authCodeRecordOpsAuth,
		reqBody.Expires,
	); err != nil {
		log.Error(c, "failed to write auth record",
			zap.String("cluster", clusterID),
			zap.Error(err),
		)
		return http.StatusInternalServerError, "failed to write auth record", nil
	}

	return http.StatusAccepted, "", nil
}

type clusterMetaInfo struct {
	cluster  *core.Cluster
	project  *account.Project
	tenantID *centralentity.ID
	userID   *centralentity.ID
	isOwner  bool
}

func getClusterByID(
	c *gin.Context,
	organizationID, clusterID string,
) (
	*clusterMetaInfo,
	error,
	int, // http status code
) {
	claim, err := apiserver.GetUserFromContext(c)
	if err != nil {
		return nil, err, http.StatusUnauthorized
	}

	clsID, err := strconv.ParseUint(clusterID, 10, 64)
	if err != nil {
		return nil, err, http.StatusBadRequest
	}
	orgID, err := strconv.ParseUint(organizationID, 10, 64)
	if err != nil {
		return nil, err, http.StatusBadRequest
	}

	if claim.Tenant.ID.Uint64() != uint64(orgID) {
		return nil, fmt.Errorf("tenant id mismatch"), http.StatusBadRequest
	}

	ctx, f := context.WithTimeout(context.Background(), 15*time.Second)
	defer f()
	projects, err := grpcli.ListProjectsByOrgsInUser(ctx, &account.ListProjectsByOrgsInUserRequest{
		UserId: uint64(claim.ID),
		OrgIds: []uint64{uint64(orgID)},
	})

	if err != nil {
		log.Error(c, "failed to get projects info",
			zap.String("cluster", clusterID),
			zap.Any("user", claim.ID),
			zap.Any("org", orgID),
			zap.Error(err),
		)
		Fail(c, "failed to get projects info", err, http.StatusInternalServerError)
	}

	var cluster *core.Cluster
	var proj *account.Project
	for _, orgProj := range projects.Projects {
		for _, proj = range orgProj.Projects {
			filter := &core.ClusterFilter{
				ClusterIds: []uint64{clsID},
				TenantIds:  []uint64{uint64(claim.Tenant.ID)},
				ProjectIds: []uint64{uint64(proj.Id)},
			}

			ctx, f := context.WithTimeout(context.Background(), 15*time.Second)
			defer f()
			cluster, err = grpcli.GetCluster(ctx, &core.ListClustersReq{Filter: filter}, clsID)
			if err == nil {
				break
			}
		}
	}
	if err != nil || cluster == nil {
		return nil, err, http.StatusNotFound
	}
	if cluster == nil {
		return nil, fmt.Errorf("cluster not found"), http.StatusNotFound
	}
	return &clusterMetaInfo{
		cluster:  cluster,
		project:  proj,
		tenantID: &claim.Tenant.ID,
		userID:   &claim.ID,
		isOwner:  claim.Role == acl.OrgRole_ORG_ROLE_OWNER,
	}, nil, 0
}

func writeUserDebugAuthRecord(orgID, clusterID, userID centralentity.ID, authID entity.ID, operation string, timeSpan int) error {
	return db.Transaction(func(tx *gorm.DB) error {
		r := entity.UserdebugAuthRecord{
			ClusterID:     entity.ID(clusterID),
			UserID:        entity.ID(userID),
			AuthID:        authID,
			AuthOperation: operation,
			TimeSpan:      timeSpan,
		}
		return r.Create(tx)
	})
}

func getUserDebugAuthRecords(c *gin.Context) {
	clusterID := c.Param("cluster")
	organizationID := c.Param("organization")
	if clusterID == "" || organizationID == "" {
		Fail(c, "", fmt.Errorf("organization and cluster must be specified"), http.StatusBadRequest)
		return
	}
	var err error
	var pageNum int
	pn := c.Query("page")
	if pn != "" {
		pageNum, err = strconv.Atoi(pn)
		if err != nil {
			Fail(c, "", err, http.StatusBadRequest)
			return
		}
	} else {
		pageNum = 1
	}
	var perPage int
	pp := c.Query("per_page")
	if pp != "" {
		perPage, err = strconv.Atoi(pp)
		if err != nil {
			Fail(c, "", err, http.StatusBadRequest)
			return
		}
	} else {
		perPage = userDebugItemsPerPageDefault
	}

	clusterInfo, err, code := getClusterByID(c, organizationID, clusterID)
	if err != nil {
		Fail(c, "", err, code)
		return
	}

	// query db to get auth records
	result := &types.UserDebugAuthorizationRecords{
		Items: make([]*types.UserDebugAuthorizationRecordItem, 0),
	}
	records := &[]entity.UserdebugAuthRecord{}
	err = db.Transaction(func(tx *gorm.DB) error {
		// query for total number first
		if result.Total, err = entity.NewUserdebugAuthRecordQuerySet(tx).
			ClusterIDEq(entity.ID(clusterInfo.cluster.Id)).
			Count(); err != nil {
			return err
		}

		// query for needed records
		return entity.NewUserdebugAuthRecordQuerySet(tx).
			ClusterIDEq(entity.ID(clusterInfo.cluster.Id)).
			OrderDescByCreatedAt().
			Offset(perPage * (pageNum - 1)).
			Limit(perPage).
			All(records)
	})
	if err != nil || records == nil {
		log.Info(c, "query for userdebug auth records failed", zap.String("cluster", clusterID), zap.Error(err))
		Fail(c, "", err, http.StatusNotFound)
		return
	}

	if len(*records) < 1 {
		c.JSON(http.StatusOK, "no authentication record")
		return
	}

	// use grpc to get user info
	userIDSet := make(map[entity.ID]struct{})
	userInfo := []*account.User{}
	for _, r := range *records {
		if _, found := userIDSet[r.UserID]; !found {
			userIDSet[r.UserID] = struct{}{}
			ctx, f := context.WithTimeout(context.Background(), 15*time.Second)
			defer f()
			user, err := grpcli.GetUserByID(ctx, &account.GetUserByIDRequest{UserId: r.UserID.Uint64()})
			if err != nil || user.User == nil {
				log.Warn(c, "query for user info failed for records", zap.String("cluster", clusterID), zap.Error(err))
				continue
			}
			userInfo = append(userInfo, user.User)
		}
	}

	userInfoMap := make(map[uint64]*account.User)
	for _, u := range userInfo {
		userInfoMap[u.Id] = u
	}

	for _, r := range *records {
		record := &types.UserDebugAuthorizationRecordItem{
			AuthID: fmt.Sprintf("%d", r.AuthID),
			User: &types.UserDebugAuthorizedBy{
				ID:        fmt.Sprintf("%d", r.UserID),
				FirstName: userInfoMap[r.UserID.Uint64()].FirstName,
				LastName:  userInfoMap[r.UserID.Uint64()].LastName,
				Email:     userInfoMap[r.UserID.Uint64()].Email,
			},
			Operation: r.AuthOperation,
			TimeSpan:  int64(r.TimeSpan),
			CreatedAt: r.CreatedAt.Unix(),
		}
		if r.Auth != nil {
			record.ExpiresAt = r.Auth.ExpiresAt.Unix()
		}

		result.Items = append(result.Items, record)

	}
	c.JSON(http.StatusOK, result)
}

func getUserDebugAuthRecordsForAuth(c *gin.Context) {
	clusterID := c.Param("cluster")
	organizationID := c.Param("organization")
	authID := c.Param("auth_id")
	if clusterID == "" || organizationID == "" || authID == "" {
		Fail(c, "", fmt.Errorf("organization, cluster and auth_id must be specified"), http.StatusBadRequest)
		return
	}

	authCodeID, err := strconv.ParseUint(authID, 10, 64)
	if err != nil {
		Fail(c, "", err, http.StatusBadRequest)
		return
	}

	var pageNum int
	pn := c.Query("page")
	if pn != "" {
		pageNum, err = strconv.Atoi(pn)
		if err != nil {
			Fail(c, "", err, http.StatusBadRequest)
			return
		}
	} else {
		pageNum = 1
	}
	var perPage int
	pp := c.Query("per_page")
	if pp != "" {
		perPage, err = strconv.Atoi(pp)
		if err != nil {
			Fail(c, "", err, http.StatusBadRequest)
			return
		}
	} else {
		perPage = userDebugItemsPerPageDefault
	}

	clusterInfo, err, code := getClusterByID(c, organizationID, clusterID)
	if err != nil {
		Fail(c, "", err, code)
		return
	}

	// query db to get auth records
	result := &types.UserDebugAuthorizationRecords{
		Items: make([]*types.UserDebugAuthorizationRecordItem, 0),
	}
	records := &[]entity.UserdebugAuthRecord{}
	err = db.Transaction(func(tx *gorm.DB) error {

		// query for total number first
		if result.Total, err = entity.NewUserdebugAuthRecordQuerySet(tx).
			ClusterIDEq(entity.ID(clusterInfo.cluster.Id)).
			AuthIDEq(entity.ID(authCodeID)).
			Count(); err != nil {
			return err
		}

		// query for needed records
		return entity.NewUserdebugAuthRecordQuerySet(tx).
			ClusterIDEq(entity.ID(clusterInfo.cluster.Id)).
			AuthIDEq(entity.ID(authCodeID)).
			OrderDescByCreatedAt().
			Offset(perPage * (pageNum - 1)).
			Limit(perPage).
			All(records)
	})
	if err != nil || records == nil {
		log.Info(c, "query for userdebug auth records failed", zap.String("cluster", clusterID), zap.Error(err))
		Fail(c, "", err, http.StatusNotFound)
		return
	}

	if len(*records) < 1 {
		c.JSON(http.StatusOK, "no authentication record")
		return
	}

	// use grpc to get user info
	userIDSet := make(map[entity.ID]struct{})
	userInfo := []*account.User{}
	for _, r := range *records {
		if _, found := userIDSet[r.UserID]; !found {
			userIDSet[r.UserID] = struct{}{}
			ctx, f := context.WithTimeout(context.Background(), 15*time.Second)
			defer f()
			user, err := grpcli.GetUserByID(ctx, &account.GetUserByIDRequest{UserId: r.UserID.Uint64()})
			if err != nil || user.User == nil {
				log.Warn(c, "query for user info failed for records", zap.String("cluster", clusterID), zap.Error(err))
				continue
			}
			userInfo = append(userInfo, user.User)
		}
	}

	userInfoMap := make(map[uint64]*account.User)
	for _, u := range userInfo {
		userInfoMap[u.Id] = u
	}

	for _, r := range *records {
		record := &types.UserDebugAuthorizationRecordItem{
			User: &types.UserDebugAuthorizedBy{
				ID:        fmt.Sprintf("%d", r.UserID),
				FirstName: userInfoMap[r.UserID.Uint64()].FirstName,
				LastName:  userInfoMap[r.UserID.Uint64()].LastName,
				Email:     userInfoMap[r.UserID.Uint64()].Email,
			},
			Operation: r.AuthOperation,
			TimeSpan:  int64(r.TimeSpan),
			CreatedAt: r.CreatedAt.Unix(),
		}
		if r.Auth != nil {
			record.ExpiresAt = r.Auth.ExpiresAt.Unix()
		}

		result.Items = append(result.Items, record)
	}

	c.JSON(http.StatusOK, result)
}

type fakeSharingCode struct {
	CreatedBy       centralentity.ID
	ClusterID       centralentity.ID
	ExpireAt        time.Time
	RevokeWritePriv bool
}

func genFakeSharingCode(userID, clusterID centralentity.ID, password string, req DashboardAuthRequest) string {
	if req.ExpireInSeconds < 0 {
		return ""
	}
	key := sha256.Sum256([]byte(password))
	shared := fakeSharingCode{
		CreatedBy:       userID,
		ClusterID:       clusterID,
		ExpireAt:        time.Now().Add(time.Second * time.Duration(req.ExpireInSeconds)),
		RevokeWritePriv: req.RevokeWritePriv,
	}

	b, err := msgpack.Marshal(&shared)
	if err != nil {
		// Do not output anything about how serialization is failed to avoid potential leaks.
		return ""
	}

	encrypted, err := cryptopasta.Encrypt(b, &key)
	if err != nil {
		return ""
	}

	codeInHex := hex.EncodeToString(encrypted)
	return codeInHex
}
