// Package controller 提供 meeting 模块的 HTTP 接口
package controller

import (
	"errors"
	"strconv"
	"strings"

	authSvc "github.com/echochat/backend/app/auth/service"
	"github.com/echochat/backend/app/constants"
	"github.com/echochat/backend/app/dto"
	"github.com/echochat/backend/app/meeting/model"
	"github.com/echochat/backend/app/meeting/service"
	"github.com/echochat/backend/config"
	"github.com/echochat/backend/pkg/logs"
	"github.com/echochat/backend/pkg/middleware"
	"github.com/echochat/backend/pkg/utils"
	"github.com/gin-gonic/gin"
	"go.uber.org/zap"
)

// MeetingController 会议 REST 控制器
// Task 5 完成：填充 12 个接口的请求解析、业务调用、DTO 转换、错误码映射
type MeetingController struct {
	meetingService *service.MeetingService
	meetingConfig  *config.MeetingConfig
	authService    *authSvc.AuthService
}

// NewMeetingController 创建 MeetingController 实例
func NewMeetingController(meetingService *service.MeetingService, meetingConfig *config.MeetingConfig, authService *authSvc.AuthService) *MeetingController {
	return &MeetingController{meetingService: meetingService, meetingConfig: meetingConfig, authService: authService}
}

type cloudLawCreateMeetingRequest struct {
	CallID       string `json:"callId"`
	CallerID     int64  `json:"callerId"`
	CallerPhone  string `json:"callerPhone"`
	CallerName   string `json:"callerName"`
	ReceiverID   int64  `json:"receiverId"`
	ReceiverType string `json:"receiverType"`
	Title        string `json:"title"`
	CallType     string `json:"callType"`
}

type cloudLawJoinMeetingRequest struct {
	ReceiverID    int64  `json:"receiverId"`
	ReceiverType  string `json:"receiverType"`
	ReceiverPhone string `json:"receiverPhone"`
	ReceiverName  string `json:"receiverName"`
	CallID        string `json:"callId"`
}

type cloudLawSessionRequest struct {
	Phone string `json:"phone"`
	Name  string `json:"name"`
}

func (ctl *MeetingController) requireCloudLawInternalToken(c *gin.Context) bool {
	expected := ""
	if ctl.meetingConfig != nil {
		expected = ctl.meetingConfig.CloudLawInternalToken
	}
	if expected == "" {
		utils.ResponseForbidden(c, "云律内部接口未配置")
		return false
	}
	if c.GetHeader("X-Internal-Token") != expected {
		utils.ResponseUnauthorized(c, "内部访问令牌无效")
		return false
	}
	return true
}

func cloudLawMeetingPayload(room *model.MeetingRoom, onlineCount int64) gin.H {
	return gin.H{
		"roomId":      strconv.FormatInt(room.ID, 10),
		"roomCode":    room.RoomCode,
		"code":        room.RoomCode,
		"meetingCode": room.RoomCode,
		"status":      room.Status,
		"onlineCount": onlineCount,
	}
}

// requireUserID 统一的当前用户取值，失败直接写 401 并返回 false
func requireUserID(c *gin.Context) (int64, bool) {
	userID, ok := middleware.GetCurrentUserID(c)
	if !ok {
		utils.ResponseUnauthorized(c, "无法获取当前用户信息")
		return 0, false
	}
	return userID, true
}

// handleError 将 service 层领域错误统一映射为 HTTP 响应
// 映射原则：
//   - 资源不存在 → 404
//   - 权限不足（非 host）→ 403
//   - 业务规则违反（密码错误、已满、状态冲突等）→ 400
//   - 其余未知错误 → 500
func (ctl *MeetingController) handleError(c *gin.Context, err error, fallbackMsg string) {
	switch {
	case errors.Is(err, service.ErrMeetingNotFound):
		utils.ResponseNotFound(c, err.Error())
	case errors.Is(err, service.ErrNotMeetingHost):
		utils.ResponseForbidden(c, err.Error())
	case errors.Is(err, service.ErrMediaServiceUnavailable):
		// P0-3：媒体服务不可用应作为 5xx 返回，让前端走"稍后重试"提示，而非按"用户输入错误"处理
		utils.ResponseError(c, err.Error())
	case errors.Is(err, service.ErrMeetingEnded),
		errors.Is(err, service.ErrMeetingFull),
		errors.Is(err, service.ErrMeetingPasswordReq),
		errors.Is(err, service.ErrMeetingPasswordWrong),
		errors.Is(err, service.ErrMeetingPasswordLocked),
		errors.Is(err, service.ErrNotInMeeting),
		errors.Is(err, service.ErrAlreadyInMeeting),
		errors.Is(err, service.ErrAlreadyInOtherMeeting),
		errors.Is(err, service.ErrInviteTokenInvalid),
		errors.Is(err, service.ErrRoomCodeConflict),
		errors.Is(err, service.ErrKickSelfForbidden),
		errors.Is(err, service.ErrTransferToSelf),
		errors.Is(err, service.ErrTransferTargetInvalid),
		errors.Is(err, service.ErrCloudLawCallerPhoneRequired),
		errors.Is(err, service.ErrCloudLawCallerPhoneInvalid),
		// P2-7 会议聊天服务端校验失败：内容为空 / 超长 / 触发限流
		// 当前使用 400（由 ResponseBadRequest 返回），保持与其余业务错误一致；
		// 未来若需要精细区分（如 ErrChatRateLimited → 429、ErrChatContentTooLong → 413），可拆分分支
		errors.Is(err, service.ErrChatContentEmpty),
		errors.Is(err, service.ErrChatContentTooLong),
		errors.Is(err, service.ErrChatRateLimited):
		utils.ResponseBadRequest(c, err.Error())
	default:
		logs.Warn(c.Request.Context(), "controller.meeting_controller.handleError",
			fallbackMsg, zap.Error(err))
		utils.ResponseError(c, fallbackMsg)
	}
}

// ====== DTO 转换 ======

// roomToDTO 将 model.MeetingRoom 转 DTO，host_name/avatar 交由上层按需补全
func roomToDTO(r *model.MeetingRoom, onlineCount int) *dto.MeetingRoomDTO {
	if r == nil {
		return nil
	}
	out := &dto.MeetingRoomDTO{
		ID:          r.ID,
		RoomCode:    r.RoomCode,
		Title:       r.Title,
		HostID:      r.HostID,
		Type:        r.Type,
		HasPassword: r.PasswordHash != nil && *r.PasswordHash != "",
		MaxMembers:  r.MaxMembers,
		Status:      r.Status,
		StatusLabel: constants.MeetingStatusMap[r.Status],
		Settings:    r.Settings,
		CreatedAt:   r.CreatedAt.Format("2006-01-02 15:04:05"),
		OnlineCount: onlineCount,
		EndedReason: r.EndedReason,
	}
	if r.ScheduledAt != nil {
		out.ScheduledAt = r.ScheduledAt.Format("2006-01-02 15:04:05")
	}
	if r.StartedAt != nil {
		out.StartedAt = r.StartedAt.Format("2006-01-02 15:04:05")
	}
	if r.EndedAt != nil {
		out.EndedAt = r.EndedAt.Format("2006-01-02 15:04:05")
	}
	return out
}

// participantToDTO 将 model.MeetingParticipant 转 DTO
// userMap 可选：若提供则附带 user_name / user_avatar，便于前端 MemberPanel/VideoTile 直接渲染昵称头像
func participantToDTO(p *model.MeetingParticipant, userMap map[int64]service.UserDisplayInfo) *dto.MeetingParticipantDTO {
	if p == nil {
		return nil
	}
	out := &dto.MeetingParticipantDTO{
		ID:         p.ID,
		RoomID:     p.RoomID,
		UserID:     p.UserID,
		Role:       p.Role,
		RoleLabel:  constants.MeetingRoleMap[p.Role],
		JoinedAt:   p.JoinedAt.Format("2006-01-02 15:04:05"),
		LeftReason: p.LeftReason,
		Duration:   p.Duration,
		IsActive:   p.IsActive(),
	}
	if p.LeftAt != nil {
		out.LeftAt = p.LeftAt.Format("2006-01-02 15:04:05")
	}
	if info, ok := userMap[p.UserID]; ok {
		out.UserName = info.Name
		out.UserAvatar = info.Avatar
	}
	return out
}

// chatToDTO 将 model.MeetingChat 转 DTO
// userMap 可选：若提供则附带 user_name / user_avatar，否则为空
func chatToDTO(m *model.MeetingChat, userMap map[int64]service.UserDisplayInfo) *dto.MeetingChatDTO {
	if m == nil {
		return nil
	}
	d := &dto.MeetingChatDTO{
		ID:        m.ID,
		RoomID:    m.RoomID,
		UserID:    m.UserID,
		Content:   m.Content,
		CreatedAt: m.CreatedAt.Format("2006-01-02 15:04:05"),
	}
	if info, ok := userMap[m.UserID]; ok {
		d.UserName = info.Name
		d.UserAvatar = info.Avatar
	}
	return d
}

// ====== REST API ======

func (ctl *MeetingController) CloudLawCreateMeeting(c *gin.Context) {
	if !ctl.requireCloudLawInternalToken(c) {
		return
	}
	var req cloudLawCreateMeetingRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		utils.ResponseBadRequest(c, "参数校验失败: "+err.Error())
		return
	}
	if req.CallerID <= 0 {
		utils.ResponseBadRequest(c, "callerId不能为空")
		return
	}
	req.CallerPhone = strings.TrimSpace(req.CallerPhone)
	req.CallerName = strings.TrimSpace(req.CallerName)
	if req.CallerPhone == "" {
		utils.ResponseBadRequest(c, "callerPhone不能为空")
		return
	}
	title := req.Title
	if title == "" {
		title = "云律立即连线"
	}
	hostUser, created, err := ctl.meetingService.EnsureCloudLawUserByPhone(c.Request.Context(), req.CallerPhone, req.CallerName)
	if err != nil {
		ctl.handleError(c, err, "创建云律会议主持人失败")
		return
	}
	room, _, _, err := ctl.meetingService.CreateRoomForInternal(c.Request.Context(), hostUser.ID, &dto.CreateMeetingRoomRequest{
		Title: title,
	})
	if err != nil {
		ctl.handleError(c, err, "创建云律会议失败")
		return
	}
	payload := cloudLawMeetingPayload(room, 0)
	payload["callId"] = req.CallID
	payload["callerId"] = req.CallerID
	payload["callerPhone"] = req.CallerPhone
	payload["echoChatHostUserId"] = strconv.FormatInt(hostUser.ID, 10)
	payload["echoChatHostUserCreated"] = created
	payload["receiverId"] = req.ReceiverID
	payload["receiverType"] = req.ReceiverType
	payload["callType"] = req.CallType
	utils.ResponseOK(c, payload)
}

func (ctl *MeetingController) CloudLawGetMeeting(c *gin.Context) {
	if !ctl.requireCloudLawInternalToken(c) {
		return
	}
	code := c.Param("code")
	if code == "" {
		utils.ResponseBadRequest(c, "会议号不能为空")
		return
	}
	room, _, onlineCount, err := ctl.meetingService.GetRoomForInternal(c.Request.Context(), code)
	if err != nil {
		ctl.handleError(c, err, "获取云律会议详情失败")
		return
	}
	utils.ResponseOK(c, cloudLawMeetingPayload(room, onlineCount))
}

func (ctl *MeetingController) CloudLawSession(c *gin.Context) {
	if !ctl.requireCloudLawInternalToken(c) {
		return
	}
	var req cloudLawSessionRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		utils.ResponseBadRequest(c, "参数校验失败: "+err.Error())
		return
	}
	req.Phone = strings.TrimSpace(req.Phone)
	req.Name = strings.TrimSpace(req.Name)
	if req.Phone == "" {
		utils.ResponseBadRequest(c, "phone不能为空")
		return
	}
	user, _, err := ctl.meetingService.EnsureCloudLawUserByPhone(c.Request.Context(), req.Phone, req.Name)
	if err != nil {
		ctl.handleError(c, err, "创建云律 EchoChat 用户失败")
		return
	}
	resp, err := ctl.authService.Login(c.Request.Context(), &dto.LoginRequest{
		Account:  user.Username,
		Password: service.CloudLawDefaultPassword,
	}, c.ClientIP(), constants.ClientTypeFrontend)
	if err != nil {
		utils.ResponseError(c, "创建云律 EchoChat 登录态失败")
		return
	}
	utils.ResponseOK(c, resp)
}

func (ctl *MeetingController) CloudLawJoinMeeting(c *gin.Context) {
	if !ctl.requireCloudLawInternalToken(c) {
		return
	}
	code := c.Param("code")
	if code == "" {
		utils.ResponseBadRequest(c, "会议号不能为空")
		return
	}
	var req cloudLawJoinMeetingRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		utils.ResponseBadRequest(c, "参数校验失败: "+err.Error())
		return
	}
	req.ReceiverPhone = strings.TrimSpace(req.ReceiverPhone)
	req.ReceiverName = strings.TrimSpace(req.ReceiverName)
	if req.ReceiverPhone == "" {
		utils.ResponseBadRequest(c, "receiverPhone不能为空")
		return
	}
	user, created, err := ctl.meetingService.EnsureCloudLawUserByPhone(c.Request.Context(), req.ReceiverPhone, req.ReceiverName)
	if err != nil {
		ctl.handleError(c, err, "创建云律会议接单人失败")
		return
	}
	room, participant, _, err := ctl.meetingService.JoinRoomForInternal(c.Request.Context(), user.ID, code)
	if err != nil {
		ctl.handleError(c, err, "云律接单人加入会议失败")
		return
	}
	payload := cloudLawMeetingPayload(room, 0)
	payload["callId"] = req.CallID
	payload["receiverId"] = req.ReceiverID
	payload["receiverType"] = req.ReceiverType
	payload["echoChatParticipantUserId"] = strconv.FormatInt(user.ID, 10)
	payload["echoChatParticipantUserCreated"] = created
	if participant != nil {
		payload["participantId"] = strconv.FormatInt(participant.ID, 10)
	}
	utils.ResponseOK(c, payload)
}

func (ctl *MeetingController) CloudLawEndMeeting(c *gin.Context) {
	if !ctl.requireCloudLawInternalToken(c) {
		return
	}
	code := c.Param("code")
	if code == "" {
		utils.ResponseBadRequest(c, "会议号不能为空")
		return
	}
	if err := ctl.meetingService.EndRoomForInternal(c.Request.Context(), code); err != nil {
		ctl.handleError(c, err, "结束云律会议失败")
		return
	}
	utils.ResponseOK(c, gin.H{"roomCode": code, "code": code, "meetingCode": code})
}

// CreateRoom POST /api/v1/meeting/rooms
func (ctl *MeetingController) CreateRoom(c *gin.Context) {
	userID, ok := requireUserID(c)
	if !ok {
		return
	}
	var req dto.CreateMeetingRoomRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		utils.ResponseBadRequest(c, "参数校验失败: "+err.Error())
		return
	}
	room, _, routerID, err := ctl.meetingService.CreateRoom(c.Request.Context(), userID, &req)
	if err != nil {
		ctl.handleError(c, err, "创建会议失败")
		return
	}
	// Task 9：CreateRoom 响应携带 router_id + rtpCapabilities，供前端 Device.load 使用
	_, rtpCaps, _ := ctl.meetingService.ResolveRouterInfo(room.RoomCode)
	resp := dto.CreateMeetingRoomResponse{
		Room:            *roomToDTO(room, 1),
		RouterID:        routerID,
		RtpCapabilities: rtpCaps,
	}
	utils.ResponseCreated(c, resp)
}

// GetRoom GET /api/v1/meeting/rooms/:code
func (ctl *MeetingController) GetRoom(c *gin.Context) {
	userID, ok := requireUserID(c)
	if !ok {
		return
	}
	code := c.Param("code")
	if code == "" {
		utils.ResponseBadRequest(c, "会议号不能为空")
		return
	}
	room, participants, onlineCount, err := ctl.meetingService.GetRoomByCode(c.Request.Context(), userID, code)
	if err != nil {
		ctl.handleError(c, err, "获取会议详情失败")
		return
	}
	userIDs := make([]int64, 0, len(participants))
	for i := range participants {
		userIDs = append(userIDs, participants[i].UserID)
	}
	userMap := ctl.meetingService.ResolveUsersDisplay(c.Request.Context(), userIDs)

	parts := make([]dto.MeetingParticipantDTO, 0, len(participants))
	for i := range participants {
		parts = append(parts, *participantToDTO(&participants[i], userMap))
	}
	resp := dto.GetMeetingRoomResponse{
		Room:         *roomToDTO(room, int(onlineCount)),
		Participants: parts,
		OnlineCount:  int(onlineCount),
	}
	utils.ResponseOK(c, resp)
}

// JoinRoom POST /api/v1/meeting/rooms/:code/join
func (ctl *MeetingController) JoinRoom(c *gin.Context) {
	userID, ok := requireUserID(c)
	if !ok {
		return
	}
	code := c.Param("code")
	if code == "" {
		utils.ResponseBadRequest(c, "会议号不能为空")
		return
	}
	var req dto.JoinMeetingRoomRequest
	if err := c.ShouldBindJSON(&req); err != nil && err.Error() != "EOF" {
		utils.ResponseBadRequest(c, "参数校验失败: "+err.Error())
		return
	}
	room, participant, routerID, err := ctl.meetingService.JoinRoom(c.Request.Context(), userID, code, req.Password)
	if err != nil {
		ctl.handleError(c, err, "加入会议失败")
		return
	}
	// Task 9：JoinRoom 响应携带 rtpCapabilities，供前端 mediasoup-client Device.load 直接使用
	_, rtpCaps, _ := ctl.meetingService.ResolveRouterInfo(code)
	userMap := ctl.meetingService.ResolveUsersDisplay(c.Request.Context(), []int64{participant.UserID})
	resp := dto.JoinMeetingRoomResponse{
		Room:            *roomToDTO(room, 0),
		Participant:     *participantToDTO(participant, userMap),
		RouterID:        routerID,
		RtpCapabilities: rtpCaps,
	}
	utils.ResponseOK(c, resp)
}

// LeaveRoom POST /api/v1/meeting/rooms/:code/leave
func (ctl *MeetingController) LeaveRoom(c *gin.Context) {
	userID, ok := requireUserID(c)
	if !ok {
		return
	}
	code := c.Param("code")
	if code == "" {
		utils.ResponseBadRequest(c, "会议号不能为空")
		return
	}
	duration, err := ctl.meetingService.LeaveRoom(c.Request.Context(), userID, code)
	if err != nil {
		ctl.handleError(c, err, "离开会议失败")
		return
	}
	utils.ResponseOK(c, dto.LeaveMeetingRoomResponse{Duration: duration})
}

// EndRoom POST /api/v1/meeting/rooms/:code/end
func (ctl *MeetingController) EndRoom(c *gin.Context) {
	userID, ok := requireUserID(c)
	if !ok {
		return
	}
	code := c.Param("code")
	if code == "" {
		utils.ResponseBadRequest(c, "会议号不能为空")
		return
	}
	if err := ctl.meetingService.EndRoom(c.Request.Context(), userID, code); err != nil {
		ctl.handleError(c, err, "结束会议失败")
		return
	}
	utils.ResponseOK(c, gin.H{})
}

// ListMyMeetings GET /api/v1/meeting/rooms/mine?status=&before_id=&limit=
func (ctl *MeetingController) ListMyMeetings(c *gin.Context) {
	userID, ok := requireUserID(c)
	if !ok {
		return
	}
	var req dto.ListMyMeetingsRequest
	if err := c.ShouldBindQuery(&req); err != nil {
		utils.ResponseBadRequest(c, "参数校验失败: "+err.Error())
		return
	}
	rooms, hasMore, err := ctl.meetingService.ListMyMeetings(c.Request.Context(), userID, req.Status, req.BeforeID, req.Limit)
	if err != nil {
		ctl.handleError(c, err, "获取会议列表失败")
		return
	}
	list := make([]dto.MeetingRoomDTO, 0, len(rooms))
	for i := range rooms {
		list = append(list, *roomToDTO(&rooms[i], 0))
	}
	utils.ResponseOK(c, dto.ListMyMeetingsResponse{List: list, HasMore: hasMore})
}

// TransferHost POST /api/v1/meeting/rooms/:code/transfer-host
func (ctl *MeetingController) TransferHost(c *gin.Context) {
	userID, ok := requireUserID(c)
	if !ok {
		return
	}
	code := c.Param("code")
	if code == "" {
		utils.ResponseBadRequest(c, "会议号不能为空")
		return
	}
	var req dto.TransferHostRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		utils.ResponseBadRequest(c, "参数校验失败: "+err.Error())
		return
	}
	if err := ctl.meetingService.TransferHost(c.Request.Context(), userID, code, req.TargetUserID); err != nil {
		ctl.handleError(c, err, "转让主持人失败")
		return
	}
	utils.ResponseOK(c, gin.H{})
}

// KickMember POST /api/v1/meeting/rooms/:code/kick
func (ctl *MeetingController) KickMember(c *gin.Context) {
	userID, ok := requireUserID(c)
	if !ok {
		return
	}
	code := c.Param("code")
	if code == "" {
		utils.ResponseBadRequest(c, "会议号不能为空")
		return
	}
	var req dto.KickMemberRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		utils.ResponseBadRequest(c, "参数校验失败: "+err.Error())
		return
	}
	if err := ctl.meetingService.KickMember(c.Request.Context(), userID, code, req.UserID); err != nil {
		ctl.handleError(c, err, "踢出成员失败")
		return
	}
	utils.ResponseOK(c, gin.H{})
}

// InviteUsers POST /api/v1/meeting/rooms/:code/invite
func (ctl *MeetingController) InviteUsers(c *gin.Context) {
	userID, ok := requireUserID(c)
	if !ok {
		return
	}
	code := c.Param("code")
	if code == "" {
		utils.ResponseBadRequest(c, "会议号不能为空")
		return
	}
	var req dto.InviteUsersRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		utils.ResponseBadRequest(c, "参数校验失败: "+err.Error())
		return
	}
	pushed, skipped, err := ctl.meetingService.InviteUsers(c.Request.Context(), userID, code, req.InviteeIDs)
	if err != nil {
		ctl.handleError(c, err, "发送邀请失败")
		return
	}
	utils.ResponseOK(c, dto.InviteUsersResponse{Pushed: pushed, Skipped: skipped})
}

// RedeemInvite POST /api/v1/meeting/invite-tokens/:token/redeem
func (ctl *MeetingController) RedeemInvite(c *gin.Context) {
	userID, ok := requireUserID(c)
	if !ok {
		return
	}
	token := c.Param("token")
	if token == "" {
		utils.ResponseBadRequest(c, "邀请 Token 不能为空")
		return
	}
	resp, err := ctl.meetingService.RedeemInviteToken(c.Request.Context(), userID, token)
	if err != nil {
		ctl.handleError(c, err, "兑换邀请链接失败")
		return
	}
	utils.ResponseOK(c, resp)
}

// SendChat POST /api/v1/meeting/rooms/:code/chats
func (ctl *MeetingController) SendChat(c *gin.Context) {
	userID, ok := requireUserID(c)
	if !ok {
		return
	}
	code := c.Param("code")
	if code == "" {
		utils.ResponseBadRequest(c, "会议号不能为空")
		return
	}
	var req dto.SendMeetingChatRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		utils.ResponseBadRequest(c, "参数校验失败: "+err.Error())
		return
	}
	msg, err := ctl.meetingService.SendChatMessage(c.Request.Context(), userID, code, req.Content)
	if err != nil {
		ctl.handleError(c, err, "发送会议聊天失败")
		return
	}
	userMap := ctl.meetingService.ResolveUsersDisplay(c.Request.Context(), []int64{msg.UserID})
	utils.ResponseCreated(c, dto.SendMeetingChatResponse{Message: *chatToDTO(msg, userMap)})
}

// ListChats GET /api/v1/meeting/rooms/:code/chats?before_id=&limit=
func (ctl *MeetingController) ListChats(c *gin.Context) {
	userID, ok := requireUserID(c)
	if !ok {
		return
	}
	code := c.Param("code")
	if code == "" {
		utils.ResponseBadRequest(c, "会议号不能为空")
		return
	}
	beforeID, _ := strconv.ParseInt(c.Query("before_id"), 10, 64)
	limit, _ := strconv.Atoi(c.Query("limit"))
	msgs, hasMore, err := ctl.meetingService.ListChatMessages(c.Request.Context(), userID, code, beforeID, limit)
	if err != nil {
		ctl.handleError(c, err, "获取会议聊天失败")
		return
	}
	userIDs := make([]int64, 0, len(msgs))
	for i := range msgs {
		userIDs = append(userIDs, msgs[i].UserID)
	}
	userMap := ctl.meetingService.ResolveUsersDisplay(c.Request.Context(), userIDs)
	list := make([]dto.MeetingChatDTO, 0, len(msgs))
	for i := range msgs {
		list = append(list, *chatToDTO(&msgs[i], userMap))
	}
	utils.ResponseOK(c, dto.ListMeetingChatsResponse{List: list, HasMore: hasMore})
}
