package userdebug

import (
	"context"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"net/http"
	"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"

	"github.com/pingcap-inc/clinic_cloud_server/pkg/common"
	"github.com/pingcap-inc/clinic_cloud_server/pkg/db"

	"github.com/pingcap/errcode"
	"github.com/pingcap/log"
	"github.com/vmihailenco/msgpack"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

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

//	@Id				GetUserDebugFeatures
//	@Summary		List userdebug features
//	@Tags			userdebug
//	@Description	list userdebug features
//	@Produce		json
//	@Param			organization	path		string	true	"organization id"
//	@Param			X-Provider		header		string	true	"provider aws/gcp"
//	@Param			X-Region		header		string	true	"region"
//	@Param			X-Org-Id		header		string	true	"organization id from dbaas"
//	@Param			X-Project-Id	header		string	true	"project id from dbaas"
//	@Param			X-Cluster-Id	header		string	true	"cluster id from dbaas"
//	@Success		200				{object}	UserDebugFeatureList
//	@Router			/{organization}/features [get]
//
// Service is the userdebug service
func (s *Service) 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 []db.UserdebugFeatureConfig
	err = db.Transaction(func(tx *gorm.DB) error {
		return db.NewUserdebugFeatureConfigQuerySet(tx).
			OrganizationIDEq(orgID).
			All(&featureList)
	})
	if err != nil && err != gorm.ErrRecordNotFound {
		log.Info("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 := UserDebugFeatureList{
		OrgID:    organizationID,
		Features: make([]*UserDebugFeatureListFeaturesItems, 0),
	}
	for _, f := range featureList {
		result.Features = append(result.Features, &UserDebugFeatureListFeaturesItems{
			Key:    f.FeatureKey,
			Status: f.Status,
		})
	}

	c.JSON(http.StatusOK, result)
}

//	@Id				GetUserDebugAuthList
//	@Summary		get userdebug auth list
//	@Tags			userdebug
//	@Description	get userdebug auth list
//	@Produce		json
//	@Param			organization	path		string	true	"organization id"
//	@Param			X-Provider		header		string	true	"provider aws/gcp"
//	@Param			X-Region		header		string	true	"region"
//	@Param			X-Org-Id		header		string	true	"organization id from dbaas"
//	@Param			X-Project-Id	header		string	true	"project id from dbaas"
//	@Param			X-Cluster-Id	header		string	true	"cluster id from dbaas"
//	@Success		200				{object}	UserDebugAuthorizationList
//	@Router			/{organization}/authorizations [get]
//
// getUserDebugAuthList returns the list of userdebug authorizations
func (s *Service) 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
	}

	data, err := base64.StdEncoding.DecodeString(c.GetHeader("X-CLOUD-USER"))
	if err != nil {
		log.Error("decode user failed", zap.String("user", c.GetHeader("X-CLOUD-USER")), zap.Error(err))
		common.AuthError(c, "invalid user")
		return
	}
	var u account.User
	if err := json.Unmarshal(data, &u); err != nil {
		log.Error("decode user failed", zap.String("data", string(data)), zap.Error(err))
		common.AuthError(c, "invalid user")
		return
	}

	type clsInfo struct {
		projID   int64
		projName string
		clsName  string
	}

	// list all clusters of the organization
	clusterIDs := make([]uint64, 0)
	clusterInfo := make(map[uint64]*clsInfo)

	projects, err := s.AccountC.ListProjectsByOrgsInUser(c.Request.Context(), &account.ListProjectsByOrgsInUserRequest{
		OrgIds: []uint64{orgID},
		UserId: u.Id,
	})
	if err != nil {
		Fail(c, "failed to get projects info", err, http.StatusInternalServerError)
		return
	}

	if len(projects.Projects) == 0 {
		log.Error("failed to lits project info",
			zap.Uint64("user", u.Id),
			zap.Uint64("org", orgID),
			zap.Error(err),
		)
		Fail(c, "no projects exist", err, http.StatusNotFound)
		return
	}

	for _, orgProj := range projects.Projects {
		for _, proj := range orgProj.Projects {
			var clusterResp *core.ListClustersResp
			filter := &core.ClusterFilter{
				TenantIds:  []uint64{orgID},
				ProjectIds: []uint64{proj.Id},
				Statuses: []core.ClusterStatus{
					core.ClusterStatus_CLUSTER_STATUS_NORMAL,
					// core.ClusterStatus_CLUSTER_STATUS_HIBERNATED,
					core.ClusterStatus_CLUSTER_STATUS_PAUSING,
					core.ClusterStatus_CLUSTER_STATUS_PAUSED,
					core.ClusterStatus_CLUSTER_STATUS_RESUMING,
					core.ClusterStatus_CLUSTER_STATUS_SCALING,
				},
			}
			clusterResp, err = s.CoreC.ListClusters(c.Request.Context(), &core.ListClustersReq{Filter: filter})
			if err != nil {
				Fail(c, "failed to get cluster info", err, http.StatusInternalServerError)
				return
			}
			for _, clsID := range clusterResp.GetOrderedClusterIds() {
				clusterIDs = append(clusterIDs, clsID)
				clusterInfo[clsID] = &clsInfo{
					projID:   int64(proj.Id),
					projName: proj.Name,
					clsName:  clusterResp.Clusters[clsID].GetName(),
				}
			}
		}
	}

	// no cluster found
	result := &UserDebugAuthorizationList{
		Items: make([]*UserDebugAuthorizationDetail, 0),
	}

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

		// query for needed records
		return db.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("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 := &UserDebugAuthorizationDetail{
			ID:          fmt.Sprintf("%d", authItem.ID),
			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)
}

//	@Id				GetUserDebugAuthDetail
//	@Summary		get the detail of a userdebug auth code
//	@Tags			userdebug
//	@Description	get the detail of a userdebug auth code
//	@Produce		json
//	@Param			organization	path		string	true	"organization id"
//	@Param			cluster			path		string	true	"cluster id"
//	@Param			auth_id			path		string	true	"auth id"
//	@Param			X-Provider		header		string	true	"provider aws/gcp"
//	@Param			X-Region		header		string	true	"region"
//	@Param			X-Org-Id		header		string	true	"organization id from dbaas"
//	@Param			X-Project-Id	header		string	true	"project id from dbaas"
//	@Param			X-Cluster-Id	header		string	true	"cluster id from dbaas"
//	@Success		200				{object}	UserDebugAuthorizationDetail
//	@Router			/{organization}/{cluster}/authorization/details/{auth_id} [get]
//
// getUserDebugAuthDetail returns the detail of a userdebug auth code
func (s *Service) 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, code, err := getClusterByID(c, s, 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 := &db.UserdebugAuth{}
	err = db.Transaction(func(tx *gorm.DB) error {
		return db.NewUserdebugAuthQuerySet(tx).
			ClusterIDEq(clusterInfo.cluster.Id).
			IDEq(uint(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("query for userdebug auth failed", zap.String("cluster", clusterID), zap.Error(err))
		Fail(c, "", err, http.StatusNotFound)
		return
	}

	user, err := s.AccountC.GetUserByID(c.Request.Context(), &account.GetUserByIDRequest{UserId: authLine.CreatedBy})

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

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

	result := &UserDebugAuthorizationDetail{
		ID:          fmt.Sprintf("%d", authLine.ID),
		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: &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)
}

//	@Id				GetUserDebugAuthCode
//	@Summary		get userdebug auth code
//	@Tags			userdebug
//	@Description	get userdebug auth code
//	@Produce		json
//	@Param			organization	path		string	true	"organization id"
//	@Param			cluster			path		string	true	"cluster id"
//	@Param			X-Provider		header		string	true	"provider aws/gcp"
//	@Param			X-Region		header		string	true	"region"
//	@Param			X-Org-Id		header		string	true	"organization id from dbaas"
//	@Param			X-Project-Id	header		string	true	"project id from dbaas"
//	@Param			X-Cluster-Id	header		string	true	"cluster id from dbaas"
//	@Param			user			query		string	false	"user name"
//	@Success		200				{object}	UserDebugAuthorization
//	@Router			/{organization}/{cluster}/authorization [get]
//
// getUserDebugAuthCode returns the userdebug auth code
func (s *Service) 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
	}

	userId := uint64(0)
	if strings.Contains(userName, "@") {
		userResp, err := s.AccountC.GetUserByEmail(
			context.Background(),
			&account.GetUserByEmailRequest{Email: userName})
		if err == nil {
			userId = userResp.Id
		}
	}

	// query db to get auth code
	authLine := &db.UserdebugAuth{}
	err = db.Transaction(func(tx *gorm.DB) error {
		return db.NewUserdebugAuthQuerySet(tx).
			ClusterIDEq(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("query for userdebug auth failed", zap.String("cluster", clusterID),
			zap.String("user", userName), zap.Error(err))
		Fail(c, "query for userdebug auth failed", err, http.StatusNotFound)
		return
	}

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

	c.JSON(http.StatusOK, 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
	})
}

//	@Id				DeleteUserDebugAuth
//	@Summary		revokes the userdebug auth code
//	@Tags			userdebug
//	@Description	revokes the userdebug auth code
//	@Produce		json
//	@Param			organization	path		string	true	"organization id"
//	@Param			cluster			path		string	true	"cluster id"
//	@Param			X-Provider		header		string	true	"provider aws/gcp"
//	@Param			X-Region		header		string	true	"region"
//	@Param			X-Org-Id		header		string	true	"organization id from dbaas"
//	@Param			X-Project-Id	header		string	true	"project id from dbaas"
//	@Param			X-Cluster-Id	header		string	true	"cluster id from dbaas"
//	@Success		200				{string}	success
//	@Router			/{organization}/{cluster}/authorization [delete]
//
// deleteUserDebugAuth revokes the userdebug auth code
func (s *Service) 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, code, err := getClusterByID(c, s, 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([]uint64, 0)
	err = db.Transaction(func(tx *gorm.DB) error {
		authCodes := &[]db.UserdebugAuth{}
		err := db.NewUserdebugAuthQuerySet(tx).
			ClusterIDEq(clusterInfo.cluster.Id).
			StatusEq(AuthCodeStatusNormal).
			ExpiresAtGt(time.Now()).
			OrderDescByCreatedAt().
			All(authCodes)
		if err != nil {
			return err
		}
		if 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, db.UserdebugAuthDBSchema.Status); err != nil {
				return err
			}
			revokedIDs = append(revokedIDs, uint64(authLine.ID))
		}
		return nil
	})
	if err != nil {
		log.Info("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,
			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")
}

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

// DashboardAuthRequest is the same as ShareRequest in tidb-dashboard
type DashboardAuthRequest struct {
	ExpireInSeconds int64 `json:"expire_in_sec"`
	RevokeWritePriv bool  `json:"revoke_write_priv"`
}

// DashboardAuthResponse is the same as ShareRequest in tidb-dashboard
type DashboardAuthResponse struct {
	Code string `json:"code"`
}

//	@Id				CreateUserDebugAuthCode
//	@Summary		create the userdebug auth code
//	@Tags			userdebug
//	@Description	create the userdebug auth code
//	@Produce		json
//	@Param			organization	path		string	true	"organization id"
//	@Param			cluster			path		string	true	"cluster id"
//	@Param			X-Provider		header		string	true	"provider aws/gcp"
//	@Param			X-Region		header		string	true	"region"
//	@Param			X-Org-Id		header		string	true	"organization id from dbaas"
//	@Param			X-Project-Id	header		string	true	"project id from dbaas"
//	@Param			X-Cluster-Id	header		string	true	"cluster id from dbaas"
//	@Success		200				{string}	success
//	@Router			/{organization}/{cluster}/authorization [post]
//
// postUserDebugAuth create the userdebug auth code
func (s *Service) postUserDebugAuth() gin.HandlerFunc {
	return func(ctx *gin.Context) {
		code, msg, err := createUserDebugAuth(ctx, s)
		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, s *Service) (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, code, err := getClusterByID(c, s, organizationID, clusterID)
	if err != nil {
		return code, err.Error(), 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 := db.NewUserdebugAuthQuerySet(tx).
			ClusterIDEq(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("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))

	// request dashboard to get auth code
	authReq := DashboardAuthRequest{
		ExpireInSeconds: int64(reqBody.Expires) * 60 * 60, // convert hours to seconds
	}

	var respObj DashboardAuthResponse

	// return http.StatusInternalServerError, nil, errors.Annotate(err, string(respBody))
	respObj.Code = genFakeSharingCode(
		*clusterInfo.userID,
		clusterInfo.cluster.Id,
		("tidbcloud-" + clusterInfo.cluster.Name + "-admin"),
		authReq,
	)

	authLine := &db.UserdebugAuth{}
	err = db.Transaction(func(tx *gorm.DB) error {
		authLine = &db.UserdebugAuth{
			ClusterID: clusterInfo.cluster.Id,
			AuthCode:  respObj.Code,
			Status:    AuthCodeStatusNormal,
			ExpiresAt: codeExpiresAt,
			CreatedBy: *clusterInfo.userID,
		}
		return authLine.Create(tx)
	})
	if err != nil {
		log.Error("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,
		clusterInfo.cluster.Id,
		*clusterInfo.userID,
		uint64(authLine.ID),
		AuthCodeRecordOpsAuth,
		reqBody.Expires,
	); err != nil {
		log.Error("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 *uint64
	userID   *uint64
	isOwner  bool
}

func getClusterByID(
	c *gin.Context,
	s *Service,
	organizationID, clusterID string,
) (
	*clusterMetaInfo,
	int, // http status code
	error,
) {
	data, err := base64.StdEncoding.DecodeString(c.GetHeader("X-CLOUD-USER"))
	if err != nil {
		log.Error("decode user failed", zap.String("user", c.GetHeader("X-CLOUD-USER")), zap.Error(err))
		return nil, http.StatusBadRequest, err
	}
	var u account.User
	if err := json.Unmarshal(data, &u); err != nil {
		log.Error("decode user failed", zap.String("data", string(data)), zap.Error(err))
		return nil, http.StatusBadRequest, err
	}

	clsID, err := strconv.ParseUint(clusterID, 10, 64)
	if err != nil {
		log.Error("parse cluster id failed", zap.Uint64("cluster id", clsID), zap.Error(err))
		return nil, http.StatusBadRequest, err
	}
	orgID, err := strconv.ParseUint(organizationID, 10, 64)
	if err != nil {
		log.Error("parse org id failed", zap.Uint64("org id", orgID), zap.Error(err))
		return nil, http.StatusBadRequest, err
	}

	userReq := &account.GetAuthUserRequest{
		UserEmail:    u.Email,
		OrgId:        orgID,
		SkipCheckOrg: false,
	}

	userResp, err := s.AccountC.GetAuthUser(c.Request.Context(), userReq)
	if err != nil {
		log.Error("failed to get auth user info",
			zap.String("cluster", clusterID),
			zap.String("user", u.Email),
			zap.Uint64("org", orgID),
			zap.Error(err),
		)
		Fail(c, "failed to get auth user info", err, http.StatusInternalServerError)
	}

	projects, err := s.AccountC.ListProjectsByOrgsInUser(c.Request.Context(), &account.ListProjectsByOrgsInUserRequest{
		OrgIds: []uint64{orgID},
		UserId: u.Id,
	})
	if err != nil {
		log.Error("failed to get projects info",
			zap.String("cluster", clusterID),
			zap.String("user", u.Email),
			zap.Uint64("org", orgID),
			zap.Error(err),
		)
		Fail(c, "failed to get projects info", err, http.StatusInternalServerError)
		return nil, http.StatusBadRequest, err
	}

	if len(projects.Projects) == 0 {
		log.Error("failed to lits project info",
			zap.String("cluster", clusterID),
			zap.String("user", u.Email),
			zap.Uint64("org", orgID),
			zap.Error(err),
		)
		Fail(c, "no projects exist", err, http.StatusNotFound)
		return nil, http.StatusInternalServerError, err
	}

	// filter := &core.ClusterFilter{
	// 	ClusterIds: []uint64{clsID},
	// 	TenantIds:  []uint64{orgID},
	// 	ProjectIds: make([]uint64, 0),
	// 	Statuses: []core.ClusterStatus{
	// 		core.ClusterStatus_CLUSTER_STATUS_NORMAL,
	// 		// core.ClusterStatus_CLUSTER_STATUS_HIBERNATED,
	// 		core.ClusterStatus_CLUSTER_STATUS_PAUSING,
	// 		core.ClusterStatus_CLUSTER_STATUS_PAUSED,
	// 		core.ClusterStatus_CLUSTER_STATUS_RESUMING,
	// 		core.ClusterStatus_CLUSTER_STATUS_SCALING,
	// 	},
	// }
	// for _, orgProj := range projects.Projects {
	// 	if orgProj.OrgId != orgID {
	// 		continue
	// 	}
	// 	for _, proj := range orgProj.Projects {
	// 		filter.ProjectIds = append(filter.ProjectIds, proj.Id)
	// 	}
	// }

	cluster, err := s.CoreC.GetCluster(c.Request.Context(), clsID)
	if err != nil {
		log.Warn("get cluster failed",
			zap.String("cluster", clusterID),
			zap.String("org", organizationID),
			zap.String("user", u.Email),
			zap.Error(err))
		return nil, http.StatusInternalServerError, fmt.Errorf("failed to get cluster info")
	}

	if cluster == nil {
		log.Error("cluster not found or you are not the account administrator",
			zap.String("cluster", clusterID),
			zap.String("org", organizationID),
			zap.String("user", u.Email),
			zap.Error(err))
		return nil, http.StatusNotFound, fmt.Errorf("you are not the account administrator or cluster status is not normal")
	}

	// get the project info
	var proj *account.Project
	for _, orgProj := range projects.Projects {
		for _, p := range orgProj.Projects {
			if p.Id == cluster.ProjectId {
				proj = p
				break
			}
		}
	}

	if proj == nil {
		log.Error("failed to get project info", zap.Uint64("project id", cluster.ProjectId), zap.String("org", organizationID))
		return nil, http.StatusInternalServerError, fmt.Errorf("failed to get project info")
	}

	return &clusterMetaInfo{
		cluster:  cluster,
		project:  proj,
		tenantID: &orgID,
		userID:   &u.Id,
		isOwner:  userResp.OrgRole == acl.OrgRole_ORG_ROLE_OWNER,
	}, 0, nil
}

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

//	@Id				GetUserDebugAuthRecords
//
//	@Summary		get userdebug auth code records
//	@Tags			userdebug
//	@Description	get use userdebug auth code records
//	@Produce		json
//	@Param			organization	path		string	true	"organization id"
//	@Param			cluster			path		string	true	"cluster id"
//	@Param			X-Provider		header		string	true	"provider aws/gcp"
//	@Param			X-Region		header		string	true	"region"
//	@Param			X-Org-Id		header		string	true	"organization id from dbaas"
//	@Param			X-Project-Id	header		string	true	"project id from dbaas"
//	@Param			X-Cluster-Id	header		string	true	"cluster id from dbaas"
//	@Param			page			query		string	false	"page number"
//	@Param			per_page		query		string	false	"records per page"
//	@Success		200				{object}	UserDebugAuthorizationRecords
//	@Router			/{organization}/{cluster}/authorization/records [get]
//
// getUserDebugAuthRecords get userdebug auth code records
func (s *Service) 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, code, err := getClusterByID(c, s, organizationID, clusterID)
	if err != nil {
		Fail(c, "", err, code)
		return
	}

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

		// query for needed records
		return db.NewUserdebugAuthRecordQuerySet(tx).
			ClusterIDEq(clusterInfo.cluster.Id).
			OrderDescByCreatedAt().
			Offset(perPage * (pageNum - 1)).
			Limit(perPage).
			All(records)
	})
	if err != nil || records == nil {
		log.Info("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[uint64]struct{})
	userInfo := []*account.User{}
	for _, r := range *records {
		if _, found := userIDSet[r.UserID]; !found {
			userIDSet[r.UserID] = struct{}{}
			user, err := s.AccountC.GetUserByID(c.Request.Context(), &account.GetUserByIDRequest{UserId: r.UserID})
			if err != nil || user.User == nil {
				log.Warn("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 := &UserDebugAuthorizationRecordItem{
			AuthID: fmt.Sprintf("%d", r.AuthID),
			User: &UserDebugAuthorizedBy{
				ID:        fmt.Sprintf("%d", r.UserID),
				FirstName: userInfoMap[r.UserID].FirstName,
				LastName:  userInfoMap[r.UserID].LastName,
				Email:     userInfoMap[r.UserID].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)
}

//	@Id				GetUserDebugAuthRecordsForAuth
//	@Summary		get userdebug auth code records for a specific auth code
//	@Tags			userdebug
//	@Description	get userdebug auth code records for a specific auth code
//	@Produce		json
//	@Param			organization	path		string	true	"organization id"
//	@Param			cluster			path		string	true	"cluster id"
//	@Param			auth_id			path		string	true	"auth id"
//	@Param			X-Provider		header		string	true	"provider aws/gcp"
//	@Param			X-Region		header		string	true	"region"
//	@Param			X-Org-Id		header		string	true	"organization id from dbaas"
//	@Param			X-Project-Id	header		string	true	"project id from dbaas"
//	@Param			X-Cluster-Id	header		string	true	"cluster id from dbaas"
//	@Param			page			query		string	false	"page number"
//	@Param			per_page		query		string	false	"records per page"
//	@Success		200				{object}	UserDebugAuthorizationRecords
//	@Router			/{organization}/{cluster}/authorization/records/{auth_id} [get]
//
// getUserDebugAuthRecordsForAuth get userdebug auth code records for a specific auth code
func (s *Service) 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, code, err := getClusterByID(c, s, organizationID, clusterID)
	if err != nil {
		Fail(c, "", err, code)
		return
	}

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

		// query for needed records
		return db.NewUserdebugAuthRecordQuerySet(tx).
			ClusterIDEq(clusterInfo.cluster.Id).
			AuthIDEq(authCodeID).
			OrderDescByCreatedAt().
			Offset(perPage * (pageNum - 1)).
			Limit(perPage).
			All(records)
	})
	if err != nil || records == nil {
		log.Info("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[uint64]struct{})
	userInfo := []*account.User{}
	for _, r := range *records {
		if _, found := userIDSet[r.UserID]; !found {
			userIDSet[r.UserID] = struct{}{}
			user, err := s.AccountC.GetUserByID(c.Request.Context(), &account.GetUserByIDRequest{UserId: r.UserID})
			if err != nil || user.User == nil {
				log.Warn("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 := &UserDebugAuthorizationRecordItem{
			User: &UserDebugAuthorizedBy{
				ID:        fmt.Sprintf("%d", r.UserID),
				FirstName: userInfoMap[r.UserID].FirstName,
				LastName:  userInfoMap[r.UserID].LastName,
				Email:     userInfoMap[r.UserID].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       uint64
	ClusterID       uint64
	ExpireAt        time.Time
	RevokeWritePriv bool
}

func genFakeSharingCode(userID, clusterID uint64, 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
}

func Success(c *gin.Context, code int, message string) {
	c.JSON(code, common.ResponseMessage{
		Code:    int64(code),
		Message: message,
	})
}

func Fail(c *gin.Context, msg string, err error, code int) {
	log.Error("got error", zap.Error(err), zap.Int("code", code))
	if e, ok := err.(errcode.ErrorCode); ok {
		if code := e.Code().HTTPCode(); code >= 400 && code < 500 {
			c.JSON(code, common.ResponseMessage{Code: int64(code), Message: e.Error()})
			return
		}
	}

	if code >= 500 {
		log.Info(fmt.Sprintf("internal server error: %s", msg), zap.Error(err))
		msg = fmt.Sprintf("%s, please check the cluster status or contact support.", msg)
	}

	c.JSON(code, common.ResponseMessage{Code: int64(code), Message: msg})
}
