package api

import (
	"fmt"
	"net/http"
	"strconv"
	"strings"

	auditlogsvc "github.com/tidbcloud/account/internel/svc/auditlog"

	"github.com/tidbcloud/account/commons/config"

	"github.com/tidbcloud/account/central/utils"
	"github.com/tidbcloud/account/internel/dao/entity"
	"github.com/tidbcloud/account/internel/svc/comsvc"
	"github.com/tidbcloud/account/internel/svc/user"
	"github.com/tidbcloud/account/internel/svc/user/oneconsole"
	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"github.com/tidbcloud/tiinf/sdk/auth"
	"go.uber.org/zap"

	"github.com/gin-gonic/gin"
)

var usersPathPattern2ResourceMap = map[string]string{
	"get:/orgs/(\\d+)/v1beta1/members":                         "account/openapi/orgs/{org_id}/users/list",
	"post:/orgs/(\\d+)/v1beta1/members":                        "account/openapi/orgs/{org_id}/users/invite",
	"patch:/orgs/(\\d+)/v1beta1/members/[^/]+":                 "account/openapi/orgs/{org_id}/users/update",
	"post:/orgs/(\\d+)/v1beta1/members/batchUpdate":            "account/openapi/orgs/{org_id}/users/batch_update",
	"post:/orgs/(\\d+)/v1beta1/members/[^/]+/resendInvitation": "account/openapi/orgs/{org_id}/users/resend_invitation",
	"delete:/orgs/(\\d+)/v1beta1/members/[^/]+":                "account/openapi/orgs/{org_id}/users/delete",
}

// create swag doc command: project root directory exec the command
// swag init -d central/api/ -g openapi_users.go --tags "Member" --output central/docs/openapi/users

// @title Account System Members OPENAPI
// @version v1beta1
// @description This is the members related API.
// @host iam.tidbapi.com
// @BasePath /v1beta1
func RegisterUsersRouter(e gin.IRouter) {
	r := e.Group("/v1beta1/members")
	auth.InitAccountClientByEndpoint(config.Global().GrpcConfig.AccountSvcAddr, 0)
	r.Use(
		auth.GinGetIdentityInfoInterceptor(&auth.IdentityAuthOption{
			EnableLog: true,
		}),
		auth.GinPermissionCheckInterceptor(&auth.PermissionCheckerCommonParam{
			IdentityF:               auth.GinGetIdentityFunc,
			PathPattern2ResourceMap: usersPathPattern2ResourceMap,
			GenAccessParam4GinF:     genUsersAccessParam4Gin,
			HttpReqCheckRetHandleF:  usersOpenApiPermissionCheckResultHandleFunc,
			EnableLog:               true,
		}),
	)
	r.GET("", ListUsersOpenApiHandler)
	r.POST("", InviteUsersOpenApiHandler)
	r.PATCH("/:user_id", UpdateUserOpenApiHandler)
	r.POST("/batchUpdate", BatchUpdateUsersOpenApiHandler)
	r.POST("/:user_id/resendInvitation", ResendInvitationOpenApiHandler)
	r.DELETE("/:user_id", DeleteUserOpenApiHandler)
}

func genUsersAccessParam4Gin(c *gin.Context) (*auth.AccessParam, error) {
	ctx := c.Request.Context()
	authInfo, err := auth.GetIdentityInfoFromGinContext(c)
	if err != nil {
		return nil, err
	}
	var orgId uint64
	if authInfo.IdentityType == auth.AuthnIdentityTypeApiKeyAK {
		orgId, err = auth.GetOrgIdFromApiKeyRsp(ctx, authInfo.ApiKeyRsp)
		if err != nil {
			return nil, err
		}
	} else if authInfo.IdentityType == auth.AuthnIdentityTypeAccessToken {
		orgId = authInfo.AccessTokenRsp.AccessTokenIdentity.OrgId
	} else {
		return nil, errs.NewBadRequest("unsupported identity type", nil)
	}
	urlPath := fmt.Sprintf("/orgs/%d%s", orgId, c.Request.URL.Path)
	tilog.Info(ctx, "genUsersAccessParam4Gin parse request url", zap.Any("urlPath", urlPath))
	param := &auth.AccessParam{
		Metadata: map[string]interface{}{
			auth.AccessMetadataPath:   urlPath,
			auth.AccessMetadataMethod: c.Request.Method,
		},
		Req: c,
	}
	return param, nil
}

func getOrgIdFromAuthInfo(c *gin.Context) (uint64, error) {
	ctx := c.Request.Context()
	authInfo, err := auth.GetIdentityInfoFromGinContext(c)
	if err != nil {
		return 0, err
	}
	if authInfo.IdentityType == auth.AuthnIdentityTypeApiKeyAK {
		return auth.GetOrgIdFromApiKeyRsp(ctx, authInfo.ApiKeyRsp)
	}
	if authInfo.IdentityType == auth.AuthnIdentityTypeAccessToken {
		return authInfo.AccessTokenRsp.AccessTokenIdentity.OrgId, nil
	}
	return 0, errs.NewBadRequest("unsupported identity type", nil)
}

func getOperatorFromAuthInfo(c *gin.Context) (*comsvc.Operator, error) {
	ipFromReq := auditlogsvc.GetClientIpFromReq(c.Request)
	authInfo, err := auth.GetIdentityInfoFromGinContext(c)
	if err != nil {
		return nil, err
	}
	if authInfo.IdentityType == auth.AuthnIdentityTypeApiKeyAK {
		return &comsvc.Operator{
			OpTye: entity.RoleBindingTargetAPIKey,
			OpID:  authInfo.ApiKeyRsp.ApiKey.Id,
			OpVal: authInfo.ApiKeyRsp.ApiKey.AccessKey,
			OpIP:  ipFromReq,
		}, nil
	}
	if authInfo.IdentityType == auth.AuthnIdentityTypeAccessToken {
		return &comsvc.Operator{
			OpTye: entity.RoleBindingTargetUser,
			OpID:  authInfo.AccessTokenRsp.AccessTokenIdentity.UserId,
			OpVal: authInfo.AccessTokenRsp.AccessTokenIdentity.UserEmail,
			OpIP:  ipFromReq,
		}, nil
	}
	return nil, errs.NewBadRequest("unsupported identity type", nil)
}

func parseUserId(c *gin.Context) (uint64, error) {
	uriReq := OpenApiUserIdReq{}
	if err := c.ShouldBindUri(&uriReq); err != nil {
		return 0, err
	}
	userId, err := strconv.ParseUint(uriReq.UserId, 10, 64)
	if err != nil {
		return 0, err
	}
	return userId, nil
}

func validateScopeId(scopeId string, expectedOrgId uint64) error {
	id, err := strconv.ParseUint(scopeId, 10, 64)
	if err != nil {
		return fmt.Errorf("invalid scopeId: %s", scopeId)
	}
	if id != expectedOrgId {
		return fmt.Errorf("scopeId %s does not match orgId %d", scopeId, expectedOrgId)
	}
	return nil
}

func validateNoDuplicateScopeId(roles []OpenApiUserRole) error {
	seen := make(map[string]struct{}, len(roles))
	for _, r := range roles {
		if _, ok := seen[r.ScopeId]; ok {
			return fmt.Errorf("duplicate scopeId: %s", r.ScopeId)
		}
		seen[r.ScopeId] = struct{}{}
	}
	return nil
}

func toRoleMap(roles []OpenApiUserRole) map[uint64]string {
	m := make(map[uint64]string, len(roles))
	for _, r := range roles {
		id, err := strconv.ParseUint(r.ScopeId, 10, 64)
		if err != nil {
			continue
		}
		m[id] = r.RbacRole
	}
	return m
}

func toApiUsersFromListRet(items []oneconsole.ListUsersItem) []OpenApiUser {
	if items == nil {
		return []OpenApiUser{}
	}
	ret := make([]OpenApiUser, 0, len(items))
	for _, item := range items {
		ou := OpenApiUser{
			UserId:    fmt.Sprintf("%d", item.UserId),
			Email:     item.Email,
			FirstName: item.FirstName,
			LastName:  item.LastName,
			Status:    item.Status,
			InviteTime: func() string {
				if item.InviteTime.IsZero() {
					return ""
				}
				return item.InviteTime.Format("2006-01-02T15:04:05Z")
			}(),
			LastLoginTime: func() string {
				if item.LastLoginTime == nil {
					return ""
				}
				return item.LastLoginTime.Format("2006-01-02T15:04:05Z")
			}(),
		}
		if item.OrgRole != nil {
			ou.OrgRole = &OpenApiOrgRole{
				ScopeId:  fmt.Sprintf("%d", item.OrgRole.ScopeId),
				RbacRole: item.OrgRole.RbacRole,
			}
		}
		for _, pr := range item.ProjectRoles {
			ou.ProjectRoles = append(ou.ProjectRoles, OpenApiProjectRole{ScopeId: fmt.Sprintf("%d", pr.ScopeId), RbacRole: pr.RbacRole})
		}
		for _, cr := range item.ClusterRoles {
			ou.InstanceRoles = append(ou.InstanceRoles, OpenApiInstanceRole{ScopeId: fmt.Sprintf("%d", cr.ScopeId), RbacRole: cr.RbacRole})
		}
		ret = append(ret, ou)
	}
	return ret
}

// @Tags Member
// @Summary List members
// @Description List members in the organization with optional filters for email, roles, statuses, project and instance.
// @Schemes
// @Accept json
// @Produce json
// @Param pageSize query int false "Page size"
// @Param pageToken query string false "Page token for pagination"
// @Param email query string false "Filter by email"
// @Param roles query []string false "Filter by roles" collectionFormat(multi) enums(org:owner, org:member, org:billing_admin, org:billing_viewer, org:audit_admin, project:owner, project:dev, project:readonly, project:ctl_plane_viewer, cluster:admin, cluster:viewer)
// @Param statuses query []string false "Filter by statuses" collectionFormat(multi) enums(Active, Pending)
// @Param orderBy query string false "Order by field and direction, e.g. 'email desc'" enums(email asc, email desc, invite_time asc, invite_time desc, login_time asc, login_time desc)
// @Param projectId query string false "Filter by project ID"
// @Param instanceId query string false "Filter by instance ID"
// @Success 200 {object} OpenApiListUsersRsp
// @Failure 400 {object} OpenApiError
// @Failure 401 {object} OpenApiError
// @Failure 403 {object} OpenApiError
// @Failure 500 {object} OpenApiError
// @Router /members [GET]
func ListUsersOpenApiHandler(c *gin.Context) {
	ctx := c.Request.Context()
	bm := utils.Begin(ctx, "ListUsersOpenApiHandler")
	defer bm.OnExit(ctx)

	orgId, err := getOrgIdFromAuthInfo(c)
	if err != nil {
		writeError(ctx, c.Writer, err, http.StatusUnauthorized, authenticateErrorCode, "authenticate fail")
		return
	}

	req := OpenApiListUsersReq{}
	if err := c.ShouldBindQuery(&req); err != nil {
		writeError(ctx, c.Writer, err, http.StatusBadRequest, paramInvalidErrorCode, "param invalid")
		return
	}

	tilog.Info(ctx, "ListUsersOpenApiHandler", zap.Any("orgId", orgId), zap.Any("req", req))

	var projectId uint64
	if req.ProjectId != "" {
		pid, pErr := strconv.ParseUint(req.ProjectId, 10, 64)
		if pErr != nil {
			writeError(ctx, c.Writer, pErr, http.StatusBadRequest, paramInvalidErrorCode, "projectId invalid")
			return
		}
		projectId = pid
	}

	var instanceId uint64
	if req.InstanceId != "" {
		iid, pErr := strconv.ParseUint(req.InstanceId, 10, 64)
		if pErr != nil {
			writeError(ctx, c.Writer, pErr, http.StatusBadRequest, paramInvalidErrorCode, "instanceId invalid")
			return
		}
		instanceId = iid
	}

	if req.ProjectId != "" && req.InstanceId != "" {
		writeError(ctx, c.Writer, nil, http.StatusBadRequest, paramInvalidErrorCode, "Only one project ID and instance ID can be queried at a time")
		return
	}

	var sortBy, order string
	if req.OrderBy != "" {
		split := strings.Split(req.OrderBy, " ")
		if len(split) == 1 {
			sortBy = split[0]
		} else if len(split) == 2 {
			sortBy = split[0]
			order = split[1]
		}
	}

	scopeType := entity.RoleBindingScopeOrg
	scopeId := orgId
	if projectId != 0 {
		scopeType = entity.RoleBindingScopeProject
		scopeId = projectId
	}
	if instanceId != 0 {
		scopeType = entity.RoleBindingScopeTiDB
		scopeId = instanceId
	}

	svc := oneconsole.GetOneConsoleUserSvc()

	ret, err := svc.ListUsers(ctx, &oneconsole.ListUsersParam{
		OrgId:     orgId,
		ScopeType: scopeType,
		ScopeId:   scopeId,
		Email: func() string {
			if req.Email != "" {
				return strings.ReplaceAll(req.Email, "%40", "@")
			}
			return ""
		}(),
		Roles:     req.Roles,
		Statuses:  req.Statuses,
		SortBy:    sortBy,
		Order:     order,
		PageSize:  req.PageSize,
		PageToken: req.PageToken,
		Source:    oneconsole.ListUsersSourceOpenApi,
	})
	if err != nil {
		tilog.Error(ctx, "ListUsersOpenApiHandler error", zap.Any("err", err))
		doTiErr(ctx, c, err, "list users failed")
		return
	}

	rsp := OpenApiListUsersRsp{
		Users:         toApiUsersFromListRet(ret.Users),
		NextPageToken: ret.NextPageToken,
		TotalSize:     ret.TotalSize,
	}
	openApiWriteResult(c.Writer, rsp)
}

// POST /v1beta1/users
// @Tags Member
// @Summary Invite members
// @Description Invite one or more members to the organization with specified org, project and instance roles.
// @Schemes
// @Accept json
// @Produce json
// @Param body body OpenApiInviteUsersReq true "invite members request"
// @Success 200 {object} OpenApiInviteUsersRsp
// @Failure 400 {object} OpenApiError
// @Failure 401 {object} OpenApiError
// @Failure 403 {object} OpenApiError
// @Failure 500 {object} OpenApiError
// @Router /members [POST]
func InviteUsersOpenApiHandler(c *gin.Context) {
	ctx := c.Request.Context()
	bm := utils.Begin(ctx, "InviteUsersOpenApiHandler")
	defer bm.OnExit(ctx)

	orgId, err := getOrgIdFromAuthInfo(c)
	if err != nil {
		writeError(ctx, c.Writer, err, http.StatusUnauthorized, authenticateErrorCode, "authenticate fail")
		return
	}

	req := OpenApiInviteUsersReq{}
	if err := c.ShouldBindJSON(&req); err != nil {
		writeError(ctx, c.Writer, err, http.StatusBadRequest, paramInvalidErrorCode, "param invalid")
		return
	}
	if len(req.Emails) == 0 {
		writeError(ctx, c.Writer, fmt.Errorf("emails is empty"), http.StatusBadRequest, paramInvalidErrorCode, "emails is required")
		return
	}

	tilog.Info(ctx, "InviteUsersOpenApiHandler", zap.Any("orgId", orgId), zap.Any("emails", req.Emails))

	operator, err := getOperatorFromAuthInfo(c)
	if err != nil {
		writeError(ctx, c.Writer, err, http.StatusUnauthorized, authenticateErrorCode, "authenticate fail")
		return
	}

	if req.OrgRole == nil || req.OrgRole.RbacRole == "" {
		writeError(ctx, c.Writer, fmt.Errorf("orgRole is required"), http.StatusBadRequest, paramInvalidErrorCode, "orgRole is required")
		return
	}
	if req.OrgRole.ScopeId != "" {
		if pErr := validateScopeId(req.OrgRole.ScopeId, orgId); pErr != nil {
			writeError(ctx, c.Writer, pErr, http.StatusBadRequest, paramInvalidErrorCode, pErr.Error())
			return
		}
	}
	orgRole := req.OrgRole.RbacRole

	if err := validateNoDuplicateScopeId(convertProjectRoles(req.ProjectRoles)); err != nil {
		writeError(ctx, c.Writer, err, http.StatusBadRequest, paramInvalidErrorCode, err.Error())
		return
	}
	if err := validateNoDuplicateScopeId(convertInstanceRoles(req.InstanceRoles)); err != nil {
		writeError(ctx, c.Writer, err, http.StatusBadRequest, paramInvalidErrorCode, err.Error())
		return
	}

	svc := oneconsole.GetOneConsoleUserSvc()
	rets, err := svc.InviteUsersToOrg(ctx, &oneconsole.InviteUsersParam{
		EmailList: req.Emails,
		OrgID:     orgId,
		ORole:     orgRole,
		PRoles:    toRoleMap(convertProjectRoles(req.ProjectRoles)),
		CRoles:    toRoleMap(convertInstanceRoles(req.InstanceRoles)),
	}, operator)
	if err != nil {
		doTiErr(ctx, c, err, "invite users failed")
		return
	}

	var results []OpenApiInviteUserResult
	allSuccess := true
	for _, r := range rets {
		if r.Code != entity.InviteUserSuccess {
			allSuccess = false
		}
		results = append(results, OpenApiInviteUserResult{
			UserId: fmt.Sprintf("%d", r.UserID),
			Email:  r.Email,
		})
	}

	msg := "SUCCESS"
	if !allSuccess {
		msg = "PARTIAL_SUCCESS"
	}

	rsp := OpenApiInviteUsersRsp{Users: results, Success: allSuccess, Message: msg}
	openApiWriteResult(c.Writer, rsp)
}

// PATCH /v1beta1/users/:user_id
// @Tags Member
// @Summary Update a member
// @Description Update a member's org role, project roles and instance roles. Project roles and instance roles are fully replaced when provided.
// @Schemes
// @Accept json
// @Produce json
// @Param user_id path string true "User ID"
// @Param body body openApiUpdateUserReqDoc true "update member request"
// @Success 200
// @Failure 400 {object} OpenApiError
// @Failure 401 {object} OpenApiError
// @Failure 403 {object} OpenApiError
// @Failure 500 {object} OpenApiError
// @Router /members/{user_id} [PATCH]
func UpdateUserOpenApiHandler(c *gin.Context) {
	ctx := c.Request.Context()
	bm := utils.Begin(ctx, "UpdateUserOpenApiHandler")
	defer bm.OnExit(ctx)

	orgId, err := getOrgIdFromAuthInfo(c)
	if err != nil {
		writeError(ctx, c.Writer, err, http.StatusUnauthorized, authenticateErrorCode, "authenticate fail")
		return
	}

	userId, err := parseUserId(c)
	if err != nil {
		writeError(ctx, c.Writer, err, http.StatusBadRequest, paramInvalidErrorCode, "user_id invalid")
		return
	}

	req := OpenApiUpdateUserReq{}
	if err := c.ShouldBindJSON(&req); err != nil {
		writeError(ctx, c.Writer, err, http.StatusBadRequest, paramInvalidErrorCode, "param invalid")
		return
	}

	tilog.Info(ctx, "UpdateUserOpenApiHandler", zap.Any("orgId", orgId), zap.Any("userId", userId))

	operator, err := getOperatorFromAuthInfo(c)
	if err != nil {
		writeError(ctx, c.Writer, err, http.StatusUnauthorized, authenticateErrorCode, "authenticate fail")
		return
	}

	var oRole *string
	if req.OrgRole != nil && req.OrgRole.RbacRole != "" {
		if req.OrgRole.ScopeId != "" {
			if pErr := validateScopeId(req.OrgRole.ScopeId, orgId); pErr != nil {
				writeError(ctx, c.Writer, pErr, http.StatusBadRequest, paramInvalidErrorCode, pErr.Error())
				return
			}
		}
		oRole = &req.OrgRole.RbacRole
	}

	var pRoles, cRoles map[uint64]string = nil, nil
	if req.ProjectRoles != nil && req.ProjectRoles.IsSet {
		if err := validateNoDuplicateScopeId(req.ProjectRoles.Value); err != nil {
			writeError(ctx, c.Writer, err, http.StatusBadRequest, paramInvalidErrorCode, err.Error())
			return
		}
		pRoles = toRoleMap(req.ProjectRoles.Value)
	}
	if req.InstanceRoles != nil && req.InstanceRoles.IsSet {
		if err := validateNoDuplicateScopeId(req.InstanceRoles.Value); err != nil {
			writeError(ctx, c.Writer, err, http.StatusBadRequest, paramInvalidErrorCode, err.Error())
			return
		}
		cRoles = toRoleMap(req.InstanceRoles.Value)

	}

	svc := oneconsole.GetOneConsoleUserSvc()
	err = svc.UpdateUserRoles(ctx, &oneconsole.UpdateRolesParam{
		UserID: userId,
		OrgID:  orgId,
		ORole:  oRole,
		PRoles: pRoles,
		CRoles: cRoles,
	}, operator)
	if err != nil {
		doTiErr(ctx, c, err, "update user failed")
		return
	}

	openApiWriteResult(c.Writer, struct{}{})
}

// POST /v1beta1/users:batchUpdate
// @Tags Member
// @Summary Batch update members
// @Description Batch update multiple members' org, project and instance roles in a single request (max 100 users per request).
// @Schemes
// @Accept json
// @Produce json
// @Param body body openApiBatchUpdateUsersReqDoc true "batch update members request"
// @Success 200 {object} OpenApiBatchUpdateUsersRsp
// @Failure 400 {object} OpenApiError
// @Failure 401 {object} OpenApiError
// @Failure 403 {object} OpenApiError
// @Failure 500 {object} OpenApiError
// @Router /members/batchUpdate [POST]
func BatchUpdateUsersOpenApiHandler(c *gin.Context) {
	ctx := c.Request.Context()
	bm := utils.Begin(ctx, "BatchUpdateUsersOpenApiHandler")
	defer bm.OnExit(ctx)

	orgId, err := getOrgIdFromAuthInfo(c)
	if err != nil {
		writeError(ctx, c.Writer, err, http.StatusUnauthorized, authenticateErrorCode, "authenticate fail")
		return
	}

	req := OpenApiBatchUpdateUsersReq{}
	if err := c.ShouldBindJSON(&req); err != nil {
		writeError(ctx, c.Writer, err, http.StatusBadRequest, paramInvalidErrorCode, "param invalid")
		return
	}
	if len(req.Users) > 100 {
		writeError(ctx, c.Writer, fmt.Errorf("max 100 users per request"), http.StatusBadRequest, paramInvalidErrorCode, "max 100 users per request")
		return
	}

	tilog.Info(ctx, "BatchUpdateUsersOpenApiHandler", zap.Any("orgId", orgId), zap.Int("count", len(req.Users)))

	operator, err := getOperatorFromAuthInfo(c)
	if err != nil {
		writeError(ctx, c.Writer, err, http.StatusUnauthorized, authenticateErrorCode, "authenticate fail")
		return
	}

	svc := oneconsole.GetOneConsoleUserSvc()
	var results []OpenApiBatchUpdateUserResult
	for _, entry := range req.Users {
		userId, pErr := strconv.ParseUint(entry.UserId, 10, 64)
		if pErr != nil {
			results = append(results, OpenApiBatchUpdateUserResult{
				UserId:  entry.UserId,
				Success: false,
				Error:   &OpenApiBatchUpdateError{Code: paramInvalidErrorCode, Message: "invalid user_id"},
			})
			continue
		}

		var oRole *string
		if entry.OrgRole != nil && entry.OrgRole.RbacRole != "" {
			if entry.OrgRole.ScopeId != "" {
				if pErr := validateScopeId(entry.OrgRole.ScopeId, orgId); pErr != nil {
					results = append(results, OpenApiBatchUpdateUserResult{
						UserId:  entry.UserId,
						Success: false,
						Error:   &OpenApiBatchUpdateError{Code: paramInvalidErrorCode, Message: pErr.Error()},
					})
					continue
				}
			}
			oRole = &entry.OrgRole.RbacRole
		}
		var pRoles, cRoles map[uint64]string = nil, nil
		if entry.ProjectRoles != nil && entry.ProjectRoles.IsSet {
			if err := validateNoDuplicateScopeId(entry.ProjectRoles.Value); err != nil {
				writeError(ctx, c.Writer, err, http.StatusBadRequest, paramInvalidErrorCode, err.Error())
				return
			}
			pRoles = toRoleMap(entry.ProjectRoles.Value)
		}
		if entry.InstanceRoles != nil && entry.InstanceRoles.IsSet {
			if err := validateNoDuplicateScopeId(entry.InstanceRoles.Value); err != nil {
				writeError(ctx, c.Writer, err, http.StatusBadRequest, paramInvalidErrorCode, err.Error())
				return
			}
			cRoles = toRoleMap(entry.InstanceRoles.Value)
		}
		uErr := svc.UpdateUserRoles(ctx, &oneconsole.UpdateRolesParam{
			UserID: userId,
			OrgID:  orgId,
			ORole:  oRole,
			PRoles: pRoles,
			CRoles: cRoles,
		}, operator)
		if uErr != nil {
			code := internalServerErrorCode
			if te, ok := errs.AsTiError(uErr); ok {
				code = errorCodeByHttpStatus(te.HTTPCode())
			}
			results = append(results, OpenApiBatchUpdateUserResult{
				UserId:  entry.UserId,
				Success: false,
				Error:   &OpenApiBatchUpdateError{Code: code, Message: uErr.Error()},
			})
			continue
		}

		results = append(results, OpenApiBatchUpdateUserResult{UserId: entry.UserId, Success: true})
	}

	rsp := OpenApiBatchUpdateUsersRsp{Results: results}
	openApiWriteResult(c.Writer, rsp)
}

// POST /v1beta1/users/:user_id:resendInvitation
// @Tags Member
// @Summary Resend invitation
// @Description Resend invitation email to a member who has not yet accepted the invitation.
// @Schemes
// @Accept json
// @Produce json
// @Param user_id path string true "User ID"
// @Success 200
// @Failure 400 {object} OpenApiError
// @Failure 401 {object} OpenApiError
// @Failure 403 {object} OpenApiError
// @Failure 500 {object} OpenApiError
// @Router /members/{user_id}/resendInvitation [POST]
func ResendInvitationOpenApiHandler(c *gin.Context) {
	ctx := c.Request.Context()
	bm := utils.Begin(ctx, "ResendInvitationOpenApiHandler")
	defer bm.OnExit(ctx)

	orgId, err := getOrgIdFromAuthInfo(c)
	if err != nil {
		writeError(ctx, c.Writer, err, http.StatusUnauthorized, authenticateErrorCode, "authenticate fail")
		return
	}

	userId, err := parseUserId(c)
	if err != nil {
		writeError(ctx, c.Writer, err, http.StatusBadRequest, paramInvalidErrorCode, "user_id invalid")
		return
	}

	tilog.Info(ctx, "ResendInvitationOpenApiHandler", zap.Any("orgId", orgId), zap.Any("userId", userId))

	operator, err := getOperatorFromAuthInfo(c)
	if err != nil {
		writeError(ctx, c.Writer, err, http.StatusUnauthorized, authenticateErrorCode, "authenticate fail")
		return
	}

	svc := oneconsole.GetOneConsoleUserSvc()
	err = svc.SendActivationEmail(ctx, userId, orgId, operator)
	if err != nil {
		doTiErr(ctx, c, err, "resend invitation failed")
		return
	}

	openApiWriteResult(c.Writer, struct{}{})
}

// DELETE /v1beta1/users/:user_id
// @Tags Member
// @Summary Delete a member
// @Description Remove a member from the organization. The member will lose all roles within the org.
// @Schemes
// @Accept json
// @Produce json
// @Param user_id path string true "User ID"
// @Success 200
// @Failure 400 {object} OpenApiError
// @Failure 401 {object} OpenApiError
// @Failure 403 {object} OpenApiError
// @Failure 500 {object} OpenApiError
// @Router /members/{user_id} [DELETE]
func DeleteUserOpenApiHandler(c *gin.Context) {
	ctx := c.Request.Context()
	bm := utils.Begin(ctx, "DeleteUserOpenApiHandler")
	defer bm.OnExit(ctx)

	orgId, err := getOrgIdFromAuthInfo(c)
	if err != nil {
		writeError(ctx, c.Writer, err, http.StatusUnauthorized, authenticateErrorCode, "authenticate fail")
		return
	}

	userId, err := parseUserId(c)
	if err != nil {
		writeError(ctx, c.Writer, err, http.StatusBadRequest, paramInvalidErrorCode, "user_id invalid")
		return
	}

	tilog.Info(ctx, "DeleteUserOpenApiHandler", zap.Any("orgId", orgId), zap.Any("userId", userId))

	userSvc, _ := user.GetUserSvc()
	err = userSvc.RemoveUserFromOrg(ctx, userId, 0, orgId)
	if err != nil {
		doTiErr(ctx, c, err, "delete user failed")
		return
	}

	openApiWriteResult(c.Writer, struct{}{})
}

func usersOpenApiPermissionCheckResultHandleFunc(w http.ResponseWriter, r *http.Request, result *auth.AccessResult, err error) (continueRun bool) {
	tilog.Info(r.Context(), "access check result", zap.Any("result", result), zap.Any("err", err))
	if err != nil {
		writeError(r.Context(), w, err, http.StatusForbidden, authorizeErrorCode, "access denied")
		return false

	}
	if result.Code != auth.AccessRetRelease {
		writeError(r.Context(), w, err, http.StatusForbidden, authorizeErrorCode, "access denied")
		return false
	}
	return true
}
