package group_r

import (
	"encoding/json"
	"git.hilo.cn/hilo-common/domain"
	"git.hilo.cn/hilo-common/mycontext"
	"git.hilo.cn/hilo-common/mylogrus"
	"git.hilo.cn/hilo-common/resource/config"
	"git.hilo.cn/hilo-common/resource/mysql"
	"git.hilo.cn/hilo-common/resource/redisCli"
	"git.hilo.cn/hilo-common/sdk/agora"
	"git.hilo.cn/hilo-common/sdk/aws"
	"git.hilo.cn/hilo-common/sdk/tencentyun"
	"git.hilo.cn/hilo-common/utils"
	"github.com/gin-gonic/gin"
	uuid "github.com/satori/go.uuid"
	"github.com/spf13/cast"
	"gorm.io/gorm"
	"hilo-group/_const/enum/diamond_e"
	"hilo-group/_const/enum/group_e"
	"hilo-group/_const/enum/msg_e"
	"hilo-group/_const/enum/online_e"
	"hilo-group/_const/enum/user_e"
	"hilo-group/_const/redis_key/group_k"
	"hilo-group/cv/billboard_cv"
	"hilo-group/cv/gift_cv"
	"hilo-group/cv/group_cv"
	"hilo-group/cv/user_cv"
	"hilo-group/domain/cache/game_c"
	"hilo-group/domain/cache/group_c"
	"hilo-group/domain/cache/res_c"
	"hilo-group/domain/cache/room_c"
	"hilo-group/domain/cache/tim_c"
	"hilo-group/domain/cache/user_c"
	"hilo-group/domain/model/diamond_m"
	"hilo-group/domain/model/game_m"
	"hilo-group/domain/model/group_m"
	"hilo-group/domain/model/noble_m"
	"hilo-group/domain/model/res_m"
	"hilo-group/domain/model/tim_m"
	"hilo-group/domain/model/user_m"
	"hilo-group/domain/service/group_s"
	"hilo-group/domain/service/signal_s"
	"hilo-group/myerr"
	"hilo-group/myerr/bizerr"
	"hilo-group/req"
	"hilo-group/resp"
	"math"
	"runtime/debug"
	"sort"
	"strconv"
	"strings"
	"time"
)

type CreateGroupRsp struct {
	ImGroupId string `json:"imGroupId"`
	Code      string `json:"code"`
}

// @Tags 群组
// @Summary 建群
// @Accept application/x-www-form-urlencoded
// @Param token header string true "token"
// @Param nonce header string true "随机数字"
// @Param name formData string false "群名"
// @Param introduction formData string false "群简介"
// @Param notification formData string false "群公告"
// @Param faceUrl formData string false "群头像 URL"
// @Success 200 {object} CreateGroupRsp
// @Router /v1/imGroup/group [post]
func CreateGroup(c *gin.Context) (*mycontext.MyContext, error) {
	myContext := mycontext.CreateMyContext(c.Keys)

	userId, err := req.GetUserId(c)
	if err != nil {
		return myContext, err
	}
	if lock := redisCli.Lock(group_k.GetGroupLockKey(userId), time.Second*5); !lock {
		return myContext, bizerr.ReqTooFrequent
	}

	model := domain.CreateModelContext(myContext)

	user, err := user_m.GetUser(model, userId)
	if err != nil {
		return myContext, err
	}

	// Kludge: 不支持的语言,强行修正为英语,以保证建群成功
	if c, err := res_m.GetCountryByLanguage(model, user.Language); err != nil || c == nil {
		user.Language = utils.DEFAULT_LANG
	}

	// 群组的头像默认为用户头像,群组的名称默认为用户昵称,群简介默认为“s%的群组”,群公告默认为“欢迎来到Hilo”
	name := c.PostForm("name")
	if len(name) <= 0 {
		text := res_m.ResMultiText{MsgId: msg_e.MSG_ID_GROUP_NAME, Language: user.Language}
		if text.Get(model.Db) == nil {
			name = strings.ReplaceAll(text.Content, "{nick}", user.Nick)
		}
	}

	introduction := c.PostForm("introduction")
	if len(introduction) <= 0 {
		text := res_m.ResMultiText{MsgId: msg_e.MSG_ID_GROUP_INTRODUCTION, Language: user.Language}
		if text.Get(model.Db) == nil {
			introduction = strings.ReplaceAll(text.Content, "{nick}", user.Nick)
		}
	}
	notification := c.PostForm("notification")
	if len(notification) <= 0 {
		text := res_m.ResMultiText{MsgId: msg_e.MSG_ID_GROUP_NOTIFICATION, Language: user.Language}
		if text.Get(model.Db) == nil {
			notification = strings.ReplaceAll(text.Content, "{nick}", user.Nick)
		}
	}
	faceUrl := c.PostForm("faceUrl")
	if len(faceUrl) <= 0 {
		faceUrl = user.Avatar
	}

	myGroups, err := group_m.FindGroupByOwner(model, userId)
	if err != nil {
		return nil, err
	}
	// Kludge: 对超级号放开建群限制
	if !user_m.IsSuperUser(userId) && len(myGroups) >= group_e.GROUP_CREATE_LIMIT {
		return myContext, bizerr.GroupCreateLimitReached
	}

	code := group_m.GenerateGroupCode(group_e.GROUP_DEFAULT_CODE_LENGTH)
	i := 0
	for ; i < group_e.CREATE_GROUP_MAX_ATTEMPT; i++ {
		success, err := res_m.CheckCode(model, code)
		if err != nil || !success {
			break
		}
		r, err := group_m.FindGroupByCode(model, code)
		if err == nil && r == nil {
			break
		}
		code = group_m.GenerateGroupCode(group_e.GROUP_DEFAULT_CODE_LENGTH)
	}

	if i >= group_e.CREATE_GROUP_MAX_ATTEMPT {
		return myContext, myerr.NewSysError("Failed in generating groupId")
	}

	groupId := group_e.OverseaGroupNamePrefix + code
	groupId, err = tencentyun.CreateGroup(name, groupId)
	if err != nil {
		return myContext, err
	}

	channelId := getUUID()
	_, _, err = agora.CreateGroupAgora(channelId, uint32(userId))

	if err != nil {
		// 回滚,删除刚刚建立的TX群组
		tencentyun.DestroyGroup(groupId, false)
		return myContext, err
	}

	roomType := group_e.OverseaRoom
	g := group_m.GroupInfo{
		ImGroupId:      groupId,
		TxGroupId:      groupId,
		Type:           uint16(roomType),
		Code:           code,
		OriginCode:     code,
		Owner:          userId,
		Name:           name,
		Introduction:   introduction,
		Notification:   notification,
		FaceUrl:        faceUrl,
		Country:        user.Country,
		ChannelId:      channelId,
		MicOn:          true,
		LoadHistory:    false,
		MicNumType:     group_e.TenMicNumType,
		TouristMic:     1,
		TouristSendMsg: 1,
		TouristSendPic: 1,
	}
	if err := group_s.NewGroupService(myContext).CreateGroup(userId, &g); err != nil {
		// 回滚,删除刚刚建立的TX群组
		tencentyun.DestroyGroup(groupId, false)
		return myContext, err
	}

	gm := group_m.GroupMember{
		GroupId: groupId,
		UserId:  userId,
	}
	if err = gm.Create(model.Db); err != nil {
		model.Log.Warnf("Failed to set GroupMember +%v", gm)
	}

	resp.ResponseOk(c, CreateGroupRsp{ImGroupId: groupId, Code: code})
	return myContext, nil
}

func getUUID() string {
	return strings.Replace(uuid.NewV4().String(), "-", "", -1)
}

// @Tags 群组
// @Summary 撤群
// @Accept application/x-www-form-urlencoded
// @Param token header string true "token"
// @Param nonce header string true "随机数字"
// @Param groupId path string true "群ID"
// @Success 200
// @Router /v1/imGroup/group/{groupId} [delete]
func DestroyGroup(c *gin.Context) (*mycontext.MyContext, error) {
	myContext := mycontext.CreateMyContext(c.Keys)

	groupId := c.Param("groupId")
	if len(groupId) <= 0 {
		return myContext, myerr.NewSysError("groupId 为必填项")
	}

	userId, _, err := req.GetUserIdAndExtId(c, myContext)
	if err != nil {
		return myContext, err
	}

	if !user_m.IsSuperUser(userId) {
		return myContext, bizerr.NoPrivileges
	}

	// 参数已经是TxGroupId,不需要再转换了
	err = tencentyun.DestroyGroup(groupId, false)
	if err != nil {
		return myContext, err
	}

	resp.ResponseOk(c, groupId)
	return myContext, nil
}

// @Tags 群组
// @Summary 查询群信息(运营用)
// @Accept application/x-www-form-urlencoded
// @Param token header string true "token"
// @Param nonce header string true "随机数字"
// @Param code path string true "群CODE"
// @Success 200 {object} group_cv.GroupInfo
// @Router /v1/imGroup/group/{code} [get]
func GetGroupInfo(c *gin.Context) (*mycontext.MyContext, error) {
	myContext := mycontext.CreateMyContext(c.Keys)

	code := c.Param("code")
	if len(code) <= 0 {
		return myContext, myerr.NewSysError("code 为必填项")
	}

	model := domain.CreateModelContext(myContext)

	r, err := group_m.FindGroupByCode(model, code)
	if r == nil {
		return myContext, bizerr.GroupNotFound
	}

	groupInfo, err := tencentyun.GetGroupInfo(model, []string{r.ImGroupId}, true)
	if err != nil {
		return myContext, err
	}
	if len(groupInfo) < 1 {
		return myContext, myerr.NewSysError("ImGroupId does not exists.")
	}
	info := &groupInfo[0]

	countryInfo, err := res_c.GetCountryIconMap(model)
	if err != nil {
		return myContext, err
	}

	result := group_cv.GroupInfo{
		GroupBasicInfo: group_cv.GroupBasicInfo{
			GroupId:         r.TxGroupId,
			Name:            r.Name,
			Introduction:    r.Introduction,
			Notification:    r.Notification,
			FaceUrl:         r.FaceUrl,
			Owner_Account:   info.Owner_Account,
			CreateTime:      info.CreateTime,
			LastMsgTime:     info.LastMsgTime,
			NextMsgSeq:      info.NextMsgSeq,
			MemberNum:       info.MemberNum,
			MaxMemberNum:    info.MaxMemberNum,
			ShutUpAllMember: info.ShutUpAllMember,
			Code:            r.Code,
			CountryIcon:     countryInfo[r.Country],
			MicNumType:      int(r.MicNumType),
		},
	}

	for _, i := range info.MemberList {
		result.MemberList = append(result.MemberList, group_cv.MemberListInfo{
			Member_Account:  i.Member_Account,
			JoinTime:        i.JoinTime,
			LastSendMsgTime: i.LastSendMsgTime,
		})
	}

	resp.ResponseOk(c, result)
	return myContext, nil
}

// @Tags 群组
// @Summary 查询群详细信息
// @Accept application/x-www-form-urlencoded
// @Param token header string true "token"
// @Param nonce header string true "随机数字"
// @Param groupId path string true "群ID"
// @Success 200 {object} group_cv.GroupDetail
// @Router /v1/imGroup/detail/{groupId} [get]
func GetGroupDetail(c *gin.Context) (*mycontext.MyContext, error) {
	myContext := mycontext.CreateMyContext(c.Keys)

	groupId := c.Param("groupId")
	if len(groupId) <= 0 {
		return myContext, myerr.NewSysError("groupId 为必填项")
	}

	myUserId, err := req.GetUserId(c)
	if err != nil {
		return myContext, err
	}

	model := domain.CreateModelContext(myContext)

	groupInfo, err := group_m.GetInfoByTxGroupId(model, groupId)
	if err != nil {
		return myContext, err
	}
	if groupInfo == nil {
		return myContext, bizerr.GroupNotFound
	}
	groupId = groupInfo.ImGroupId

	memberCount, err := group_m.GetMemberCount(model.Db, groupId)
	if err != nil {
		return myContext, err
	}

	countryInfo, err := res_c.GetCountryIconMap(model)
	if err != nil {
		return myContext, err
	}

	themeUrl := ""
	var themeId uint64 = 0
	var themeType uint8 = 1
	if groupInfo.ThemeId > 0 {
		//官方主题
		// 找不到的,默认为空(可能被后台删了)
		themeType = 1
		themes, _ := res_m.GroupThemeGetAll(model.Db)
		for _, i := range themes {
			if i.ID == uint64(groupInfo.ThemeId) {
				themeUrl = i.Url
				themeId = i.ID
				break
			}
		}
	} else {
		//可能是自定义主题
		themeId, themeUrl, err = group_m.GetShowCustomTheme(model, groupInfo.ImGroupId)
		if err != nil {
			return myContext, err
		}
		if themeId != 0 {
			themeType = 2
		}
	}

	result := group_cv.GroupDetail{
		GroupBasicInfo: group_cv.GroupBasicInfo{
			GroupId:        groupInfo.TxGroupId,
			Name:           groupInfo.Name,
			Introduction:   groupInfo.Introduction,
			Notification:   groupInfo.Notification,
			FaceUrl:        groupInfo.FaceUrl,
			MemberNum:      memberCount,
			Code:           groupInfo.Code,
			CountryIcon:    countryInfo[groupInfo.Country],
			MicNumType:     int(groupInfo.MicNumType),
			TouristMic:     groupInfo.TouristMic,
			TouristSendMsg: groupInfo.TouristSendMsg,
			TouristSendPic: groupInfo.TouristSendPic,
			MemberFee:      groupInfo.MemberFee,
		},
		MicOn:       groupInfo.MicOn,
		LoadHistory: groupInfo.LoadHistory,
		ThemeId:     themeId,
		ThemeUrl:    themeUrl,
		ThemeType:   themeType,
		Role:        group_e.GROUP_VISITOR,
	}

	result.TotalConsume, err = gift_cv.GetGroupConsumeTotal(model, groupId)
	if err != nil {
		return myContext, err
	}

	model.Log.Infof("GetGroupDetail, before GetGroupTop3Consume: +%v", result)
	result.TopConsumers, err = billboard_cv.GetGroupTop3Consume(model, groupId, myUserId)
	if err != nil {
		return myContext, err
	}
	model.Log.Infof("GetGroupDetail, after GetGroupTop3Consume: +%v", result)

	result.WelcomeText, _, _, err = group_s.NewGroupService(myContext).GetWelcomeText(groupInfo)
	if err != nil {
		return myContext, err
	}
	result.WelcomeText = strings.ReplaceAll(result.WelcomeText, "@%s", "")

	result.DiceNum = group_e.GROUP_DICE_NUM_DEFAULT
	result.DiceType = 1
	gs := group_m.GroupSetting{GroupId: groupId}
	err = gs.Get(model.Db)
	if err == nil {
		result.DiceNum = gs.DiceNum
		result.DiceType = gs.DiceType
	} else if err != gorm.ErrRecordNotFound {
		return myContext, err
	}

	roles, _, err := group_m.GetRolesInGroup(model, groupInfo.ImGroupId)
	if err != nil {
		return myContext, err
	}

	userId, err := req.GetUserId(c)
	if err != nil {
		return myContext, err
	}

	isGroupMember, err := group_m.IsGroupMember(model.Db, groupId, userId)
	if err != nil {
		return myContext, err
	}
	if isGroupMember {
		result.Role = group_e.GROUP_MEMBER
	}

	groupUser := group_m.GroupUser{Model: model, UserId: userId}
	msgStatus, err := groupUser.Get()
	if err != nil {
		return myContext, err
	}

	// 默认为免打扰
	ok := false
	if result.MsgStatus, ok = msgStatus[groupId]; !ok {
		//result.MsgStatus = group_enum.DoNotDisturbMsgStatusGroupUser
		result.MsgStatus = group_e.NormalMsgStatusGroupUser
	}

	emptyStr := ""
	if len(groupInfo.Password) > 0 {
		// 代表有密码
		result.Password = &emptyStr
	}

	userIds := make([]uint64, 0)
	for u, r := range roles {
		userIds = append(userIds, u)

		if u == userId {
			if r == group_e.GROUP_OWNER || r == group_e.GROUP_MANAGER {
				// 如果用户是OW或经理,可以看到密码,同时设置角色
				result.Role = r

				if len(groupInfo.Password) > 0 {
					result.Password = &groupInfo.Password
				}
			} else if r == group_e.GROUP_ADMIN {
				// 如果用户是管理员,仅设置角色
				result.Role = r
			}
		}
	}
	userIds = append(userIds, groupInfo.Owner)
	users, err := user_cv.GetUserTinyMap(userIds)
	if err != nil {
		return myContext, err
	}
	result.Owner, err = user_cv.GetUserDetail(model, groupInfo.Owner, userId)
	if err != nil {
		return myContext, err
	}

	vips, err := user_m.BatchGetVips(userIds)
	if err != nil {
		return myContext, myerr.WrapErr(err)
	}

	//extIds := make([]string, 0)
	//for _, i := range users {
	//	extIds = append(extIds, i.ExternalId)
	//}
	/* fixme:为了性能,直接不查IM在线状态
	   status, err := tim_m.GetOnlineStatus(model, extIds)
	   if err != nil {
	   	return myContext, err
	   }
	*/
	superManagerMap, err := user_m.GetSuperManagerMap(userIds)
	if err != nil {
		return myContext, err
	}

	status := make(map[string]uint, 0)

	for u, _ := range users {
		m := users[u]
		result.RoleMembers = append(result.RoleMembers, group_cv.RoleMemberInfo{
			CvUserBase: user_cv.CvUserBase{
				Avatar:          &m.Avatar,
				ExternalId:      &m.ExternalId,
				Nick:            &m.Nick,
				Sex:             &m.Sex,
				Country:         &m.CountryIcon,
				CountryIcon:     &m.CountryIcon,
				Code:            &m.Code,
				IsPrettyCode:    m.IsPrettyCode,
				IsVip:           vips[m.Id] != nil,
				IsOfficialStaff: superManagerMap[m.Id],
				VipExpireTime:   vips[m.Id],
			},
			Role:         roles[u],
			OnlineStatus: status[m.ExternalId],
		})
	}

	sort.SliceStable(result.RoleMembers, func(i, j int) bool {
		if result.RoleMembers[i].Role > result.RoleMembers[j].Role {
			return true
		} else if result.RoleMembers[i].Role == result.RoleMembers[j].Role {
			if result.RoleMembers[i].OnlineStatus > result.RoleMembers[j].OnlineStatus {
				return true
			} else if result.RoleMembers[i].OnlineStatus == result.RoleMembers[j].OnlineStatus && *result.RoleMembers[i].Code > *result.RoleMembers[j].Code {
				return true
			}
		}
		return false
	})

	// 截取前N个
	endPos := group_e.GROUP_ROLE_VIEW_LIMIT
	if endPos > len(result.RoleMembers) {
		endPos = len(result.RoleMembers)
	}
	result.RoleMembers = result.RoleMembers[0:endPos]

	// todo: 直接用全量接口,只为方便
	supportLevels, err := group_s.NewGroupService(myContext).GetWeekMaxSupportLevelMap()
	if err != nil {
		return myContext, err
	}

	result.SupportLevel = supportLevels[groupId]

	resp.ResponseOk(c, result)
	return myContext, nil
}

// @Tags 群组
// @Summary 修改群信息
// @Accept application/x-www-form-urlencoded
// @Param token header string true "token"
// @Param nonce header string true "随机数字"
// @Param groupId path string true "群ID"
// @Param name formData string false "群名"
// @Param introduction formData string false "群简介"
// @Param notification formData string false "群公告"
// @Param faceUrl formData string false "群头像 URL"
// @Param password formData string false "入群密码"
// @Param micOn formData bool false "是否打开麦位"
// @Param loadHistory formData bool false "入群后是否加载历史"
// @Param themeId formData int false "群组背景ID"
// @Param welcomeText formData string false "入群欢迎语"
// @Param diceNum formData uint16 false "骰子数量"
// @Param diceType formData int false "骰子类型 1:数字0-9 2:数字1-6"
// @Param touristMic formData int false "游客是否能上麦(1是2否,不传则不改变)"
// @Param touristSendMsg formData int false "游客是否是否能发言(1是2否,不传则不改变)"
// @Param touristSendPic formData int false "游客是否能发图片(1是2否,不传则不改变)"
// @Param isChangeFee formData int false "是否更改加入群组需要的黄钻数,0否1是(兼容旧版本)"
// @Param memberFee formData int false "加入群组需要的黄钻数"
// @Success 200
// @Router /v1/imGroup/group/{groupId} [put]
func ModifyGroupInfo(c *gin.Context) (*mycontext.MyContext, error) {
	myContext := mycontext.CreateMyContext(c.Keys)

	imGroupId := c.Param("groupId")
	if len(imGroupId) <= 0 {
		return myContext, myerr.NewSysError("groupId为必填项")
	}

	userId, externalId, err := req.GetUserIdAndExtId(c, myContext)
	if err != nil {
		return myContext, err
	}

	model := domain.CreateModelContext(myContext)

	imGroupId, err = group_m.ToImGroupId(model, imGroupId)
	if err != nil {
		return myContext, err
	}

	// 判断有没有权限修改资料
	role, err := group_m.GetRoleInGroup(model, userId, imGroupId)
	if err != nil {
		return myContext, err
	}
	if role != group_e.GROUP_OWNER && role != group_e.GROUP_MANAGER {
		return myContext, bizerr.NoPrivileges
	}
	groupInfo, err := group_m.GetGroupInfo(model, imGroupId)
	if err != nil {
		return myContext, err
	}
	diamondAccount, err := diamond_m.GetDiamondAccountByUserId(model, groupInfo.Owner)
	if err != nil {
		return myContext, err
	}
	if diamondAccount.Status == diamond_e.Frozen {
		// 如果已经冻结,则不能操作
		return myContext, bizerr.NoPrivileges
	}
	type signalMsg struct {
		Name         *string `json:"name"`
		Introduction *string `json:"introduction"`
		Notification *string `json:"notification"`
		FaceUrl      *string `json:"faceUrl"`
		MicOn        *bool   `json:"micOn"`
		MicNumType   *group_e.GroupMicNumType
		ThemeId      *int16  `json:"themeId"`
		ThemeUrl     *string `json:"themeUrl"`
		//1: 官方 2:自定义
		ThemeType      *uint8  `json:"themeType"`
		TouristMic     *uint8  `json:"touristMic"`     // 游客是否能上麦1是2否
		TouristSendMsg *uint8  `json:"touristSendMsg"` // 游客是否能发消息1是2否
		TouristSendPic *uint8  `json:"touristSendPic"` // 游客是否能发图片1是2否
		MemberFee      *uint64 `json:"memberFee"`      // 加入会员需要黄钻数
	}
	signal := signalMsg{}
	fields := make([]string, 0)
	name, exists := c.GetPostForm("name")
	if exists {
		fields = append(fields, "name")
		signal.Name = &name

		// 2022-04-06 官方群组不能通过客户端修改名称和头像
		if group_m.IsOfficialGroup(imGroupId) {
			return myContext, bizerr.NoPrivileges
		}
	}

	introduction, exists := c.GetPostForm("introduction")
	if exists {
		fields = append(fields, "introduction")
		signal.Introduction = &introduction
	}
	notification, exists := c.GetPostForm("notification")
	if exists {
		fields = append(fields, "notification")
		signal.Notification = &notification
	}
	g := group_m.GroupInfo{
		Name:         name,
		Introduction: introduction,
		Notification: notification,
	}

	faceUrl, exists := c.GetPostForm("faceUrl")
	welcomeText := c.PostForm("welcomeText")

	// 编辑cd
	if len(name) > 0 || len(introduction) > 0 || len(notification) > 0 || len(faceUrl) > 0 || len(welcomeText) > 0 {
		if group_c.IsEditGroupCd(model, imGroupId) {
			return myContext, bizerr.EditCd
		}
	}
	if exists {
		// 2022-04-06 官方群组不能通过客户端修改名称和头像
		if group_m.IsOfficialGroup(imGroupId) {
			return myContext, bizerr.NoPrivileges
		}

		// 2022-06-20 HL限制1115,1116群组改群头像
		/*		if imGroupId == "@TGS#3LXQ2TWH3" || imGroupId == "@TGS#3LICQDVHM" {
					return myContext, bizerr.NoPrivileges
				}
		*/
		if len(faceUrl) > 0 {
			g.FaceUrl = utils.MakeFullUrl(faceUrl)

			switch config.GetConfigApp().MODERATE {
			case "AWS":
				passed, err := aws.ModerateLabels(model.Log, userId, faceUrl)
				if err == nil {
					if !passed {
						return myContext, bizerr.ImagePolicyViolation
					}
				} else {
					model.Log.Warnf("ModerateLabels err:%v", err)
				}
			case "TENCENT":
				label, err := tencentyun.ModerateImage(model, userId, "", faceUrl, g.FaceUrl)
				if err == nil && label != "Pass" {
					return myContext, bizerr.ImagePolicyViolation
				}
			}
		}
		fields = append(fields, "face_url")
		signal.FaceUrl = &g.FaceUrl
	}

	password, exists := c.GetPostForm("password")
	if exists {
		if len(password) > 0 && len(password) != group_e.ROOM_PASSWORD_LENGTH {
			return myContext, bizerr.WrongPasswordLength
		}
		g.Password = password
		fields = append(fields, "password")
	}

	if micOn, err := strconv.ParseBool(c.PostForm("micOn")); err == nil {
		g.MicOn = micOn

		fields = append(fields, "mic_on")
		signal.MicOn = &g.MicOn
	}

	if loadHistory, err := strconv.ParseBool(c.PostForm("loadHistory")); err == nil {
		g.LoadHistory = loadHistory

		fields = append(fields, "load_history")
	}

	if touristMic, err := strconv.ParseInt(c.PostForm("touristMic"), 10, 16); err == nil && touristMic > 0 && touristMic < 3 {
		g.TouristMic = uint8(touristMic)
		fields = append(fields, "tourist_mic")
		signal.TouristMic = &g.TouristMic
	}
	if touristSendMsg, err := strconv.ParseInt(c.PostForm("touristSendMsg"), 10, 16); err == nil && touristSendMsg > 0 && touristSendMsg < 3 {
		g.TouristSendMsg = uint8(touristSendMsg)
		fields = append(fields, "tourist_send_msg")
		signal.TouristSendMsg = &g.TouristSendMsg
	}
	if touristSendPic, err := strconv.ParseInt(c.PostForm("touristSendPic"), 10, 16); err == nil && touristSendPic > 0 && touristSendPic < 3 {
		g.TouristSendPic = uint8(touristSendPic)
		fields = append(fields, "tourist_send_pic")
		signal.TouristSendPic = &g.TouristSendPic
	}
	if isChangeFee, err := strconv.ParseInt(c.PostForm("isChangeFee"), 10, 16); err == nil && isChangeFee == 1 {
		if memberFee, err := strconv.ParseUint(c.PostForm("memberFee"), 10, 16); err == nil {
			g.MemberFee = memberFee
			fields = append(fields, "member_fee")
			signal.MemberFee = &g.MemberFee
		}
	}

	if themeId, err := strconv.ParseInt(c.PostForm("themeId"), 10, 16); err == nil {
		if themeId == 0 {
			g.ThemeId = int16(themeId)
			fields = append(fields, "theme_id")
			signal.ThemeId = &g.ThemeId
			var t uint8 = group_e.SETTING_CUSTOMIZED
			signal.ThemeType = &t
		} else if rows, err := res_m.GroupThemeGetAllInUse(model.Db); err == nil {
			for _, i := range rows {
				if i.ID == uint64(themeId) {
					g.ThemeId = int16(themeId)
					fields = append(fields, "theme_id")
					signal.ThemeId = &g.ThemeId
					signal.ThemeUrl = &i.Url
					var t uint8 = group_e.SETTING_OFFICIAL
					signal.ThemeType = &t
					break
				}
			}
		}
	}

	if len(welcomeText) > 0 {
		if len([]rune(welcomeText)) > group_e.GROUP_INTRODUCTION_LENGTH_LIMIT {
			model.Log.Warnf("ModifyGroupInfo %s - text too long", imGroupId)
		} else {
			gwt := group_m.GroupWelcomeText{GroupId: imGroupId, UserId: userId, Text: welcomeText}
			if err = gwt.Save(model.Db); err != nil {
				model.Log.Warnf("ModifyGroupInfo %s - Save WelcomeText FAILED:%v", imGroupId, err)
			}
		}
	}

	// 没必要就不调用
	if len(fields) > 0 {
		db := g.Update(model, imGroupId, fields)
		if db.Error != nil {
			return myContext, db.Error
		}

		buf, err := json.Marshal(signal)
		if err == nil {
			systemMsg := group_m.GroupSystemMsg{MsgId: group_e.GroupEditProfileSignal, Source: externalId, Content: string(buf)}
			signal_s.SendSignalMsg(model, imGroupId, systemMsg, false)
		}
	}

	if diceNum, err := strconv.Atoi(c.PostForm("diceNum")); err == nil {
		if diceNum <= 0 || diceNum > group_e.GROUP_DICE_NUM_MAX {
			model.Log.Warnf("ModifyGroupInfo %s: Invalid dice number %d", imGroupId, diceNum)
		} else {
			gs := group_m.GroupSetting{GroupId: imGroupId, DiceNum: uint16(diceNum)}
			if err = gs.SetDiceNum(model.Db); err != nil {
				model.Log.Warnf("ModifyGroupInfo %s: set failed: %s", imGroupId, err.Error())
			}
		}
	}
	if diceType, err := strconv.Atoi(c.PostForm("diceType")); err == nil {
		if diceType <= 0 || diceType > 2 {
			model.Log.Warnf("ModifyGroupInfo %s: Invalid dice diceType %d", imGroupId, diceType)
		} else {
			gs := group_m.GroupSetting{GroupId: imGroupId, DiceType: uint16(diceType)}
			if err = gs.SetDiceType(model.Db); err != nil {
				model.Log.Warnf("ModifyGroupInfo %s: set failed: %s", imGroupId, err.Error())
			}
		}
	}

	resp.ResponseOk(c, nil)
	return myContext, nil
}

// @Tags 群组
// @Summary 搜索群
// @Accept application/x-www-form-urlencoded
// @Param token header string true "token"
// @Param nonce header string true "随机数字"
// @Param code path string true "群CODE"
// @Success 200 {object} []group_cv.GroupInfo
// @Router /v1/imGroup/search/{code} [get]
func SearchGroup(c *gin.Context) (*mycontext.MyContext, error) {
	myContext := mycontext.CreateMyContext(c.Keys)

	myUserId, err := req.GetUserId(c)
	if err != nil {
		return myContext, err
	}

	code := c.Param("code")
	if len(code) <= 0 {
		return myContext, myerr.NewSysError("code 为必填项")
	}

	model := domain.CreateModelContext(myContext)

	result := make([]group_cv.GroupInfo, 0)
	total := 0

	g, err := group_m.GetGroupByCode(model, code)
	if err != nil {
		return myContext, err
	}

	if g != nil {
		owner, err := user_m.GetUser(model, g.Owner)
		if err != nil {
			return myContext, err
		}
		if owner.Status == user_e.UserStatusFrozen {
			if flag, _ := user_m.IsSuperManagerV2(model, myUserId, owner.ID); !flag {
				// 被封锁的用户,除了超管账户,其它用户搜索他的群组和个人ID,搜索结果为空
				resp.ResponsePageOk(c, result, uint(total), 1)
				return myContext, nil
			}
		}
		if group_m.IsHiddenGroupBy(model, g.ImGroupId) {
			if flag, _ := user_m.IsSuperManagerV2(model, myUserId, g.Owner); !flag {
				// 被隐藏的用户,除了超管账户,其它用户搜索他的群组和个人ID,搜索结果为空
				resp.ResponsePageOk(c, result, uint(total), 1)
				return myContext, nil
			}
		}
		supportLevels, err := group_s.NewGroupService(myContext).GetWeekMaxSupportLevelMap()
		if err != nil {
			return myContext, err
		}

		// 允许多个结果,虽然目前只有一个群
		groupIds := []string{g.ImGroupId}
		groupInfo := map[string]group_m.GroupInfo{g.ImGroupId: *g}

		if len(groupIds) > 0 {
			countryInfo, err := res_c.GetCountryIconMap(model)
			if err != nil {
				return myContext, err
			}

			for _, i := range groupIds {
				var password *string = nil
				if len(groupInfo[i].Password) > 0 && groupInfo[i].Owner != myUserId {
					emptyStr := ""
					password = &emptyStr
				}

				memberCount, err := group_m.GetMemberCount(model.Db, i)
				if err != nil {
					return myContext, err
				}

				result = append(result, group_cv.GroupInfo{
					GroupBasicInfo: group_cv.GroupBasicInfo{
						GroupId:      groupInfo[i].TxGroupId,
						Name:         groupInfo[i].Name,
						Introduction: groupInfo[i].Introduction,
						Notification: groupInfo[i].Notification,
						FaceUrl:      groupInfo[i].FaceUrl,
						//Owner_Account:   i.Owner_Account,
						MemberNum:    memberCount,
						Code:         groupInfo[i].Code,
						CountryIcon:  countryInfo[groupInfo[i].Country],
						Password:     password,
						SupportLevel: supportLevels[i],
						MicNumType:   int(groupInfo[i].MicNumType),
					},
				})
			}
		}
	}
	resp.ResponsePageOk(c, result, uint(total), 1)
	return myContext, nil
}

// @Tags 群组
// @Summary 离开群组(obsolete)
// @Accept application/x-www-form-urlencoded
// @Param token header string true "token"
// @Param nonce header string true "随机数字"
// @Param groupId path string true "群ID"
// @Success 200 {object} uint
// @Router /v1/imGroup/member/{groupId} [delete]
func LeaveGroup(c *gin.Context) (*mycontext.MyContext, error) {
	myContext := mycontext.CreateMyContext(c.Keys)

	groupId := c.Param("groupId")
	if len(groupId) <= 0 {
		return myContext, myerr.NewSysError("groupId 为必填项")
	}

	userId, externalId, err := req.GetUserIdAndExtId(c, myContext)
	if err != nil {
		return myContext, err
	}

	model := domain.CreateModelContext(myContext)

	groupInfo, err := group_m.GetInfoByTxGroupId(model, groupId)
	if groupInfo == nil {
		return myContext, bizerr.GroupNotFound
	}

	if userId == groupInfo.Owner {
		return myContext, bizerr.OwnerCannotLeave
	}

	if err = group_s.NewGroupService(myContext).LeaveGroup(model, groupId, userId, externalId); err != nil {
		return myContext, err
	}

	resp.ResponseOk(c, nil)
	return myContext, nil
}

// @Tags 群组
// @Summary 成为永久成员
// @Accept application/x-www-form-urlencoded
// @Param token header string true "token"
// @Param nonce header string true "随机数字"
// @Param groupId path string true "群ID"
// @Param isInvite formData int false "是否群邀请0否1是"
// @Success 200 {object} uint "结果:1 为新加入成功;2 为已经是群成员"
// @Router /v1/imGroup/permanent/{groupId} [put]
func AddPermanentMember(c *gin.Context) (*mycontext.MyContext, error) {
	myContext := mycontext.CreateMyContext(c.Keys)

	groupId := c.Param("groupId")
	if len(groupId) <= 0 {
		return myContext, myerr.NewSysError("groupId 为必填项")
	}
	isInvite, err := strconv.Atoi(c.PostForm("isInvite"))
	if err != nil {
		isInvite = 0
	}

	userId, externalId, nick, avatar, _, err := req.GetUserEx(c, myContext)
	if err != nil {
		return myContext, err
	}

	model := domain.CreateModelContext(myContext)

	ip := req.GetRequestIP(c)
	imei, err := req.GetAppImei(c)
	if err != nil {
		imei, err = user_m.GetUserImeiStr(model, userId)
		if err != nil {
			return myContext, err
		}
	}

	groupId, err = group_m.ToImGroupId(model, groupId)
	if err != nil {
		return myContext, err
	}

	// 群是否被封禁
	banned := group_m.GroupBanned{ImGroupId: groupId}
	if err = banned.Get(model); err != gorm.ErrRecordNotFound {
		return myContext, bizerr.GroupIsBanned
	}

	// 已经成员的,直接返回
	rec, err := group_m.GetJoinedGroups(model.Db, userId)
	if err != nil {
		return myContext, err
	}
	for _, i := range rec {
		if i.GroupId == groupId {
			resp.ResponseOk(c, group_e.ADD_GROUP_DUPLICATE)
			return myContext, nil

		}
	}

	// 用户是否在群的黑名单中
	if group_m.InGroupBlackList(model, groupId, imei, ip, userId) {
		return myContext, bizerr.InBlacklist
	}

	// 检查所在群数是不是已达上限
	maxJoin, err := group_s.NewGroupService(myContext).GetJoinGroupLimit(userId)
	if err != nil {
		return myContext, err
	}
	isVip, _, err := user_m.IsVip(userId)
	if err != nil {
		return myContext, err
	}
	if uint(len(rec)) >= maxJoin {
		if isVip {
			return myContext, bizerr.GroupLimitReached
		} else {
			return myContext, bizerr.GroupLimitReachedVip
		}
	}

	// 不在房间内的不能成为永久成员
	userIds, err := group_m.RoomLivingExistsUserId(groupId)
	if err != nil {
		return myContext, err
	}
	inRoom := false
	for _, u := range userIds {
		if u == userId {
			inRoom = true
			break
		}
	}
	if !inRoom {
		if !config.AppIsLocal() {
			return myContext, bizerr.IncorrectState
		}
	}
	// 加入群组是否需要收费
	needCost, err := AddPermanentMemberCost(c, myContext, isInvite, userId, groupId)
	if err != nil {
		return myContext, err
	}

	gm := group_m.GroupMember{
		GroupId: groupId,
		UserId:  userId,
	}
	if err = gm.Create(model.Db); err != nil {
		return myContext, err
	}

	// 新进群的公屏消息
	if nobleLevel, err := noble_m.GetNobleLevel(model.Db, userId); err == nil {
		msg := group_m.CommonPublicMsg{Type: group_e.UserJoinPublicScreenMsg,
			ExternalId: externalId, Nick: nick, Avatar: avatar, IsVip: isVip, NobleLevel: nobleLevel}
		buf, err := json.Marshal(msg)
		if err == nil {
			go func(groupId, text string) {
				defer func() {
					if r := recover(); r != nil {
						//打印错误堆栈信息
						mylogrus.MyLog.Errorf("SendCustomMsg SYSTEM ACTION PANIC: %v, stack: %v", r, string(debug.Stack()))
					}
				}()
				signal_s.SendCustomMsg(domain.CreateModelContext(model.MyContext), groupId, nil, text)
			}(groupId, string(buf))
		}
	}

	// fixme: 这些缓存还需要吗?
	group_c.ClearGroupMemberCount(groupId)
	tim_c.AddGroupMember(model, groupId, externalId)

	if isInvite == 1 && !needCost { // 已经接受了进群邀请
		group_m.AcceptGroupInviteJoin(model, userId, groupId)
	}

	resp.ResponseOk(c, group_e.ADD_GROUP_DONE)
	return myContext, nil
}

// 成员永久会议,检查是否需要收费
func AddPermanentMemberCost(c *gin.Context, myContext *mycontext.MyContext, isInvite int, userId uint64, groupId string) (bool, error) {
	needCost := true
	// 加入群组是否需要收费
	var model = domain.CreateModelContext(myContext)
	info, err := group_m.GetByImGroupId(model, groupId)
	if err != nil {
		return needCost, err
	}
	if info.MemberFee > 0 { // 需要收费
		// 旧版本(2.32.0以下),提示升级
		_, major, minor, _, err := req.GetAppVersion(c)
		if err != nil {
			return needCost, err
		}
		if major <= 2 && minor < 32 {
			return needCost, bizerr.UpgradeRequired
		}
		// 验证是否邀请
		if isInvite == 1 {
			inviteInfo, err := group_m.GetGroupInviteJoin(model, userId, groupId)
			if err != nil {
				return needCost, err
			}
			if inviteInfo != nil && time.Now().Unix()-inviteInfo.CreatedTime.Unix() < 86400 { // 一天内的邀请才有效
				needCost = false
			}
		}

		if needCost { // 需要扣黄钻
			err = ChangeDiamondAccountDetail(myContext, diamond_e.JoinGroupCost, mysql.ID(info.Id), userId, mysql.Num(info.MemberFee))
			if err != nil {
				return needCost, err
			}
			// 给一半群主
			err = ChangeDiamondAccountDetail(myContext, diamond_e.JoinGroupAdd, mysql.ID(info.Id), info.Owner, mysql.Num(math.Floor(float64(info.MemberFee)/2)))
			if err != nil {
				return needCost, err
			}
		}
	}
	return needCost, nil
}

func ChangeDiamondAccountDetail(myContext *mycontext.MyContext, operateType diamond_e.OperateType, originId mysql.ID, userId mysql.ID, diamondNum mysql.Num) error {
	db := mysql.Db.Begin()
	model := domain.CreateModelContext(myContext)
	diamondAccount, err := diamond_m.GetDiamondAccountByUserId(model, userId)
	if err != nil {
		return err
	}
	diamondAccountDetail, err := diamondAccount.ChangeDiamondAccountDetail(operateType, originId, diamondNum)
	if err != nil {
		db.Rollback()
		return err
	}
	if err := diamondAccountDetail.PersistentNoInTransactional(); err != nil {
		db.Rollback()
		return err
	}
	db.Commit()
	return nil
}

// @Tags 群组
// @Summary 退出永久成员
// @Accept application/x-www-form-urlencoded
// @Param token header string true "token"
// @Param nonce header string true "随机数字"
// @Param groupId path string true "群ID"
// @Success 200 {object} uint
// @Router /v1/imGroup/permanent/{groupId} [delete]
func RemovePermanentMember(c *gin.Context) (*mycontext.MyContext, error) {
	myContext := mycontext.CreateMyContext(c.Keys)

	groupId := c.Param("groupId")
	if len(groupId) <= 0 {
		return myContext, myerr.NewSysError("groupId 为必填项")
	}

	userId, externalId, err := req.GetUserIdAndExtId(c, myContext)
	if err != nil {
		return myContext, err
	}

	model := domain.CreateModelContext(myContext)

	groupInfo, err := group_m.GetInfoByTxGroupId(model, groupId)
	if err != nil {
		return myContext, err
	}
	if groupInfo == nil {
		return myContext, bizerr.GroupNotFound
	}
	groupId = groupInfo.ImGroupId

	if userId == groupInfo.Owner {
		return myContext, bizerr.OwnerCannotLeave
	}

	if err = group_s.NewGroupService(myContext).LeaveGroupMember(model, groupId, userId, externalId); err != nil {
		return myContext, err
	}

	resp.ResponseOk(c, nil)
	return myContext, nil
}

type GroupMembersRsp struct {
	Members []group_cv.MemberDetail `json:"members"`
	Online  uint                    `json:"online"` // 在线人数
	Total   uint                    `json:"total"`
}

// @Tags 群组
// @Summary 获取永久成员列表
// @Accept application/x-www-form-urlencoded
// @Param token header string true "token"
// @Param nonce header string true "随机数字"
// @Param groupId path string true "群ID"
// @Param pageSize query int false "分页大小 默认:10" default(10)
// @Param pageIndex query int false "第几个分页,从1开始 默认:1" default(1)
// @Success 200 {object} GroupMembersRsp
// @Router /v1/imGroup/permanent/{groupId} [get]
func GetPermanentMember(c *gin.Context) (*mycontext.MyContext, error) {
	myContext := mycontext.CreateMyContext(c.Keys)

	groupId := c.Param("groupId")
	if len(groupId) <= 0 {
		return myContext, bizerr.ParaMissing
	}

	pageSize, err := strconv.Atoi(c.Query("pageSize"))
	if err != nil || pageSize <= 0 {
		pageSize = 10
	}
	pageIndex, err := strconv.Atoi(c.Query("pageIndex"))
	if err != nil || pageIndex <= 0 {
		pageIndex = 1
	}

	userId, _, err := req.GetUserIdAndExtId(c, myContext)
	if err != nil {
		return myContext, err
	}

	model := domain.CreateModelContext(myContext)

	groupId, err = group_m.ToImGroupId(model, groupId)
	if err != nil {
		return myContext, err
	}

	rows, err := group_m.GetMembers(model.Db, groupId)
	if err != nil {
		return myContext, err
	}
	userIds := make([]uint64, 0)
	for _, i := range rows {
		userIds = append(userIds, i.UserId)
	}
	users, err := user_m.GetUserMapByIds(model, userIds)
	if err != nil {
		return myContext, err
	}
	model.Log.Infof("GetGroupMembers %s: memberNum = %d, user size = %d", groupId, len(rows), len(userIds))

	result := GroupMembersRsp{Total: uint(len(rows))}

	beginPos := pageSize * (pageIndex - 1)
	if uint(beginPos) < result.Total {
		// 取在线状态
		extIds := make([]string, 0)
		for _, i := range users {
			extIds = append(extIds, i.ExternalId)
		}
		statusMap, err := tim_m.GetOnlineStatus(model, extIds)
		if err != nil {
			return myContext, err
		}

		result.Online = 0
		for _, v := range statusMap {
			if v == online_e.IM_STATUS_ON_LINE {
				result.Online++
			}
		}
		model.Log.Infof("GetGroupMembers %s: statusMap size = %d, onLine = %d", groupId, len(statusMap), result.Online)

		roles, _, err := group_m.GetRolesInGroup(model, groupId)
		if err != nil {
			return myContext, err
		}

		nobleLevels, err := noble_m.BatchGetNobleLevel(model.Db, userIds)
		if err != nil {
			return myContext, err
		}

		vips, err := user_m.BatchGetVips(userIds)
		if err != nil {
			return myContext, err
		}

		model.Log.Infof("GetGroupMembers %s, users %v, roles: %v, nobles: %v, vips: %v", groupId, userIds, roles, nobleLevels, vips)

		roomUsers, err := group_m.RoomLivingExistsUserId(groupId)
		if err != nil {
			return myContext, err
		}

		roomUserMap := utils.SliceToMapUInt64(roomUsers)
		//model.Log.Infof("GetGroupMembers %s, roomStatusMap %v", groupId, roomStatusMap)

		// 排序规则 :在房间的优先,其次是在线,再次看角色,最后看贵族
		sort.Slice(userIds, func(i, j int) bool {
			ui := userIds[i]
			uj := userIds[j]

			_, ok1 := roomUserMap[ui]
			_, ok2 := roomUserMap[uj]
			if ok1 && !ok2 {
				return true
			} else if ok1 == ok2 {
				ei := users[ui].ExternalId
				ej := users[uj].ExternalId
				if statusMap[ei] > statusMap[ej] {
					return true
				}
				if statusMap[ei] == statusMap[ej] {
					if roles[ui] > roles[uj] {
						return true
					}
					if roles[ui] == roles[uj] {
						// 贵族5>贵族4>贵族3>贵族2>VIP
						if nobleLevels[ui] > nobleLevels[uj] && nobleLevels[ui] >= 2 {
							return true
						}
						if nobleLevels[ui] == nobleLevels[uj] || nobleLevels[ui] < 2 && nobleLevels[uj] < 2 {
							if vips[ui] != nil {
								if vips[uj] == nil {
									return true
								} else {
									return users[ui].Code < users[uj].Code
								}
							} else if vips[uj] == nil {
								return users[ui].Code < users[uj].Code
							}
						}
					}
				}
			}
			return false
		})

		model.Log.Infof("GetGroupMembers %s, sorted users: %v", groupId, userIds)

		endPos := pageSize * pageIndex
		if endPos > len(userIds) {
			endPos = len(userIds)
		}

		userIds = userIds[beginPos:endPos]
		userExtends, err := user_cv.BatchGetUserExtend(model, userIds, userId)
		if err != nil {
			return myContext, err
		}

		for _, u := range userIds {
			inRoom := false
			if _, ok := roomUserMap[u]; ok {
				inRoom = true
			}
			result.Members = append(result.Members, group_cv.MemberDetail{
				CvUserExtend: userExtends[u],
				Role:         roles[u],
				OnlineStatus: statusMap[users[u].ExternalId],
				InRoom:       inRoom,
			})
		}
	}
	resp.ResponseOk(c, result)
	return myContext, nil
}

// @Tags 群组
// @Summary 全服广播群消息(运营用,慎用)
// @Accept application/x-www-form-urlencoded
// @Param token header string true "token"
// @Param nonce header string true "随机数字"
// @Param contentType formData int true "消息类型:1 跳转url;2 跳转房间"
// @Param content formData string true "消息内容"
// @Param h5url formData string false "要转跳的url"
// @Param groupId formData string false "群ID"
// @Success 200 {object} uint
// @Router /v1/imGroup/allGroupMsg [put]
func SendTextMsg(c *gin.Context) (*mycontext.MyContext, error) {
	myContext := mycontext.CreateMyContext(c.Keys)

	userId, err := req.GetUserId(c)
	if err != nil {
		return myContext, err
	}

	if !user_m.IsSuperUser(userId) {
		return myContext, bizerr.NoPrivileges
	}

	contentType, err := strconv.Atoi(c.PostForm("contentType"))
	if err != nil {
		return myContext, bizerr.ParaMissing
	}
	content := c.PostForm("content")
	if len(content) <= 0 {
		return myContext, bizerr.ParaMissing
	}

	h5url := ""
	groupId := ""
	if contentType == 1 {
		h5url = c.PostForm("h5url")
		if len(h5url) == 0 {
			return myContext, bizerr.InvalidParameter
		}
	} else if contentType == 2 {
		groupId = c.PostForm("groupId")
		if len(groupId) == 0 {
			return myContext, bizerr.InvalidParameter
		}
	} else {
		return myContext, bizerr.InvalidParameter
	}

	model := domain.CreateModelContext(myContext)

	txGroupId := groupId
	groupId, err = group_m.ToImGroupId(model, groupId)
	if err != nil {
		return myContext, err
	}

	gsm := group_m.GroupCustomMsg{
		CommonPublicMsg: group_m.CommonPublicMsg{Type: group_e.JumpMessage},
		ContentType:     contentType,
		Content:         content,
		H5:              h5url,
		GroupId:         txGroupId,
	}
	buf, err := json.Marshal(gsm)
	var failedCount uint = 0
	if err == nil {
		groupIds, err := group_m.GetAllGroupIds(model.Db)
		if err != nil {
			return myContext, err
		}
		for i, g := range groupIds {
			seq, err := signal_s.SendCustomMsg(model, g, nil, string(buf))
			if err == nil {
				model.Log.Infof("Succeeded in sending GroupCustomMsg to group %s, seq = %d", g, seq)
			} else {
				model.Log.Infof("Failed in sending GroupCustomMsg to group %s", g)
				failedCount++
			}
			if i%100 == 0 {
				time.Sleep(time.Millisecond * 500)
			}
		}
	}
	resp.ResponseOk(c, failedCount)
	return myContext, nil
}

// @Tags 群组
// @Summary 复制群到海外版
// @Accept application/x-www-form-urlencoded
// @Param token header string true "token"
// @Param nonce header string true "随机数字"
// @Param groupId formData string false "群ID"
// @Param size formData uint false "批量单位"
// @Param limit formData uint false "批量数"
// @Success 200 {object} []string
// @Router /v1/imGroup/upgrade [put]
func UpgradeGroup(c *gin.Context) (*mycontext.MyContext, error) {
	myContext := mycontext.CreateMyContext(c.Keys)

	userId, err := req.GetUserId(c)
	if err != nil {
		return myContext, err
	}

	if !user_m.IsSuperUser(userId) {
		return myContext, bizerr.NoPrivileges
	}

	model := domain.CreateModelContext(myContext)

	failed := make([]string, 0)
	groupId := c.PostForm("groupId")
	if len(groupId) > 0 {
		gi, err := group_m.GetGroupInfo(model, groupId)
		if err != nil {
			return myContext, err
		}
		if gi == nil {
			return myContext, bizerr.GroupNotFound
		}
		// fixme:
		if gi.Type == group_e.OverseaRoom {
			model.Log.Infof("UpgradeGroup, no need to upgrade %s", groupId)
		} else {
			if err = upgradeRoom(model, gi); err != nil {
				failed = append(failed, gi.ImGroupId)
			}
		}
	} else {
		size, err := strconv.Atoi(c.PostForm("size"))
		if err != nil || size == 0 {
			return myContext, bizerr.InvalidParameter
		} else {
			limit, _ := strconv.Atoi(c.PostForm("limit"))

			g := group_m.GroupInfo{Type: group_e.LocalRoom}
			rec, err := g.FindAllGroups(model.Db)
			if err != nil {
				return myContext, err
			}
			index := 0
			for start := 0; start < len(rec) && index < limit; start += size {
				end := start + size
				if end >= len(rec) {
					end = len(rec)
				}
				go func(index int, groups []group_m.GroupInfo) {
					model.Log.Infof("Rountine %d process %d", index, len(groups))
					for i, g := range groups {
						model.Log.Infof("Routine %d upgrading %s", index, g.ImGroupId)
						if err = upgradeRoom(model, &g); err != nil {
							model.Log.Errorf("Upgrade %s failed", g.ImGroupId)
						}
						if i > 0 && i%50 == 0 {
							time.Sleep(time.Second)
						}
					}
				}(index, rec[start:end])
				index++
			}
		}
	}

	resp.ResponseOk(c, failed)
	return myContext, nil
}

func upgradeRoom(model *domain.Model, gi *group_m.GroupInfo) error {
	if strings.HasPrefix(gi.TxGroupId, group_e.OverseaGroupNamePrefix) {
		// 已经是新版,无需处理
		return nil
	}

	if strings.HasPrefix(gi.TxGroupId, group_e.OldGroupNamePrefix) {
		// 升级群号,然后两边都升级
		newGroupId := group_e.NewGroupNamePrefix + gi.TxGroupId[len(group_e.OldGroupNamePrefix):]

		// 避免TX报错
		if len(gi.Name) <= 0 {
			gi.Name = "empty"
		}
		txGroupId, err := tencentyun.CreateGroup(gi.Name, newGroupId)
		if err != nil {
			return err
		}
		g := group_m.GroupInfo{
			TxGroupId: txGroupId,
			Type:      group_e.OverseaRoom,
		}
		db := g.Update(model, gi.ImGroupId, []string{"tx_group_id", "type"})
		if db.Error != nil {
			return err
		}

		tencentyun.DestroyGroup(gi.TxGroupId, true)
	} else {
		// 在海外建一个同样的群即可
		// 避免TX报错
		if len(gi.Name) <= 0 {
			gi.Name = "empty"
		}
		_, err := tencentyun.CreateGroup(gi.Name, gi.TxGroupId)
		if err != nil {
			return err
		}

		// 更新type即可
		g := group_m.GroupInfo{
			Type: group_e.OverseaRoom,
		}
		db := g.Update(model, gi.ImGroupId, []string{"type"})
		if db.Error != nil {
			return err
		}
	}
	// 删除旧群
	return nil
}

// @Tags 群组
// @Summary 降级直播间为会议模式
// @Accept application/x-www-form-urlencoded
// @Param token header string true "token"
// @Param nonce header string true "随机数字"
// @Param groupId formData string false "群ID"
// @Success 200
// @Router /v1/imGroup/downgrade [put]
func DowngradeGroup(c *gin.Context) (*mycontext.MyContext, error) {
	myContext := mycontext.CreateMyContext(c.Keys)

	userId, err := req.GetUserId(c)
	if err != nil {
		return myContext, err
	}

	if !user_m.IsSuperUser(userId) {
		return myContext, bizerr.NoPrivileges
	}

	model := domain.CreateModelContext(myContext)

	groupId := c.PostForm("groupId")
	if len(groupId) <= 0 {
		return myContext, bizerr.InvalidParameter
	}

	gi, err := group_m.GetGroupInfo(model, groupId)
	if err != nil {
		return myContext, err
	}
	if gi == nil {
		return myContext, bizerr.GroupNotFound
	}
	if gi.Type == 2 {
		model.Log.Infof("DowngradeGroup, no need to downgrade %s", groupId)
	} else {
		if err = downgradeRoom(myContext, gi); err != nil {
			return myContext, err
		}
	}

	resp.ResponseOk(c, nil)
	return myContext, nil
}

func downgradeRoom(myContext *mycontext.MyContext, gi *group_m.GroupInfo) error {
	// 删除旧直播间
	err := tencentyun.DestroyGroup(gi.TxGroupId, false)
	if err != nil {
		return err
	}

	// 恢复会议群
	groupId, err := tencentyun.CreateGroup(gi.Name, "")
	if err != nil {
		return err
	}

	g := group_m.GroupInfo{
		ImGroupId:      groupId,
		TxGroupId:      groupId,
		Type:           group_e.LocalRoom,
		Code:           gi.Code + "#",
		OriginCode:     gi.OriginCode,
		Owner:          gi.Owner,
		Name:           gi.Name,
		Introduction:   gi.Introduction,
		Notification:   gi.Notification,
		FaceUrl:        gi.FaceUrl,
		Country:        gi.Country,
		ChannelId:      gi.ChannelId,
		MicOn:          gi.MicOn,
		LoadHistory:    gi.LoadHistory,
		MicNumType:     gi.MicNumType,
		TouristMic:     1,
		TouristSendMsg: 1,
		TouristSendPic: 1,
	}

	if err := group_s.NewGroupService(myContext).CreateGroup(g.Owner, &g); err != nil {
		// 回滚,删除刚刚建立的TX群组
		tencentyun.DestroyGroup(groupId, false)
		return err
	}
	return nil
}

// @Tags 群组
// @Summary 进入房间
// @Accept application/x-www-form-urlencoded
// @Param token header string true "token"
// @Param nonce header string true "随机数字"
// @Param groupId formData string true "群ID"
// @Param password formData string false "房间密码"
// @Param enterType formData int false "进房类型:1.ludo游戏快速匹配进房 2:uno"
// @Param gameCode formData string false "gameCode"
// @Success 200 {object} group_cv.GroupChannelId
// @Router /v1/imGroup/in [put]
func GroupIn(c *gin.Context) (*mycontext.MyContext, error) {
	myContext := mycontext.CreateMyContext(c.Keys)
	userId, externalId, err := req.GetUserIdAndExtId(c, myContext)
	if err != nil {
		return myContext, err
	}
	groupId := c.PostForm("groupId")
	password := c.PostForm("password")
	enterType := c.PostForm("enterType")
	gameCode := c.PostForm("gameCode")
	// 把id:9 添加进房间:5030的黑名单
	if (userId == 2087771 || userId == 1763211) && groupId == "HTGS#a46766257" {
		return myContext, bizerr.NoPrivileges
	}
	// 把Cartier和1980加入到房间id:VIP、房间id:305、房间id:7的房间黑名单
	if (userId == 2129451 || userId == 2809891) && groupId == "HTGS#a25015185" {
		return myContext, bizerr.NoPrivileges
	}
	if (userId == 2129451 || userId == 2809891) && groupId == "HTGS#a48723458" {
		return myContext, bizerr.NoPrivileges
	}
	if (userId == 2129451 || userId == 2809891) && groupId == "HTGS#a69660046" {
		return myContext, bizerr.NoPrivileges
	}

	model := domain.CreateModelContext(myContext)
	gi, err := group_m.GetInfoByTxGroupId(model, groupId)
	if err != nil {
		return myContext, err
	}
	if gi == nil {
		return myContext, bizerr.GroupNotFound
	}
	groupId = gi.ImGroupId

	ip := req.GetRequestIP(c)
	imei, err := req.GetAppImei(c)
	if err != nil {
		imei, err = user_m.GetUserImeiStr(model, userId)
		if err != nil {
			return myContext, err
		}
	}
	model.Log.Infof("GroupIn ip userId:%v,imGroupId:%v,ip:%v,imei:%v", userId, groupId, ip, imei)

	provider := group_e.GroupProvider_SW
	if group_m.IsUseTRTC(model, groupId) {
		provider = group_e.GroupProvider_TRTC
		//total := int64(11000)
		//if !config.AppIsRelease() {
		//	total = 30
		//}
		//if group_m.CountTRTC(model) > total {
		//	roomUser, err := group_m.GetRoomOnlineUser(myContext, groupId)
		//	if err == nil && roomUser.Total == 0 {
		//		if err := group_m.DeleteTRTC(model, groupId); err == nil {
		//			provider = group_e.GroupProvider_SW
		//		}
		//	}
		//}
	} else {
		// 增加trtc房间
		//dayMaxCovertNum := int64(600)
		//numKey := rediskey.GetConvertToTRTCNum()
		//covertNum, err := redisCli.GetCacheInt64(numKey)
		//if time.Now().Unix() > 1684810309 && time.Now().Unix() < 1684857600 && err == nil && covertNum <= dayMaxCovertNum {
		//	roomUser, err := group_m.GetRoomOnlineUser(myContext, groupId)
		//	if err == nil && roomUser.Total == 0 {
		//		newNum, err := redisCli.IncrNumExpire(numKey, 1, time.Hour*24)
		//		if err == nil && newNum <= dayMaxCovertNum {
		//			if err := group_m.InitTRTC(model, groupId); err != nil {
		//				return myContext, err
		//			}
		//			provider = group_e.GroupProvider_TRTC
		//			model.Log.Infof("auto shift trtc room groupId:%v,%+v-%v", groupId, roomUser, err)
		//		}
		//	}
		//}
	}
	// 进房时判断要用哪个im
	targetProvider := group_m.GetRandomImProvider(model)
	if provider != targetProvider {
		roomUser, err := group_m.GetRoomOnlineUser(myContext, groupId)
		if err == nil && roomUser.Total == 0 { // 房间没人才做切换
			switch provider {
			case group_e.GroupProvider_SW: // 旧的是声网,切换成trtc
				if err := group_m.InitTRTC(model, groupId); err != nil {
					return myContext, err
				}
			case group_e.GroupProvider_TRTC: // 旧的是trtc,切换成声网
				if err := group_m.DeleteTRTC(model, groupId); err != nil {
					return myContext, err
				}
			}
			// 切换
			provider = targetProvider
			// log
			err = group_m.CreateGroupImLog(model, groupId, provider)
			if err != nil {
				model.Log.Errorf("CreateGroupImLog err:%v, imGroupId:%s, provider:%v", err, groupId, provider)
			}
		}
	}

	if provider == group_e.GroupProvider_TRTC {
		// 版本升级提示,旧版本(3.6.0以下),提示升级
		_, major, minor, _, err := req.GetAppVersion(c)
		if err != nil {
			return myContext, err
		}
		if (major < 3) || (major == 3 && minor < 6) {
			return myContext, bizerr.UpgradeRequired
		}
	}
	// 指定房间号写死trtc
	if groupId == "HTGS#a93989299" {
		provider = group_e.GroupProvider_TRTC
	}

	if channelId, token, err := group_s.NewGroupService(myContext).GroupIn(userId, externalId, groupId, password, imei, ip, provider, gi.Id); err != nil {
		return myContext, err
	} else {
		// 加入房间缓存
		if err = room_c.ProcessRoomVisit(groupId, userId); err != nil {
			myContext.Log.Infof("GroupIn, ProcessRoomVisit err: %s", err.Error())
		}
		// 更新用户进入房间缓存记录
		if err = room_c.ProcessUserRoomVisit(userId, groupId); err != nil {
			myContext.Log.Infof("GroupIn, ProcessUserRoomVisit err: %s", err.Error())
		}
		resp.ResponseOk(c, group_cv.GroupChannelId{
			ChannelId:  channelId,
			Token:      token,
			AgoraId:    uint32(userId),
			MicNumType: gi.MicNumType,
			Provider:   provider,
		})

		// v2.26及以后,客户端自己加TIM群,不再由服务器代加
		_, major, minor, _, err := req.GetAppVersion(c)
		if err != nil || major < 2 || major == 2 && minor < 26 {
			go func() {
				defer func() {
					if r := recover(); r != nil {
						//打印错误堆栈信息
						mylogrus.MyLog.Errorf("GroupIn - JoinGroup, SYSTEM ACTION PANIC: %v, stack: %v", r, string(debug.Stack()))
					}
				}()
				err := group_s.NewGroupService(myContext).JoinGroup(userId, externalId, gi.TxGroupId)
				mylogrus.MyLog.Infof("myService.JoinGroup %s, user %d, err:%v", groupId, userId, err)
			}()
		}
		// 判断是否需要执行游戏逻辑
		if enterType != "" && gameCode != "" {
			traceId, _ := c.Get(mycontext.TRACEID)
			token := c.Writer.Header().Get(mycontext.TOKEN)
			err := game_c.SetAutoMathEnterRoom(userId, gi.ImGroupId, cast.ToString(traceId), token, enterType, gameCode)
			if err != nil {
				model.Log.Errorf("GroupIn cache.SetAutoMathEnterRoom userId:%v, imGroupId:%v, err:%v", userId, gi.ImGroupId, err)
			}
			//go proxy.GameAfterEnterRoom(model, userId, externalId, traceId, token, enterType, gameCode, gi)
		}
		//// 临时
		//go func() {
		//	time.Sleep(time.Second * 2)
		//	//发送全麦信息
		//	myContext.Log.Infof("imCallBack CallbackAfterNewMemberJoin MicAllRPush begin MemberAccount:%v, gi:%v", externalId, gi)
		//	if err := group_m.MicAllRPush(domain.CreateModelContext(myContext), groupId, externalId); err != nil {
		//		myContext.Log.Errorf("imCallBack CallbackAfterNewMemberJoin MicAllRPush err MemberAccount:%v, gi:%v,err:%v", externalId, gi, err)
		//	} else {
		//		myContext.Log.Infof("imCallBack CallbackAfterNewMemberJoin MicAllRPush success MemberAccount:%v, gi:%v,err:%v", externalId, gi, err)
		//	}
		//	//加入在线列表
		//	user, err := user_m.GetUserByExtId(domain.CreateModelContext(myContext), externalId)
		//	if err != nil {
		//		myContext.Log.Errorf("imCallBack CallbackAfterNewMemberJoin RoomLivingIn GetUserByExtId err:%+v, MemberAccount:%v", err, externalId)
		//	}
		//	//添加用户在线列表
		//	err = group_m.RoomLivingIn(domain.CreateModelContext(myContext), groupId, user.ID, user.ExternalId, false)
		//	if err != nil {
		//		myContext.Log.Errorf("imCallBack CallbackAfterNewMemberJoin err:%+v, userId:%v", err, user.ID)
		//	} else {
		//		myContext.Log.Infof("imCallBack CallbackAfterNewMemberJoin RoomLivingIn success MemberAccount:%v, gi:%v", externalId, gi)
		//	}
		//}()
		return myContext, nil
	}
}

// @Tags 群组
// @Summary 离开房间
// @Accept application/x-www-form-urlencoded
// @Param token header string true "token"
// @Param nonce header string true "随机数字"
// @Param groupId formData string true "群ID"
// @Success 200
// @Router /v1/imGroup/leave [post]
func GroupLeave(c *gin.Context) (*mycontext.MyContext, error) {
	myContext := mycontext.CreateMyContext(c.Keys)
	userId, exteranlId, err := req.GetUserIdAndExtId(c, myContext)
	if err != nil {
		return myContext, err
	}

	groupId := c.PostForm("groupId")
	if len(groupId) <= 0 {
		return myContext, bizerr.InvalidParameter
	}

	model := domain.CreateModelContext(myContext)
	groupId, err = group_m.ToImGroupId(model, groupId)
	if err != nil {
		return myContext, err
	}

	if err := group_s.NewGroupService(myContext).GroupLeave(userId, exteranlId, groupId); err != nil {
		myContext.Log.Errorf("GroupLeave GroupLeave err:%v", err)
		return myContext, err
	} else {
		/*		roomOnlineUser, err := cv.GetGroupInUser(domain.CreateModelContext(myContext), groupId)
				if err != nil {
					myContext.Log.Errorf("cron socketStatus cv.GetGroupInUser err:%v", err)
				}
				buf, err := json.Marshal(roomOnlineUser)
				if err != nil {
					myContext.Log.Errorf("cron socketStatus json.Marshal err:%v", err)
				}
				service.SendSignalMsg(groupId, group_m.GroupSystemMsg{
					MsgId:  group_enum.GroupOnlineUser,
					Content: string(buf),
				} , true)*/
	}
	resp.ResponseOk(c, nil)
	return myContext, nil
}

// @Tags 群组
// @Summary 踢人
// @Accept application/x-www-form-urlencoded
// @Param token header string true "token"
// @Param nonce header string true "随机数字"
// @Param groupId formData string true "群ID"
// @Param externalId formData string true "用户的externalId"
// @Success 200
// @Router /v1/imGroup/kick [post]
func GroupKick(c *gin.Context) (*mycontext.MyContext, error) {
	myContext := mycontext.CreateMyContext(c.Keys)
	userId, externalId, nick, avatar, _, err := req.GetUserEx(c, myContext)
	if err != nil {
		return myContext, err
	}
	groupId := c.PostForm("groupId")
	if len(groupId) <= 0 {
		return myContext, bizerr.InvalidParameter
	}

	_, lang, err := req.GetUserIdLang(c, myContext)
	if err != nil {
		return myContext, err
	}

	model := domain.CreateModelContext(myContext)
	txGroupId := groupId
	groupId, err = group_m.ToImGroupId(model, groupId)
	if err != nil {
		return myContext, err
	}

	beKickExternalId := c.PostForm("externalId")
	beKickUser, err := user_c.GetUserByExternalId(domain.CreateModelContext(myContext), beKickExternalId)
	if err != nil {
		return myContext, err
	}
	isGaming, err := game_m.IsGaming(model, beKickUser.ID, txGroupId)
	if err != nil {
		return myContext, err
	}
	if isGaming {
		return myContext, bizerr.GamingCannotKick
	}

	if err = CheckOptToSvip6(model, userId, beKickUser.ID, lang, 10); err != nil {
		return myContext, err
	}

	//beKickUserId, err := toUserId(beKickExternalId)
	if err := group_s.NewGroupService(myContext).GroupKick(groupId, userId, externalId, nick, avatar, beKickUser.ID, beKickUser.ExternalId, beKickUser.Nick, beKickUser.Avatar, lang); err != nil {
		return myContext, err
	}
	resp.ResponseOk(c, nil)
	return myContext, nil
}