// Copyright © 2023 OpenIM. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package api

import (
	"context"
	"crypto/md5"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"math/big"
	"math/rand"
	"time"

	"github.com/gin-gonic/gin"
	"go.mongodb.org/mongo-driver/mongo"

	"git.imall.cloud/openim/open-im-server-deploy/pkg/apistruct"
	"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database"
	"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
	"git.imall.cloud/openim/open-im-server-deploy/pkg/common/webhook"
	"git.imall.cloud/openim/open-im-server-deploy/pkg/rpcli"
	"git.imall.cloud/openim/protocol/constant"
	"git.imall.cloud/openim/protocol/group"
	"git.imall.cloud/openim/protocol/sdkws"
	"git.imall.cloud/openim/protocol/wrapperspb"
	"github.com/openimsdk/tools/apiresp"
	"github.com/openimsdk/tools/errs"
	"github.com/openimsdk/tools/log"
	"github.com/openimsdk/tools/mcontext"
	"github.com/openimsdk/tools/utils/datautil"
	"github.com/openimsdk/tools/utils/idutil"
	"github.com/openimsdk/tools/utils/timeutil"
)

type MeetingApi struct {
	meetingDB          database.Meeting
	meetingCheckInDB   database.MeetingCheckIn
	groupClient        *rpcli.GroupClient
	userClient         *rpcli.UserClient
	conversationClient *rpcli.ConversationClient
	systemConfigDB     database.SystemConfig
}

func NewMeetingApi(meetingDB database.Meeting, meetingCheckInDB database.MeetingCheckIn, groupClient *rpcli.GroupClient, userClient *rpcli.UserClient, conversationClient *rpcli.ConversationClient, systemConfigDB database.SystemConfig) *MeetingApi {
	return &MeetingApi{
		meetingDB:          meetingDB,
		meetingCheckInDB:   meetingCheckInDB,
		groupClient:        groupClient,
		userClient:         userClient,
		conversationClient: conversationClient,
		systemConfigDB:     systemConfigDB,
	}
}

// IsNotFound 检查错误是否是记录不存在
func (m *MeetingApi) IsNotFound(err error) bool {
	return errs.ErrRecordNotFound.Is(err) || errs.Unwrap(err) == mongo.ErrNoDocuments
}

// loadAnchorUsers 加载主播用户信息
func (m *MeetingApi) loadAnchorUsers(ctx context.Context, anchorUserIDs []string) []*sdkws.UserInfo {
	if len(anchorUserIDs) == 0 {
		return nil
	}
	users, err := m.userClient.GetUsersInfo(ctx, anchorUserIDs)
	if err != nil {
		log.ZWarn(ctx, "loadAnchorUsers: failed to get anchor users info", err, "anchorUserIDs", anchorUserIDs)
		return nil
	}
	return users
}

// genMeetingPassword 生成6位数字密码
func (m *MeetingApi) genMeetingPassword() string {
	return fmt.Sprintf("%06d", rand.Intn(1000000))
}

// genMeetingID 生成会议ID
func (m *MeetingApi) genMeetingID(ctx context.Context, meetingID string) (string, error) {
	if meetingID != "" {
		_, err := m.meetingDB.Take(ctx, meetingID)
		if err == nil {
			return "", errs.ErrArgs.WrapMsg("meeting id already exists: " + meetingID)
		}
		// 如果记录不存在，说明可以使用这个ID
		if m.IsNotFound(err) {
			return meetingID, nil
		}
		// 其他错误直接返回
		return "", err
	}
	// 生成唯一ID
	for i := 0; i < 10; i++ {
		opID := mcontext.GetOperationID(ctx)
		timestamp := time.Now().UnixNano()
		random := rand.Int()
		data := fmt.Sprintf("%s,%d,%d", opID, timestamp, random)
		hash := md5.Sum([]byte(data))
		id := hex.EncodeToString(hash[:])[:16]
		bi := big.NewInt(0)
		bi.SetString(id, 16)
		id = bi.String()
		_, err := m.meetingDB.Take(ctx, id)
		if err == nil {
			// ID已存在，继续生成下一个
			continue
		}
		// 如果记录不存在，说明可以使用这个ID
		if m.IsNotFound(err) {
			return id, nil
		}
		// 其他错误直接返回
		return "", err
	}
	return "", errs.ErrInternalServer.WrapMsg("failed to generate meeting id")
}

// paginationWrapper 实现 pagination.Pagination 接口
type meetingPaginationWrapper struct {
	pageNumber int32
	showNumber int32
}

func (p *meetingPaginationWrapper) GetPageNumber() int32 {
	if p.pageNumber <= 0 {
		return 1
	}
	return p.pageNumber
}

func (p *meetingPaginationWrapper) GetShowNumber() int32 {
	if p.showNumber <= 0 {
		return 20
	}
	return p.showNumber
}

// CreateMeeting 创建会议
func (m *MeetingApi) CreateMeeting(c *gin.Context) {
	var (
		req  apistruct.CreateMeetingReq
		resp apistruct.CreateMeetingResp
	)
	if err := c.BindJSON(&req); err != nil {
		apiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap())
		return
	}

	// 获取当前用户ID
	creatorUserID := mcontext.GetOpUserID(c)
	if creatorUserID == "" {
		apiresp.GinError(c, errs.ErrArgs.WrapMsg("user id not found"))
		return
	}

	// 生成会议ID
	meetingID, err := m.genMeetingID(c, req.MeetingID)
	if err != nil {
		log.ZError(c, "CreateMeeting: failed to generate meeting id", err)
		apiresp.GinError(c, err)
		return
	}

	// 验证预约时间不能早于当前时间
	scheduledTime := time.UnixMilli(req.ScheduledTime)
	if scheduledTime.Before(time.Now()) {
		apiresp.GinError(c, errs.ErrArgs.WrapMsg("scheduled time cannot be earlier than current time"))
		return
	}

	// 处理密码：如果未提供，自动生成6位数字密码
	password := req.Password
	if password == "" {
		password = m.genMeetingPassword()
	} else {
		// 验证密码格式：必须是6位数字
		if len(password) != 6 {
			apiresp.GinError(c, errs.ErrArgs.WrapMsg("password must be 6 digits"))
			return
		}
		for _, char := range password {
			if char < '0' || char > '9' {
				apiresp.GinError(c, errs.ErrArgs.WrapMsg("password must be 6 digits"))
				return
			}
		}
	}

	// 创建群聊，群聊名称为"会议群-[会议主题]"
	groupName := fmt.Sprintf("会议群-%s", req.Subject)
	// 在Ex字段中设置会议标识
	exData := map[string]interface{}{
		"isMeetingGroup": true,
		"meetingID":      meetingID,
	}
	exJSON, err := json.Marshal(exData)
	if err != nil {
		log.ZError(c, "CreateMeeting: failed to marshal ex data", err)
		apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("failed to marshal ex data"))
		return
	}

	groupInfo := &sdkws.GroupInfo{
		GroupName:        groupName,
		GroupType:        constant.WorkingGroup,
		FaceURL:          req.CoverURL,      // 使用会议封面作为群头像
		Ex:               string(exJSON),    // 设置会议标识
		NeedVerification: constant.Directly, // 设置为直接加入，任何人都能随意加群
	}

	// 设置群主和管理员：第一个主播为群主，其他主播为管理员
	var ownerUserID string
	var adminUserIDs []string
	var memberUserIDs []string
	if len(req.AnchorUserIDs) > 0 {
		// 有主播时，第一个主播为群主，其他主播为管理员
		ownerUserID = req.AnchorUserIDs[0]
		if len(req.AnchorUserIDs) > 1 {
			adminUserIDs = req.AnchorUserIDs[1:]
		}
		// 如果创建者不在主播列表中，将创建者作为普通成员加入
		anchorMap := make(map[string]bool)
		for _, anchorID := range req.AnchorUserIDs {
			anchorMap[anchorID] = true
		}
		if !anchorMap[creatorUserID] {
			memberUserIDs = []string{creatorUserID}
		}
	} else {
		// 没有主播时，创建者为群主
		ownerUserID = creatorUserID
	}

	createGroupReq := &group.CreateGroupReq{
		OwnerUserID:   ownerUserID,
		AdminUserIDs:  adminUserIDs,
		MemberUserIDs: memberUserIDs,
		GroupInfo:     groupInfo,
	}

	createGroupResp, err := m.groupClient.GroupClient.CreateGroup(c, createGroupReq)
	if err != nil {
		log.ZError(c, "CreateMeeting: failed to create group", err, "meetingID", meetingID)
		apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("failed to create group"))
		return
	}

	groupID := createGroupResp.GroupInfo.GroupID

	// 兜底：再次对主播列表进行角色同步，确保主播=群主/管理员
	if len(req.AnchorUserIDs) > 0 {
		if err := m.updateGroupAnchors(c, groupID, req.AnchorUserIDs); err != nil {
			log.ZWarn(c, "CreateMeeting: failed to sync anchors to owner/admin", err, "groupID", groupID, "anchorUserIDs", req.AnchorUserIDs)
		}
	}

	// 收集所有群成员ID（群主、管理员、普通成员），用于创建会话
	allMemberUserIDs := make([]string, 0)
	allMemberUserIDs = append(allMemberUserIDs, ownerUserID)
	allMemberUserIDs = append(allMemberUserIDs, adminUserIDs...)
	allMemberUserIDs = append(allMemberUserIDs, memberUserIDs...)
	// 去重
	allMemberUserIDs = datautil.Distinct(allMemberUserIDs)

	// 为所有群成员创建会话记录
	if len(allMemberUserIDs) > 0 {
		if err := m.conversationClient.CreateGroupChatConversations(c, groupID, allMemberUserIDs); err != nil {
			log.ZWarn(c, "CreateMeeting: failed to create group chat conversations", err, "groupID", groupID, "userIDs", allMemberUserIDs)
			// 不阻断流程，继续创建会议，只是会话可能不会立即显示
		}
	}

	// 根据评论开关设置群的禁言状态
	// 如果开启评论，取消群禁言；如果关闭评论，禁言群
	if req.EnableComment {
		// 开启评论，取消群禁言
		_, err := m.groupClient.GroupClient.CancelMuteGroup(c, &group.CancelMuteGroupReq{
			GroupID: groupID,
		})
		if err != nil {
			log.ZWarn(c, "CreateMeeting: failed to cancel mute group", err, "groupID", groupID)
			// 不阻断流程，继续创建会议
		}
	} else {
		// 关闭评论，禁言群
		_, err := m.groupClient.GroupClient.MuteGroup(c, &group.MuteGroupReq{
			GroupID: groupID,
		})
		if err != nil {
			log.ZWarn(c, "CreateMeeting: failed to mute group", err, "groupID", groupID)
			// 不阻断流程，继续创建会议
		}
	}

	// 创建会议记录
	meeting := &model.Meeting{
		MeetingID:      meetingID,
		Subject:        req.Subject,
		CoverURL:       req.CoverURL,
		ScheduledTime:  scheduledTime,
		Status:         model.MeetingStatusScheduled,
		CreatorUserID:  creatorUserID,
		Description:    req.Description,
		Duration:       req.Duration,
		EstimatedCount: req.EstimatedCount,
		EnableMic:      req.EnableMic,
		EnableComment:  req.EnableComment,
		AnchorUserIDs:  req.AnchorUserIDs,
		GroupID:        groupID,
		CheckInCount:   0,        // 初始化签到统计为0
		Password:       password, // 会议密码
	}

	if err := m.meetingDB.Create(c, meeting); err != nil {
		log.ZError(c, "CreateMeeting: failed to create meeting", err, "meetingID", meetingID)
		// 如果创建会议失败，尝试删除已创建的群聊，避免孤立群
		if groupID != "" {
			if dismissErr := m.groupClient.DismissGroup(c, groupID, true); dismissErr != nil {
				log.ZWarn(c, "CreateMeeting: failed to rollback created group", dismissErr, "meetingID", meetingID, "groupID", groupID)
			}
		}
		apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("failed to create meeting"))
		return
	}

	// 加载主播信息
	anchorUsers := m.loadAnchorUsers(c, meeting.AnchorUserIDs)

	// 转换为响应格式
	resp.MeetingInfo = &apistruct.MeetingInfo{
		MeetingID:      meeting.MeetingID,
		Subject:        meeting.Subject,
		CoverURL:       meeting.CoverURL,
		ScheduledTime:  meeting.ScheduledTime.UnixMilli(),
		Status:         meeting.Status,
		CreatorUserID:  meeting.CreatorUserID,
		Description:    meeting.Description,
		Duration:       meeting.Duration,
		EstimatedCount: meeting.EstimatedCount,
		EnableMic:      meeting.EnableMic,
		EnableComment:  meeting.EnableComment,
		AnchorUserIDs:  meeting.AnchorUserIDs,
		AnchorUsers:    anchorUsers,
		CreateTime:     meeting.CreateTime.UnixMilli(),
		UpdateTime:     meeting.UpdateTime.UnixMilli(),
		Ex:             meeting.Ex,
		GroupID:        meeting.GroupID,
		CheckInCount:   meeting.CheckInCount,
		Password:       meeting.Password,
	}
	resp.GroupID = groupID

	// 将会议群ID添加到webhook配置的attentionIds中
	if m.systemConfigDB != nil && groupID != "" {
		if err := webhook.UpdateAttentionIds(c, m.systemConfigDB, groupID, true); err != nil {
			log.ZWarn(c, "CreateMeeting: failed to add groupID to webhook attentionIds", err, "meetingID", meetingID, "groupID", groupID)
		}
	}

	log.ZInfo(c, "CreateMeeting: success", "meetingID", meetingID, "groupID", groupID, "creatorUserID", creatorUserID)
	apiresp.GinSuccess(c, resp)
}

// UpdateMeeting 更新会议（可以单独编辑某个字段）
func (m *MeetingApi) UpdateMeeting(c *gin.Context) {
	var (
		req  apistruct.UpdateMeetingReq
		resp apistruct.UpdateMeetingResp
	)
	if err := c.BindJSON(&req); err != nil {
		apiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap())
		return
	}

	// 获取当前用户ID
	opUserID := mcontext.GetOpUserID(c)

	// 查询会议是否存在
	meeting, err := m.meetingDB.Take(c, req.MeetingID)
	if err != nil {
		if errs.ErrRecordNotFound.Is(err) {
			apiresp.GinError(c, errs.ErrArgs.WrapMsg("meeting not found"))
			return
		}
		log.ZError(c, "UpdateMeeting: failed to get meeting", err, "meetingID", req.MeetingID)
		apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("failed to get meeting"))
		return
	}

	// 验证权限（只有创建者可以修改）
	if meeting.CreatorUserID != opUserID {
		apiresp.GinError(c, errs.ErrNoPermission.WrapMsg("only creator can update meeting"))
		return
	}

	// 已结束或已取消的会议不允许修改
	if meeting.Status == model.MeetingStatusFinished || meeting.Status == model.MeetingStatusCancelled {
		apiresp.GinError(c, errs.ErrArgs.WrapMsg("cannot update finished or cancelled meeting"))
		return
	}

	// 构建更新数据（只更新提供的字段）
	updateData := make(map[string]any)
	if req.Subject != "" {
		updateData["subject"] = req.Subject
		// 如果更新了主题，同步更新群聊名称
		if meeting.GroupID != "" {
			groupName := fmt.Sprintf("会议群-%s", req.Subject)
			_, err := m.groupClient.GroupClient.SetGroupInfo(c, &group.SetGroupInfoReq{
				GroupInfoForSet: &sdkws.GroupInfoForSet{
					GroupID:   meeting.GroupID,
					GroupName: groupName,
				},
			})
			if err != nil {
				log.ZWarn(c, "UpdateMeeting: failed to update group name", err, "groupID", meeting.GroupID)
			}
		}
	}
	if req.CoverURL != "" {
		updateData["cover_url"] = req.CoverURL
		// 如果更新了封面，同步更新群聊头像
		if meeting.GroupID != "" {
			_, err := m.groupClient.GroupClient.SetGroupInfo(c, &group.SetGroupInfoReq{
				GroupInfoForSet: &sdkws.GroupInfoForSet{
					GroupID: meeting.GroupID,
					FaceURL: req.CoverURL,
				},
			})
			if err != nil {
				log.ZWarn(c, "UpdateMeeting: failed to update group face", err, "groupID", meeting.GroupID)
			}
		}
	}
	if req.ScheduledTime > 0 {
		scheduledTime := time.UnixMilli(req.ScheduledTime)
		// 验证预约时间不能早于当前时间
		if scheduledTime.Before(time.Now()) {
			apiresp.GinError(c, errs.ErrArgs.WrapMsg("scheduled time cannot be earlier than current time"))
			return
		}
		updateData["scheduled_time"] = scheduledTime
	}
	if req.Status > 0 {
		// 验证状态值
		if req.Status < 1 || req.Status > 4 {
			apiresp.GinError(c, errs.ErrArgs.WrapMsg("invalid status, must be 1-4"))
			return
		}
		updateData["status"] = req.Status
	}
	if req.Description != "" {
		updateData["description"] = req.Description
	}
	if req.Duration > 0 {
		updateData["duration"] = req.Duration
	}
	if req.EstimatedCount > 0 {
		updateData["estimated_count"] = req.EstimatedCount
	}
	if req.EnableMic != nil {
		updateData["enable_mic"] = *req.EnableMic
	}
	if req.EnableComment != nil {
		updateData["enable_comment"] = *req.EnableComment
		// 如果更新了评论开关，同步更新群的禁言状态
		if meeting.GroupID != "" {
			if *req.EnableComment {
				// 开启评论，取消群禁言
				_, err := m.groupClient.GroupClient.CancelMuteGroup(c, &group.CancelMuteGroupReq{
					GroupID: meeting.GroupID,
				})
				if err != nil {
					log.ZWarn(c, "UpdateMeeting: failed to cancel mute group", err, "groupID", meeting.GroupID)
					// 不阻断流程，继续更新会议
				}
			} else {
				// 关闭评论，禁言群
				_, err := m.groupClient.GroupClient.MuteGroup(c, &group.MuteGroupReq{
					GroupID: meeting.GroupID,
				})
				if err != nil {
					log.ZWarn(c, "UpdateMeeting: failed to mute group", err, "groupID", meeting.GroupID)
					// 不阻断流程，继续更新会议
				}
			}
		}
	}
	if req.AnchorUserIDs != nil {
		updateData["anchor_user_ids"] = req.AnchorUserIDs
		// 如果更新了主播列表，需要同步更新群的管理员和群主
		if meeting.GroupID != "" {
			if err := m.updateGroupAnchors(c, meeting.GroupID, req.AnchorUserIDs); err != nil {
				log.ZWarn(c, "UpdateMeeting: failed to update group anchors", err, "groupID", meeting.GroupID, "anchorUserIDs", req.AnchorUserIDs)
				// 不阻断流程，继续更新会议
			}
		}
	}
	if req.Password != nil {
		password := *req.Password
		// 验证密码格式：必须是6位数字
		if password != "" {
			if len(password) != 6 {
				apiresp.GinError(c, errs.ErrArgs.WrapMsg("password must be 6 digits"))
				return
			}
			for _, char := range password {
				if char < '0' || char > '9' {
					apiresp.GinError(c, errs.ErrArgs.WrapMsg("password must be 6 digits"))
					return
				}
			}
		}
		updateData["password"] = password
	}
	if req.Ex != "" {
		updateData["ex"] = req.Ex
	}

	if len(updateData) == 0 {
		apiresp.GinError(c, errs.ErrArgs.WrapMsg("no fields to update"))
		return
	}

	// 更新会议
	if err := m.meetingDB.Update(c, req.MeetingID, updateData); err != nil {
		log.ZError(c, "UpdateMeeting: failed to update meeting", err, "meetingID", req.MeetingID)
		apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("failed to update meeting"))
		return
	}

	// 重新查询会议信息
	meeting, err = m.meetingDB.Take(c, req.MeetingID)
	if err != nil {
		log.ZError(c, "UpdateMeeting: failed to get updated meeting", err, "meetingID", req.MeetingID)
		apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("failed to get updated meeting"))
		return
	}

	// 加载主播信息
	anchorUsers := m.loadAnchorUsers(c, meeting.AnchorUserIDs)

	// 转换为响应格式
	resp.MeetingInfo = &apistruct.MeetingInfo{
		MeetingID:      meeting.MeetingID,
		Subject:        meeting.Subject,
		CoverURL:       meeting.CoverURL,
		ScheduledTime:  meeting.ScheduledTime.UnixMilli(),
		Status:         meeting.Status,
		CreatorUserID:  meeting.CreatorUserID,
		Description:    meeting.Description,
		Duration:       meeting.Duration,
		EstimatedCount: meeting.EstimatedCount,
		EnableMic:      meeting.EnableMic,
		EnableComment:  meeting.EnableComment,
		AnchorUserIDs:  meeting.AnchorUserIDs,
		AnchorUsers:    anchorUsers,
		CreateTime:     meeting.CreateTime.UnixMilli(),
		UpdateTime:     meeting.UpdateTime.UnixMilli(),
		Ex:             meeting.Ex,
		GroupID:        meeting.GroupID,
		CheckInCount:   meeting.CheckInCount,
		Password:       meeting.Password,
	}

	log.ZInfo(c, "UpdateMeeting: success", "meetingID", req.MeetingID)
	apiresp.GinSuccess(c, resp)
}

// updateGroupAnchors 更新群的主播（群主和管理员）
func (m *MeetingApi) updateGroupAnchors(ctx context.Context, groupID string, newAnchorUserIDs []string) error {
	// 获取当前群的所有成员
	currentMemberUserIDs, err := m.groupClient.GetGroupMemberUserIDs(ctx, groupID)
	if err != nil {
		return errs.WrapMsg(err, "failed to get group members")
	}

	// 构建当前成员的映射
	currentMemberMap := make(map[string]bool)
	for _, userID := range currentMemberUserIDs {
		currentMemberMap[userID] = true
	}

	// 找出不在群里的主播，先把他们拉进群
	var usersToInvite []string
	for _, anchorID := range newAnchorUserIDs {
		if !currentMemberMap[anchorID] {
			usersToInvite = append(usersToInvite, anchorID)
		}
	}

	// 邀请不在群里的主播进群
	if len(usersToInvite) > 0 {
		_, err := m.groupClient.GroupClient.InviteUserToGroup(ctx, &group.InviteUserToGroupReq{
			GroupID:        groupID,
			InvitedUserIDs: usersToInvite,
		})
		if err != nil {
			return errs.WrapMsg(err, "failed to invite anchors to group")
		}

		// 为新加入的成员创建会话记录
		if err := m.conversationClient.CreateGroupChatConversations(ctx, groupID, usersToInvite); err != nil {
			log.ZWarn(ctx, "updateGroupAnchors: failed to create conversations for new members", err, "groupID", groupID, "userIDs", usersToInvite)
			// 不阻断流程，继续执行
		}
	}

	// 如果没有主播，不需要更新群主和管理员
	if len(newAnchorUserIDs) == 0 {
		return nil
	}

	// 获取当前群主
	groupInfo, err := m.groupClient.GetGroupInfo(ctx, groupID)
	if err != nil {
		return errs.WrapMsg(err, "failed to get group info")
	}

	currentOwnerUserID := groupInfo.OwnerUserID
	newOwnerUserID := newAnchorUserIDs[0] // 第一个主播为群主
	var newAdminUserIDs []string
	if len(newAnchorUserIDs) > 1 {
		newAdminUserIDs = newAnchorUserIDs[1:] // 其他主播为管理员
	}

	// 获取所有群成员信息（包括角色）
	allMemberUserIDs := append([]string{currentOwnerUserID}, newAnchorUserIDs...)
	allMemberUserIDs = datautil.Distinct(allMemberUserIDs)
	members, err := m.groupClient.GetGroupMembersInfo(ctx, groupID, allMemberUserIDs)
	if err != nil {
		return errs.WrapMsg(err, "failed to get group members info")
	}

	// 构建成员映射
	memberMap := make(map[string]*sdkws.GroupMemberFullInfo)
	for _, member := range members {
		memberMap[member.UserID] = member
	}

	// 如果群主需要变更，先转移群主
	if currentOwnerUserID != newOwnerUserID {
		// 确保新群主在群里
		if _, exists := memberMap[newOwnerUserID]; !exists {
			return errs.ErrArgs.WrapMsg("new owner not in group")
		}

		// 获取当前群主的角色级别（用于转移后设置）
		currentOwnerMember := memberMap[currentOwnerUserID]
		var oldOwnerNewRoleLevel int32 = constant.GroupOrdinaryUsers
		if currentOwnerMember != nil {
			oldOwnerNewRoleLevel = currentOwnerMember.RoleLevel
		}

		// 如果当前群主不在新主播列表中，降级为普通成员
		anchorMap := make(map[string]bool)
		for _, anchorID := range newAnchorUserIDs {
			anchorMap[anchorID] = true
		}
		if !anchorMap[currentOwnerUserID] {
			oldOwnerNewRoleLevel = constant.GroupOrdinaryUsers
		} else {
			// 如果当前群主在新主播列表中但不是第一个，降级为管理员
			if currentOwnerUserID != newOwnerUserID {
				oldOwnerNewRoleLevel = constant.GroupAdmin
			}
		}

		// 转移群主
		_, err := m.groupClient.GroupClient.TransferGroupOwner(ctx, &group.TransferGroupOwnerReq{
			GroupID:        groupID,
			OldOwnerUserID: currentOwnerUserID,
			NewOwnerUserID: newOwnerUserID,
		})
		if err != nil {
			return errs.WrapMsg(err, "failed to transfer group owner")
		}

		// 如果旧群主需要降级，设置其角色
		if oldOwnerNewRoleLevel != constant.GroupOwner {
			_, err := m.groupClient.GroupClient.SetGroupMemberInfo(ctx, &group.SetGroupMemberInfoReq{
				Members: []*group.SetGroupMemberInfo{
					{
						GroupID:   groupID,
						UserID:    currentOwnerUserID,
						RoleLevel: &wrapperspb.Int32Value{Value: oldOwnerNewRoleLevel},
					},
				},
			})
			if err != nil {
				log.ZWarn(ctx, "updateGroupAnchors: failed to set old owner role", err, "groupID", groupID, "userID", currentOwnerUserID, "roleLevel", oldOwnerNewRoleLevel)
			}
		}
	}

	// 设置其他主播为管理员
	if len(newAdminUserIDs) > 0 {
		var membersToUpdate []*group.SetGroupMemberInfo
		for _, adminID := range newAdminUserIDs {
			// 检查该成员当前角色
			member := memberMap[adminID]
			if member == nil || member.RoleLevel != constant.GroupAdmin {
				membersToUpdate = append(membersToUpdate, &group.SetGroupMemberInfo{
					GroupID:   groupID,
					UserID:    adminID,
					RoleLevel: &wrapperspb.Int32Value{Value: constant.GroupAdmin},
				})
			}
		}

		if len(membersToUpdate) > 0 {
			_, err := m.groupClient.GroupClient.SetGroupMemberInfo(ctx, &group.SetGroupMemberInfoReq{
				Members: membersToUpdate,
			})
			if err != nil {
				log.ZWarn(ctx, "updateGroupAnchors: failed to set admins", err, "groupID", groupID, "adminUserIDs", newAdminUserIDs)
			}
		}
	}

	// 如果原来的管理员不在新主播列表中，降级为普通成员
	// 获取所有当前管理员
	var currentAdminsToDemote []string
	for userID, member := range memberMap {
		if member.RoleLevel == constant.GroupAdmin {
			// 检查是否在新主播列表中
			isInNewAnchors := false
			for _, anchorID := range newAnchorUserIDs {
				if anchorID == userID {
					isInNewAnchors = true
					break
				}
			}
			// 如果不在新主播列表中，需要降级
			if !isInNewAnchors {
				currentAdminsToDemote = append(currentAdminsToDemote, userID)
			}
		}
	}

	// 降级不在新主播列表中的管理员
	if len(currentAdminsToDemote) > 0 {
		var membersToDemote []*group.SetGroupMemberInfo
		for _, userID := range currentAdminsToDemote {
			membersToDemote = append(membersToDemote, &group.SetGroupMemberInfo{
				GroupID:   groupID,
				UserID:    userID,
				RoleLevel: &wrapperspb.Int32Value{Value: constant.GroupOrdinaryUsers},
			})
		}

		_, err := m.groupClient.GroupClient.SetGroupMemberInfo(ctx, &group.SetGroupMemberInfoReq{
			Members: membersToDemote,
		})
		if err != nil {
			log.ZWarn(ctx, "updateGroupAnchors: failed to demote admins", err, "groupID", groupID, "userIDs", currentAdminsToDemote)
		}
	}

	return nil
}

// GetMeetings 获取会议列表
func (m *MeetingApi) GetMeetings(c *gin.Context) {
	var (
		req  apistruct.GetMeetingsReq
		resp apistruct.GetMeetingsResp
	)
	if err := c.BindJSON(&req); err != nil {
		apiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap())
		return
	}

	// 设置默认分页参数
	if req.Pagination.PageNumber <= 0 {
		req.Pagination.PageNumber = 1
	}
	if req.Pagination.ShowNumber <= 0 {
		req.Pagination.ShowNumber = 20
	}

	// 创建分页对象
	pagination := &meetingPaginationWrapper{
		pageNumber: req.Pagination.PageNumber,
		showNumber: req.Pagination.ShowNumber,
	}

	var total int64
	var meetings []*model.Meeting
	var err error

	// 根据查询条件查询会议
	if req.CreatorUserID != "" {
		// 按创建者查询
		total, meetings, err = m.meetingDB.FindByCreator(c, req.CreatorUserID, pagination)
	} else if req.Status > 0 {
		// 按状态查询
		total, meetings, err = m.meetingDB.FindByStatus(c, req.Status, pagination)
	} else if req.Keyword != "" {
		// 按关键词搜索
		total, meetings, err = m.meetingDB.Search(c, req.Keyword, pagination)
	} else if req.StartTime > 0 && req.EndTime > 0 {
		// 按时间范围查询
		total, meetings, err = m.meetingDB.FindByScheduledTimeRange(c, req.StartTime, req.EndTime, pagination)
	} else {
		// 查询所有会议
		total, meetings, err = m.meetingDB.FindAll(c, pagination)
	}

	if err != nil {
		log.ZError(c, "GetMeetings: failed to find meetings", err)
		apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("failed to find meetings"))
		return
	}

	// 收集所有主播ID
	allAnchorUserIDs := make([]string, 0)
	for _, meeting := range meetings {
		allAnchorUserIDs = append(allAnchorUserIDs, meeting.AnchorUserIDs...)
	}
	// 去重
	allAnchorUserIDs = datautil.Distinct(allAnchorUserIDs)

	// 批量获取主播用户信息
	anchorUserMap := make(map[string]*sdkws.UserInfo)
	if len(allAnchorUserIDs) > 0 {
		anchorUsers, err := m.userClient.GetUsersInfo(c, allAnchorUserIDs)
		if err != nil {
			log.ZWarn(c, "GetMeetings: failed to get anchor users info", err, "anchorUserIDs", allAnchorUserIDs)
			// 不阻断流程，继续返回会议列表，只是没有主播信息
		} else {
			for _, user := range anchorUsers {
				anchorUserMap[user.UserID] = user
			}
		}
	}

	// 转换为响应格式
	meetingInfos := make([]*apistruct.MeetingInfo, 0, len(meetings))
	for _, meeting := range meetings {
		// 获取该会议的主播信息
		anchorUsers := make([]*sdkws.UserInfo, 0, len(meeting.AnchorUserIDs))
		for _, anchorID := range meeting.AnchorUserIDs {
			if user, ok := anchorUserMap[anchorID]; ok {
				anchorUsers = append(anchorUsers, user)
			}
		}

		meetingInfos = append(meetingInfos, &apistruct.MeetingInfo{
			MeetingID:      meeting.MeetingID,
			Subject:        meeting.Subject,
			CoverURL:       meeting.CoverURL,
			ScheduledTime:  meeting.ScheduledTime.UnixMilli(),
			Status:         meeting.Status,
			CreatorUserID:  meeting.CreatorUserID,
			Description:    meeting.Description,
			Duration:       meeting.Duration,
			EstimatedCount: meeting.EstimatedCount,
			EnableMic:      meeting.EnableMic,
			EnableComment:  meeting.EnableComment,
			AnchorUserIDs:  meeting.AnchorUserIDs,
			AnchorUsers:    anchorUsers,
			CreateTime:     meeting.CreateTime.UnixMilli(),
			UpdateTime:     meeting.UpdateTime.UnixMilli(),
			Ex:             meeting.Ex,
			GroupID:        meeting.GroupID,
			CheckInCount:   meeting.CheckInCount,
			Password:       meeting.Password,
		})
	}

	resp.Total = total
	resp.Meetings = meetingInfos

	log.ZInfo(c, "GetMeetings: success", "total", total, "count", len(meetingInfos))
	apiresp.GinSuccess(c, resp)
}

// DeleteMeeting 删除会议
func (m *MeetingApi) DeleteMeeting(c *gin.Context) {
	var (
		req  apistruct.DeleteMeetingReq
		resp apistruct.DeleteMeetingResp
	)
	if err := c.BindJSON(&req); err != nil {
		apiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap())
		return
	}

	// 获取当前用户ID
	opUserID := mcontext.GetOpUserID(c)

	// 查询会议是否存在
	meeting, err := m.meetingDB.Take(c, req.MeetingID)
	if err != nil {
		if errs.ErrRecordNotFound.Is(err) {
			apiresp.GinError(c, errs.ErrArgs.WrapMsg("meeting not found"))
			return
		}
		log.ZError(c, "DeleteMeeting: failed to get meeting", err, "meetingID", req.MeetingID)
		apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("failed to get meeting"))
		return
	}

	// 验证权限（只有创建者可以删除）
	if meeting.CreatorUserID != opUserID {
		apiresp.GinError(c, errs.ErrNoPermission.WrapMsg("only creator can delete meeting"))
		return
	}

	// 如果有关联的群聊，先解散群聊
	if meeting.GroupID != "" {
		err := m.groupClient.DismissGroup(c, meeting.GroupID, false)
		if err != nil {
			log.ZWarn(c, "DeleteMeeting: failed to dismiss group", err, "meetingID", req.MeetingID, "groupID", meeting.GroupID)
			// 不阻断流程，继续删除会议
		} else {
			log.ZInfo(c, "DeleteMeeting: successfully dismissed group", "meetingID", req.MeetingID, "groupID", meeting.GroupID)
		}

		// 从webhook配置的attentionIds中移除会议群ID
		if m.systemConfigDB != nil {
			if err := webhook.UpdateAttentionIds(c, m.systemConfigDB, meeting.GroupID, false); err != nil {
				log.ZWarn(c, "DeleteMeeting: failed to remove groupID from webhook attentionIds", err, "meetingID", req.MeetingID, "groupID", meeting.GroupID)
			}
		}
	}

	// 删除会议
	if err := m.meetingDB.Delete(c, req.MeetingID); err != nil {
		log.ZError(c, "DeleteMeeting: failed to delete meeting", err, "meetingID", req.MeetingID)
		apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("failed to delete meeting"))
		return
	}

	log.ZInfo(c, "DeleteMeeting: success", "meetingID", req.MeetingID)
	apiresp.GinSuccess(c, resp)
}

// GetMeetingPublic 获取会议信息（用户端）
func (m *MeetingApi) GetMeetingPublic(c *gin.Context) {
	var (
		req  apistruct.GetMeetingReq
		resp apistruct.GetMeetingResp
	)
	if err := c.BindJSON(&req); err != nil {
		apiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap())
		return
	}

	// 查询会议
	meeting, err := m.meetingDB.Take(c, req.MeetingID)
	if err != nil {
		if m.IsNotFound(err) {
			apiresp.GinError(c, errs.ErrArgs.WrapMsg("meeting not found"))
			return
		}
		log.ZError(c, "GetMeetingPublic: failed to get meeting", err, "meetingID", req.MeetingID)
		apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("failed to get meeting"))
		return
	}

	// 用户端只能查看已预约（1）和进行中（2）的会议
	if meeting.Status != model.MeetingStatusScheduled && meeting.Status != model.MeetingStatusOngoing {
		apiresp.GinError(c, errs.ErrArgs.WrapMsg("meeting not available"))
		return
	}

	// 加载主播信息
	anchorUsers := m.loadAnchorUsers(c, meeting.AnchorUserIDs)

	// 转换为用户端响应格式（过滤管理字段）
	resp.MeetingInfo = &apistruct.MeetingPublicInfo{
		MeetingID:     meeting.MeetingID,
		Subject:       meeting.Subject,
		CoverURL:      meeting.CoverURL,
		ScheduledTime: meeting.ScheduledTime.UnixMilli(),
		Status:        meeting.Status,
		Description:   meeting.Description,
		Duration:      meeting.Duration,
		EnableMic:     meeting.EnableMic,
		EnableComment: meeting.EnableComment,
		AnchorUsers:   anchorUsers,
		GroupID:       meeting.GroupID,
		CheckInCount:  meeting.CheckInCount,
		Password:      meeting.Password,
	}

	apiresp.GinSuccess(c, resp)
}

// GetMeetingsPublic 获取会议列表（用户端）
func (m *MeetingApi) GetMeetingsPublic(c *gin.Context) {
	var (
		req  apistruct.GetMeetingsPublicReq
		resp apistruct.GetMeetingsPublicResp
	)
	if err := c.BindJSON(&req); err != nil {
		apiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap())
		return
	}

	// 设置默认分页参数
	if req.Pagination.PageNumber <= 0 {
		req.Pagination.PageNumber = 1
	}
	if req.Pagination.ShowNumber <= 0 {
		req.Pagination.ShowNumber = 20
	}

	// 创建分页对象
	pagination := &meetingPaginationWrapper{
		pageNumber: req.Pagination.PageNumber,
		showNumber: req.Pagination.ShowNumber,
	}

	var total int64
	var meetings []*model.Meeting
	var err error

	// 用户端只显示已预约（1）和进行中（2）的会议
	// 如果指定了状态，验证是否为允许的状态
	if req.Status > 0 {
		if req.Status != model.MeetingStatusScheduled && req.Status != model.MeetingStatusOngoing {
			apiresp.GinError(c, errs.ErrArgs.WrapMsg("user can only view scheduled (1) or ongoing (2) meetings"))
			return
		}
		// 按状态查询
		total, meetings, err = m.meetingDB.FindByStatus(c, req.Status, pagination)
	} else {
		// 对于其他查询方式，先查询再过滤状态
		var allMeetings []*model.Meeting
		if req.Keyword != "" {
			// 按关键词搜索
			total, allMeetings, err = m.meetingDB.Search(c, req.Keyword, pagination)
		} else if req.StartTime > 0 && req.EndTime > 0 {
			// 按时间范围查询
			total, allMeetings, err = m.meetingDB.FindByScheduledTimeRange(c, req.StartTime, req.EndTime, pagination)
		} else {
			// 查询所有会议
			total, allMeetings, err = m.meetingDB.FindAll(c, pagination)
		}

		if err == nil {
			// 过滤只保留已预约和进行中的会议
			meetings = make([]*model.Meeting, 0, len(allMeetings))
			for _, meeting := range allMeetings {
				if meeting.Status == model.MeetingStatusScheduled || meeting.Status == model.MeetingStatusOngoing {
					meetings = append(meetings, meeting)
				}
			}
			total = int64(len(meetings))
		}
	}

	if err != nil {
		log.ZError(c, "GetMeetingsPublic: failed to find meetings", err)
		apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("failed to find meetings"))
		return
	}

	// 收集所有主播ID
	allAnchorUserIDs := make([]string, 0)
	for _, meeting := range meetings {
		allAnchorUserIDs = append(allAnchorUserIDs, meeting.AnchorUserIDs...)
	}
	// 去重
	allAnchorUserIDs = datautil.Distinct(allAnchorUserIDs)

	// 批量获取主播用户信息
	anchorUserMap := make(map[string]*sdkws.UserInfo)
	if len(allAnchorUserIDs) > 0 {
		anchorUsers, err := m.userClient.GetUsersInfo(c, allAnchorUserIDs)
		if err != nil {
			log.ZWarn(c, "GetMeetingsPublic: failed to get anchor users info", err, "anchorUserIDs", allAnchorUserIDs)
			// 不阻断流程，继续返回会议列表，只是没有主播信息
		} else {
			for _, user := range anchorUsers {
				anchorUserMap[user.UserID] = user
			}
		}
	}

	// 转换为用户端响应格式（过滤管理字段）
	meetingInfos := make([]*apistruct.MeetingPublicInfo, 0, len(meetings))
	for _, meeting := range meetings {
		// 获取该会议的主播信息
		anchorUsers := make([]*sdkws.UserInfo, 0, len(meeting.AnchorUserIDs))
		for _, anchorID := range meeting.AnchorUserIDs {
			if user, ok := anchorUserMap[anchorID]; ok {
				anchorUsers = append(anchorUsers, user)
			}
		}

		meetingInfos = append(meetingInfos, &apistruct.MeetingPublicInfo{
			MeetingID:     meeting.MeetingID,
			Subject:       meeting.Subject,
			CoverURL:      meeting.CoverURL,
			ScheduledTime: meeting.ScheduledTime.UnixMilli(),
			Status:        meeting.Status,
			Description:   meeting.Description,
			Duration:      meeting.Duration,
			EnableMic:     meeting.EnableMic,
			EnableComment: meeting.EnableComment,
			AnchorUsers:   anchorUsers,
			GroupID:       meeting.GroupID,
			CheckInCount:  meeting.CheckInCount,
			Password:      meeting.Password,
		})
	}

	resp.Total = total
	resp.Meetings = meetingInfos

	log.ZInfo(c, "GetMeetingsPublic: success", "total", total, "count", len(meetingInfos))
	apiresp.GinSuccess(c, resp)
}

// CheckInMeeting 会议签到
func (m *MeetingApi) CheckInMeeting(c *gin.Context) {
	var (
		req  apistruct.CheckInMeetingReq
		resp apistruct.CheckInMeetingResp
	)
	if err := c.BindJSON(&req); err != nil {
		apiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap())
		return
	}

	// 获取当前用户ID
	userID := mcontext.GetOpUserID(c)
	if userID == "" {
		apiresp.GinError(c, errs.ErrArgs.WrapMsg("user id not found"))
		return
	}

	// 验证会议是否存在
	meeting, err := m.meetingDB.Take(c, req.MeetingID)
	if err != nil {
		if m.IsNotFound(err) {
			apiresp.GinError(c, errs.ErrArgs.WrapMsg("meeting not found"))
			return
		}
		log.ZError(c, "CheckInMeeting: failed to get meeting", err, "meetingID", req.MeetingID)
		apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("failed to get meeting"))
		return
	}

	// 验证会议状态（只能对已预约和进行中的会议签到）
	if meeting.Status != model.MeetingStatusScheduled && meeting.Status != model.MeetingStatusOngoing {
		apiresp.GinError(c, errs.ErrArgs.WrapMsg("can only check in for scheduled or ongoing meetings"))
		return
	}

	// 检查是否已经签到
	existingCheckIn, err := m.meetingCheckInDB.FindByUserAndMeetingID(c, userID, req.MeetingID)
	if err == nil && existingCheckIn != nil {
		apiresp.GinError(c, errs.ErrArgs.WrapMsg("already checked in"))
		return
	}
	if err != nil && !m.IsNotFound(err) {
		log.ZError(c, "CheckInMeeting: failed to check existing check-in", err, "userID", userID, "meetingID", req.MeetingID)
		apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("failed to check existing check-in"))
		return
	}

	// 生成签到ID
	checkInID := idutil.GetMsgIDByMD5(userID + req.MeetingID + timeutil.GetCurrentTimeFormatted())

	// 创建签到记录
	checkIn := &model.MeetingCheckIn{
		CheckInID:   checkInID,
		MeetingID:   req.MeetingID,
		UserID:      userID,
		CheckInTime: time.Now(),
	}

	if err := m.meetingCheckInDB.Create(c, checkIn); err != nil {
		log.ZError(c, "CheckInMeeting: failed to create check-in", err, "meetingID", req.MeetingID, "userID", userID)
		apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("failed to create check-in"))
		return
	}

	// 更新会议的签到统计
	checkInCount, err := m.meetingCheckInDB.CountByMeetingID(c, req.MeetingID)
	if err != nil {
		log.ZWarn(c, "CheckInMeeting: failed to count check-ins", err, "meetingID", req.MeetingID)
		// 不阻断流程，继续返回成功
	} else {
		if err := m.meetingDB.Update(c, req.MeetingID, map[string]any{"check_in_count": int32(checkInCount)}); err != nil {
			log.ZWarn(c, "CheckInMeeting: failed to update meeting check-in count", err, "meetingID", req.MeetingID)
			// 不阻断流程，继续返回成功
		}
	}

	resp.CheckInID = checkInID
	resp.CheckInTime = checkIn.CheckInTime.UnixMilli()

	log.ZInfo(c, "CheckInMeeting: success", "meetingID", req.MeetingID, "userID", userID, "checkInID", checkInID)
	apiresp.GinSuccess(c, resp)
}

// GetMeetingCheckIns 获取会议签到列表
func (m *MeetingApi) GetMeetingCheckIns(c *gin.Context) {
	var (
		req  apistruct.GetMeetingCheckInsReq
		resp apistruct.GetMeetingCheckInsResp
	)
	if err := c.BindJSON(&req); err != nil {
		apiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap())
		return
	}

	// 设置默认分页参数
	if req.Pagination.PageNumber <= 0 {
		req.Pagination.PageNumber = 1
	}
	if req.Pagination.ShowNumber <= 0 {
		req.Pagination.ShowNumber = 20
	}

	// 创建分页对象
	pagination := &meetingPaginationWrapper{
		pageNumber: req.Pagination.PageNumber,
		showNumber: req.Pagination.ShowNumber,
	}

	// 查询签到列表
	total, checkIns, err := m.meetingCheckInDB.FindByMeetingID(c, req.MeetingID, pagination)
	if err != nil {
		log.ZError(c, "GetMeetingCheckIns: failed to find check-ins", err, "meetingID", req.MeetingID)
		apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("failed to find check-ins"))
		return
	}

	// 收集所有用户ID
	userIDs := make([]string, 0, len(checkIns))
	for _, checkIn := range checkIns {
		userIDs = append(userIDs, checkIn.UserID)
	}

	// 批量获取用户信息
	userMap := make(map[string]*sdkws.UserInfo)
	if len(userIDs) > 0 {
		users, err := m.userClient.GetUsersInfo(c, userIDs)
		if err != nil {
			log.ZWarn(c, "GetMeetingCheckIns: failed to get users info", err, "userIDs", userIDs)
			// 不阻断流程，继续返回签到列表，只是没有用户信息
		} else {
			for _, user := range users {
				userMap[user.UserID] = user
			}
		}
	}

	// 转换为响应格式
	checkInInfos := make([]*apistruct.MeetingCheckInInfo, 0, len(checkIns))
	for _, checkIn := range checkIns {
		checkInInfo := &apistruct.MeetingCheckInInfo{
			CheckInID:   checkIn.CheckInID,
			MeetingID:   checkIn.MeetingID,
			UserID:      checkIn.UserID,
			CheckInTime: checkIn.CheckInTime.UnixMilli(),
		}
		if user, ok := userMap[checkIn.UserID]; ok {
			checkInInfo.UserInfo = user
		}
		checkInInfos = append(checkInInfos, checkInInfo)
	}

	resp.Total = total
	resp.CheckIns = checkInInfos

	log.ZInfo(c, "GetMeetingCheckIns: success", "meetingID", req.MeetingID, "total", total, "count", len(checkInInfos))
	apiresp.GinSuccess(c, resp)
}

// GetMeetingCheckInStats 获取会议签到统计
func (m *MeetingApi) GetMeetingCheckInStats(c *gin.Context) {
	var (
		req  apistruct.GetMeetingCheckInStatsReq
		resp apistruct.GetMeetingCheckInStatsResp
	)
	if err := c.BindJSON(&req); err != nil {
		apiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap())
		return
	}

	// 查询签到统计
	checkInCount, err := m.meetingCheckInDB.CountByMeetingID(c, req.MeetingID)
	if err != nil {
		log.ZError(c, "GetMeetingCheckInStats: failed to count check-ins", err, "meetingID", req.MeetingID)
		apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("failed to count check-ins"))
		return
	}

	resp.MeetingID = req.MeetingID
	resp.CheckInCount = checkInCount

	log.ZInfo(c, "GetMeetingCheckInStats: success", "meetingID", req.MeetingID, "checkInCount", checkInCount)
	apiresp.GinSuccess(c, resp)
}

// CheckUserCheckIn 检查用户是否已签到
func (m *MeetingApi) CheckUserCheckIn(c *gin.Context) {
	var (
		req  apistruct.CheckUserCheckInReq
		resp apistruct.CheckUserCheckInResp
	)
	if err := c.BindJSON(&req); err != nil {
		apiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap())
		return
	}

	// 获取当前用户ID
	userID := mcontext.GetOpUserID(c)
	if userID == "" {
		apiresp.GinError(c, errs.ErrArgs.WrapMsg("user id not found"))
		return
	}

	// 查询用户是否已签到
	checkIn, err := m.meetingCheckInDB.FindByUserAndMeetingID(c, userID, req.MeetingID)
	if err != nil {
		if m.IsNotFound(err) {
			// 未签到
			resp.IsCheckedIn = false
			log.ZInfo(c, "CheckUserCheckIn: user not checked in", "meetingID", req.MeetingID, "userID", userID)
			apiresp.GinSuccess(c, resp)
			return
		}
		log.ZError(c, "CheckUserCheckIn: failed to find check-in", err, "meetingID", req.MeetingID, "userID", userID)
		apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("failed to find check-in"))
		return
	}

	// 已签到，获取用户信息
	var userInfo *sdkws.UserInfo
	users, err := m.userClient.GetUsersInfo(c, []string{userID})
	if err != nil {
		log.ZWarn(c, "CheckUserCheckIn: failed to get user info", err, "userID", userID)
		// 不阻断流程，继续返回签到信息，只是没有用户详细信息
	} else if len(users) > 0 {
		userInfo = users[0]
	}

	resp.IsCheckedIn = true
	resp.CheckInInfo = &apistruct.MeetingCheckInInfo{
		CheckInID:   checkIn.CheckInID,
		MeetingID:   checkIn.MeetingID,
		UserID:      checkIn.UserID,
		CheckInTime: checkIn.CheckInTime.UnixMilli(),
		UserInfo:    userInfo,
	}

	log.ZInfo(c, "CheckUserCheckIn: success", "meetingID", req.MeetingID, "userID", userID, "isCheckedIn", true)
	apiresp.GinSuccess(c, resp)
}
