package service

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"time"

	"github.com/echochat/backend/app/constants"
	"github.com/echochat/backend/app/dto"
	"github.com/echochat/backend/app/im/dao"
	"github.com/echochat/backend/app/im/model"
	"github.com/echochat/backend/pkg/logs"
	"github.com/echochat/backend/pkg/ws"
	"github.com/redis/go-redis/v9"
	"go.uber.org/zap"
)

const (
	unreadKeyPrefix = "echo:im:unread:" // Redis 未读总数 key 前缀，完整 key: echo:im:unread:{user_id}
	defaultPageSize = 30                // 历史消息默认拉取条数
	maxPageSize     = 100               // 历史消息最大拉取条数
)

var (
	ErrNotFriend        = errors.New("对方不是你的好友")
	ErrEmptyContent     = errors.New("消息内容不能为空")
	ErrConvNotFound     = errors.New("会话不存在")
	ErrMsgNotFound      = errors.New("消息不存在")
	ErrNotSender        = errors.New("只能撤回自己发送的消息")
	ErrRecallTimeout    = errors.New("超过撤回时限")
	ErrNotMember        = errors.New("你不是该会话的成员")
	ErrDuplicateMsg     = errors.New("重复消息")
	ErrInvalidMsgType   = errors.New("不支持的消息类型")
	ErrGroupDissolved   = errors.New("群聊已解散")
	ErrGroupAllMuted    = errors.New("当前群已开启全体禁言")
	ErrUserMuted        = errors.New("你已被禁言，无法发送消息")
)

// IMService 即时通讯核心业务服务
type IMService struct {
	convDAO        *dao.ConversationDAO
	msgDAO         *dao.MessageDAO
	pubsub         *ws.PubSub
	rdb            *redis.Client
	friendChecker  FriendChecker
	userInfoGetter UserInfoGetter
	groupInfo      GroupInfoGetter
	readRecorder   MessageReadRecorder
}

// NewIMService 创建 IMService 实例
func NewIMService(
	convDAO *dao.ConversationDAO,
	msgDAO *dao.MessageDAO,
	pubsub *ws.PubSub,
	rdb *redis.Client,
	friendChecker FriendChecker,
	userInfoGetter UserInfoGetter,
	groupInfo GroupInfoGetter,
	readRecorder MessageReadRecorder,
) *IMService {
	return &IMService{
		convDAO:        convDAO,
		msgDAO:         msgDAO,
		pubsub:         pubsub,
		rdb:            rdb,
		friendChecker:  friendChecker,
		userInfoGetter: userInfoGetter,
		groupInfo:      groupInfo,
		readRecorder:   readRecorder,
	}
}

// SendMessage 发送消息（核心流程，同时支持单聊和群聊）
// 单聊：校验好友关系 → 查找/创建会话 → 写入消息 → 推送给对方
// 群聊：校验群成员+禁言 → 写入消息（含 @信息）→ 推送给所有群成员
func (s *IMService) SendMessage(ctx context.Context, senderID int64, req *dto.SendMessageRequest) (*dto.MessageDTO, error) {
	funcName := "service.im_service.SendMessage"
	logs.Info(ctx, funcName, "发送消息",
		zap.Int64("sender_id", senderID),
		zap.Int64("conversation_id", req.ConversationID),
		zap.Int64("target_user_id", req.TargetUserID))

	if req.Type == 0 {
		req.Type = constants.MessageTypeText
	}
	switch req.Type {
	case constants.MessageTypeText:
		if req.Content == "" {
			return nil, ErrEmptyContent
		}
	case constants.MessageTypeImage, constants.MessageTypeVoice, constants.MessageTypeFile:
		if req.Extra == "" {
			return nil, ErrEmptyContent
		}
	default:
		return nil, ErrInvalidMsgType
	}

	convID := req.ConversationID

	if convID == 0 && req.TargetUserID > 0 {
		return s.sendPrivateMessage(ctx, senderID, req)
	} else if convID > 0 {
		conv, err := s.convDAO.GetByID(ctx, convID)
		if err != nil {
			return nil, ErrConvNotFound
		}
		if conv.Type == constants.ConversationTypeGroup {
			return s.sendGroupMessage(ctx, senderID, req)
		}
		return s.sendPrivateMessageByConvID(ctx, senderID, convID, req)
	}
	return nil, ErrConvNotFound
}

// sendPrivateMessage 发送单聊消息（首次发送，通过 TargetUserID）
func (s *IMService) sendPrivateMessage(ctx context.Context, senderID int64, req *dto.SendMessageRequest) (*dto.MessageDTO, error) {
	funcName := "service.im_service.sendPrivateMessage"

	isFriend, err := s.friendChecker.IsFriend(ctx, senderID, req.TargetUserID)
	if err != nil {
		logs.Error(ctx, funcName, "检查好友关系失败", zap.Error(err))
		return nil, err
	}
	if !isFriend {
		return nil, ErrNotFriend
	}

	conv, err := s.getOrCreatePrivateConversation(ctx, senderID, req.TargetUserID)
	if err != nil {
		return nil, err
	}

	return s.writeAndPushPrivateMessage(ctx, senderID, conv.ID, req.TargetUserID, req)
}

// sendPrivateMessageByConvID 发送单聊消息（已有会话 ID）
func (s *IMService) sendPrivateMessageByConvID(ctx context.Context, senderID, convID int64, req *dto.SendMessageRequest) (*dto.MessageDTO, error) {
	funcName := "service.im_service.sendPrivateMessageByConvID"

	member, err := s.convDAO.GetMember(ctx, convID, senderID)
	if err != nil || member == nil {
		return nil, ErrNotMember
	}

	peerID, err := s.convDAO.GetPeerUserID(ctx, convID, senderID)
	if err != nil {
		logs.Error(ctx, funcName, "查询对方用户 ID 失败", zap.Error(err))
		return nil, err
	}

	return s.writeAndPushPrivateMessage(ctx, senderID, convID, peerID, req)
}

// writeAndPushPrivateMessage 单聊：幂等去重 + 写消息 + 推送
func (s *IMService) writeAndPushPrivateMessage(ctx context.Context, senderID, convID, peerID int64, req *dto.SendMessageRequest) (*dto.MessageDTO, error) {
	funcName := "service.im_service.writeAndPushPrivateMessage"

	if req.ClientMsgID != "" {
		existing, err := s.msgDAO.FindByClientMsgID(ctx, convID, req.ClientMsgID)
		if err != nil {
			logs.Error(ctx, funcName, "幂等去重查询失败", zap.Error(err))
			return nil, err
		}
		if existing != nil {
			return s.toMessageDTO(existing), ErrDuplicateMsg
		}
	}

	msg := &model.Message{
		ConversationID: convID,
		SenderID:       senderID,
		Type:           req.Type,
		Content:        req.Content,
		Status:         constants.MessageStatusNormal,
		ClientMsgID:    req.ClientMsgID,
	}
	if req.Extra != "" {
		msg.Extra = &req.Extra
	}
	if err := s.msgDAO.Create(ctx, msg); err != nil {
		return nil, err
	}

	now := time.Now()
	preview := generateLastMsgPreview(req.Type, req.Content, msg.Extra)
	if err := s.convDAO.UpdateLastMessage(ctx, convID, msg.ID, preview, senderID, now); err != nil {
		logs.Error(ctx, funcName, "更新最后消息失败", zap.Error(err))
	}

	if err := s.convDAO.RestoreMember(ctx, convID, peerID); err != nil {
		logs.Error(ctx, funcName, "恢复对方会话视图失败", zap.Error(err))
	}

	if err := s.convDAO.IncrementUnread(ctx, convID, peerID); err != nil {
		logs.Error(ctx, funcName, "递增未读计数失败", zap.Error(err))
	}

	s.incrementTotalUnread(ctx, peerID)

	pushData := s.buildMessagePushData(ctx, msg, senderID)
	pushData["conv_type"] = constants.ConversationTypePrivate
	s.pushToUser(ctx, peerID, "im.message.new", pushData)

	return s.toMessageDTO(msg), nil
}

// sendGroupMessage 发送群聊消息
func (s *IMService) sendGroupMessage(ctx context.Context, senderID int64, req *dto.SendMessageRequest) (*dto.MessageDTO, error) {
	funcName := "service.im_service.sendGroupMessage"
	convID := req.ConversationID

	member, err := s.convDAO.GetMember(ctx, convID, senderID)
	if err != nil || member == nil {
		return nil, ErrNotMember
	}

	if s.groupInfo != nil {
		groupBrief, gErr := s.groupInfo.GetGroupBrief(ctx, convID)
		if gErr != nil {
			logs.Error(ctx, funcName, "获取群信息失败", zap.Error(gErr))
			return nil, ErrConvNotFound
		}
		if groupBrief.Status == constants.GroupStatusDissolved {
			return nil, ErrGroupDissolved
		}
		if groupBrief.IsAllMuted && member.Role < constants.GroupRoleAdmin {
			return nil, ErrGroupAllMuted
		}
	}

	if member.IsMuted {
		return nil, ErrUserMuted
	}

	if req.ClientMsgID != "" {
		existing, err := s.msgDAO.FindByClientMsgID(ctx, convID, req.ClientMsgID)
		if err != nil {
			logs.Error(ctx, funcName, "幂等去重查询失败", zap.Error(err))
			return nil, err
		}
		if existing != nil {
			return s.toMessageDTO(existing), ErrDuplicateMsg
		}
	}

	msg := &model.Message{
		ConversationID: convID,
		SenderID:       senderID,
		Type:           req.Type,
		Content:        req.Content,
		Status:         constants.MessageStatusNormal,
		ClientMsgID:    req.ClientMsgID,
	}
	if req.Extra != "" {
		msg.Extra = &req.Extra
	}
	if len(req.AtUserIDs) > 0 {
		msg.AtUserIDs = req.AtUserIDs
	}
	if err := s.msgDAO.Create(ctx, msg); err != nil {
		return nil, err
	}

	now := time.Now()
	preview := generateLastMsgPreview(req.Type, req.Content, msg.Extra)
	if err := s.convDAO.UpdateLastMessage(ctx, convID, msg.ID, preview, senderID, now); err != nil {
		logs.Error(ctx, funcName, "更新最后消息失败", zap.Error(err))
	}

	memberIDs, err := s.convDAO.GetConversationMemberIDs(ctx, convID)
	if err != nil {
		logs.Error(ctx, funcName, "获取群成员列表失败", zap.Error(err))
		return s.toMessageDTO(msg), nil
	}

	dndMap, dndErr := s.convDAO.GetMemberDNDMap(ctx, convID)
	if dndErr != nil {
		logs.Error(ctx, funcName, "获取免打扰状态失败", zap.Error(dndErr))
		dndMap = make(map[int64]bool)
	}

	pushData := s.buildMessagePushData(ctx, msg, senderID)
	pushData["conv_type"] = constants.ConversationTypeGroup
	if len(req.AtUserIDs) > 0 {
		pushData["at_user_ids"] = req.AtUserIDs
	}

	for _, uid := range memberIDs {
		if uid == senderID {
			continue
		}
		if err := s.convDAO.IncrementUnread(ctx, convID, uid); err != nil {
			logs.Error(ctx, funcName, "递增未读计数失败", zap.Int64("user_id", uid), zap.Error(err))
		}

		if !dndMap[uid] {
			s.incrementTotalUnread(ctx, uid)
		}

		if len(req.AtUserIDs) > 0 {
			isAtMe := false
			for _, atID := range req.AtUserIDs {
				if atID == uid || atID == 0 {
					isAtMe = true
					break
				}
			}
			if isAtMe {
				if err := s.convDAO.IncrementAtMeCount(ctx, convID, uid); err != nil {
					logs.Error(ctx, funcName, "递增@计数失败", zap.Int64("user_id", uid), zap.Error(err))
				}
			}
		}

		s.pushToUser(ctx, uid, "im.message.new", pushData)
	}

	return s.toMessageDTO(msg), nil
}

// RecallMessage 撤回消息
// 单聊：只能撤回自己的消息，2 分钟内
// 群聊：自己的消息 2 分钟内撤回；群主/管理员可无时限撤回任何消息
func (s *IMService) RecallMessage(ctx context.Context, operatorID int64, messageID int64) error {
	funcName := "service.im_service.RecallMessage"
	logs.Info(ctx, funcName, "撤回消息",
		zap.Int64("operator_id", operatorID), zap.Int64("message_id", messageID))

	msg, err := s.msgDAO.GetByID(ctx, messageID)
	if err != nil {
		return ErrMsgNotFound
	}

	conv, err := s.convDAO.GetByID(ctx, msg.ConversationID)
	if err != nil {
		logs.Error(ctx, funcName, "获取会话信息失败", zap.Error(err))
		return ErrConvNotFound
	}

	isAdmin := false
	if conv.Type == constants.ConversationTypeGroup {
		member, mErr := s.convDAO.GetMember(ctx, msg.ConversationID, operatorID)
		if mErr != nil || member == nil {
			return ErrNotMember
		}
		isAdmin = member.Role >= constants.GroupRoleAdmin
	}

	if msg.SenderID == operatorID {
		if time.Since(msg.CreatedAt).Seconds() > float64(constants.MessageRecallTimeLimit) {
			if !isAdmin {
				return ErrRecallTimeout
			}
		}
	} else {
		if !isAdmin {
			return ErrNotSender
		}
	}

	if err := s.msgDAO.UpdateStatus(ctx, messageID, constants.MessageStatusRecalled); err != nil {
		logs.Error(ctx, funcName, "更新消息状态失败", zap.Error(err))
		return err
	}

	recallText := "撤回了一条消息"
	if msg.SenderID != operatorID && isAdmin {
		operatorInfo, infoErr := s.userInfoGetter.GetUsersByIDs(ctx, []int64{operatorID, msg.SenderID})
		operatorName := "管理员"
		senderName := "成员"
		if infoErr == nil {
			for _, u := range operatorInfo {
				if u.ID == operatorID {
					operatorName = u.Nickname
				}
				if u.ID == msg.SenderID {
					senderName = u.Nickname
				}
			}
		}
		recallText = fmt.Sprintf("管理员 %s 撤回了 %s 的一条消息", operatorName, senderName)
	} else {
		senderInfo, infoErr := s.userInfoGetter.GetUsersByIDs(ctx, []int64{operatorID})
		if infoErr == nil && len(senderInfo) > 0 {
			recallText = senderInfo[0].Nickname + " 撤回了一条消息"
		}
	}

	if conv.LastMessageID != nil && *conv.LastMessageID == msg.ID {
		if updateErr := s.convDAO.UpdateLastMessage(ctx, msg.ConversationID, msg.ID, recallText, operatorID, msg.CreatedAt); updateErr != nil {
			logs.Error(ctx, funcName, "更新会话预览失败", zap.Error(updateErr))
		}
	}

	memberIDs, err := s.convDAO.GetConversationMemberIDs(ctx, msg.ConversationID)
	if err != nil {
		logs.Error(ctx, funcName, "获取会话成员失败", zap.Error(err))
		return nil
	}
	for _, uid := range memberIDs {
		if uid == operatorID {
			continue
		}
		s.pushToUser(ctx, uid, "im.message.recalled", map[string]interface{}{
			"message_id":      messageID,
			"conversation_id": msg.ConversationID,
			"operator_id":     operatorID,
			"sender_id":       msg.SenderID,
			"recall_text":     recallText,
		})
	}

	return nil
}

// GetConversationList 获取会话列表（含对方用户信息 / 群聊信息）
func (s *IMService) GetConversationList(ctx context.Context, userID int64) (*dto.ConversationListResponse, error) {
	funcName := "service.im_service.GetConversationList"
	logs.Debug(ctx, funcName, "获取会话列表", zap.Int64("user_id", userID))

	convs, err := s.convDAO.GetUserConversations(ctx, userID)
	if err != nil {
		return nil, err
	}

	peerIDs := make([]int64, 0, len(convs))
	groupConvIDs := make([]int64, 0)
	for _, c := range convs {
		if c.Type == constants.ConversationTypePrivate && c.PeerUserID > 0 {
			peerIDs = append(peerIDs, c.PeerUserID)
		} else if c.Type == constants.ConversationTypeGroup {
			groupConvIDs = append(groupConvIDs, c.ID)
		}
	}

	userMap := make(map[int64]*userBrief)
	if len(peerIDs) > 0 {
		users, uErr := s.userInfoGetter.GetUsersByIDs(ctx, peerIDs)
		if uErr != nil {
			logs.Error(ctx, funcName, "批量查询用户信息失败", zap.Error(uErr))
		} else {
			for i := range users {
				u := users[i]
				userMap[u.ID] = &userBrief{Nickname: u.Nickname, Avatar: u.Avatar}
			}
		}
	}

	groupMap := make(map[int64]*dto.GroupBrief)
	if s.groupInfo != nil && len(groupConvIDs) > 0 {
		for _, convID := range groupConvIDs {
			brief, gErr := s.groupInfo.GetGroupBrief(ctx, convID)
			if gErr != nil {
				logs.Error(ctx, funcName, "获取群信息失败",
					zap.Int64("conversation_id", convID), zap.Error(gErr))
				continue
			}
			groupMap[convID] = brief
		}
	}

	list := make([]dto.ConversationDTO, 0, len(convs))
	for _, c := range convs {
		item := dto.ConversationDTO{
			ID:              c.ID,
			Type:            c.Type,
			PeerUserID:      c.PeerUserID,
			LastMsgContent:  c.LastMsgContent,
			LastMsgSenderID: c.LastMsgSenderID,
			IsPinned:        c.IsPinned,
			UnreadCount:     c.UnreadCount,
			IsDoNotDisturb:  c.IsDoNotDisturb,
			AtMeCount:       c.AtMeCount,
		}
		if c.ClearBeforeMsgID > 0 && c.LastMessageID != nil && *c.LastMessageID <= c.ClearBeforeMsgID {
			item.LastMsgContent = ""
			item.LastMsgSenderID = nil
		}
		if c.LastMsgTime != nil {
			item.LastMsgTime = c.LastMsgTime.Format("2006-01-02 15:04:05")
		}

		if c.Type == constants.ConversationTypePrivate {
			if brief, ok := userMap[c.PeerUserID]; ok {
				item.PeerNickname = brief.Nickname
				item.PeerAvatar = brief.Avatar
			}
		} else if c.Type == constants.ConversationTypeGroup {
			if gBrief, ok := groupMap[c.ID]; ok {
				item.PeerNickname = gBrief.Name
				item.PeerAvatar = gBrief.Avatar
				item.GroupID = gBrief.ID
			}
		}

		list = append(list, item)
	}

	return &dto.ConversationListResponse{List: list}, nil
}

// GetHistoryMessages 获取历史消息（游标分页）
func (s *IMService) GetHistoryMessages(ctx context.Context, userID int64, req *dto.HistoryMessageRequest) (*dto.HistoryMessageResponse, error) {
	funcName := "service.im_service.GetHistoryMessages"
	logs.Debug(ctx, funcName, "查询历史消息",
		zap.Int64("user_id", userID), zap.Int64("conversation_id", req.ConversationID))

	member, err := s.convDAO.GetMember(ctx, req.ConversationID, userID)
	if err != nil || member == nil {
		return nil, ErrNotMember
	}

	limit := req.Limit
	if limit <= 0 {
		limit = defaultPageSize
	}
	if limit > maxPageSize {
		limit = maxPageSize
	}

	messages, err := s.msgDAO.GetByConversation(ctx, req.ConversationID, req.BeforeID, member.ClearBeforeMsgID, limit+1)
	if err != nil {
		return nil, err
	}

	hasMore := len(messages) > limit
	if hasMore {
		messages = messages[:limit]
	}

	list := make([]dto.MessageDTO, 0, len(messages))
	for _, m := range messages {
		list = append(list, *s.toMessageDTO(&m))
	}

	resp := &dto.HistoryMessageResponse{List: list, HasMore: hasMore}

	// ========== 已读状态回填（修复页面刷新后已读标签变未读的 Bug） ==========
	// 获取会话类型以决定回填策略
	conv, convErr := s.convDAO.GetByID(ctx, req.ConversationID)
	if convErr != nil || conv == nil {
		// 拉不到会话元信息时静默跳过已读回填，消息仍正常返回（向前兼容）
		return resp, nil
	}

	switch conv.Type {
	case constants.ConversationTypePrivate:
		// 单聊：查询对方成员的 last_read_msg_id
		peerID, pErr := s.convDAO.GetPeerUserID(ctx, req.ConversationID, userID)
		if pErr == nil && peerID > 0 {
			peerMember, pmErr := s.convDAO.GetMember(ctx, req.ConversationID, peerID)
			if pmErr == nil && peerMember != nil {
				resp.PeerLastReadMsgID = peerMember.LastReadMsgID
			}
		}
	case constants.ConversationTypeGroup:
		// 群聊：批量查询"本次返回消息中自己发送的那些"的 read_count
		// 仅查自己发的，因为前端仅在 isSelf=true 时展示 "N人已读"
		selfMsgIDs := make([]int64, 0, len(messages))
		for i := range messages {
			if messages[i].SenderID == userID && messages[i].ID > 0 {
				selfMsgIDs = append(selfMsgIDs, messages[i].ID)
			}
		}
		if len(selfMsgIDs) > 0 && s.readRecorder != nil {
			countMap, rcErr := s.readRecorder.GetReadCountBatch(ctx, selfMsgIDs)
			if rcErr == nil && len(countMap) > 0 {
				resp.ReadCountMap = countMap
			}
		}
	}

	return resp, nil
}

// MarkRead 标记会话已读（清零未读 + 更新 Redis 总未读数 + 推送已读 ACK 给对方）
func (s *IMService) MarkRead(ctx context.Context, userID int64, conversationID int64) error {
	funcName := "service.im_service.MarkRead"
	logs.Info(ctx, funcName, "标记已读",
		zap.Int64("user_id", userID), zap.Int64("conversation_id", conversationID))

	member, err := s.convDAO.GetMember(ctx, conversationID, userID)
	if err != nil || member == nil {
		return ErrNotMember
	}

	latestMsgID, err := s.msgDAO.GetLatestMessageID(ctx, conversationID)
	if err != nil {
		logs.Error(ctx, funcName, "获取最新消息 ID 失败", zap.Error(err))
		return err
	}

	if member.UnreadCount > 0 {
		if err := s.convDAO.ClearUnread(ctx, conversationID, userID, latestMsgID); err != nil {
			logs.Error(ctx, funcName, "清零未读失败", zap.Error(err))
			return err
		}
		s.decrementTotalUnread(ctx, userID, member.UnreadCount)
	}

	memberIDs, err := s.convDAO.GetConversationMemberIDs(ctx, conversationID)
	if err != nil {
		logs.Error(ctx, funcName, "获取会话成员失败", zap.Error(err))
		return nil
	}
	for _, peerID := range memberIDs {
		if peerID != userID {
			s.pushToUser(ctx, peerID, "im.message.read.ack", map[string]interface{}{
				"conversation_id":   conversationID,
				"reader_id":         userID,
				"last_read_msg_id":  latestMsgID,
			})
		}
	}

	return nil
}

// MarkGroupMessagesRead 标记群聊消息已读（消息级别）
// 将指定消息标记为已读 + 推送已读计数变化给消息发送者
func (s *IMService) MarkGroupMessagesRead(ctx context.Context, userID int64, req *dto.MarkGroupReadRequest) error {
	funcName := "service.im_service.MarkGroupMessagesRead"
	logs.Info(ctx, funcName, "群消息标记已读",
		zap.Int64("user_id", userID), zap.Int64("conversation_id", req.ConversationID),
		zap.Int("msg_count", len(req.MessageIDs)))

	member, err := s.convDAO.GetMember(ctx, req.ConversationID, userID)
	if err != nil || member == nil {
		return ErrNotMember
	}

	if s.readRecorder == nil || len(req.MessageIDs) == 0 {
		return nil
	}

	if err := s.readRecorder.BatchCreateReads(ctx, req.MessageIDs, userID); err != nil {
		logs.Error(ctx, funcName, "批量创建已读记录失败", zap.Error(err))
		return err
	}

	if member.AtMeCount > 0 {
		if err := s.convDAO.ClearAtMeCount(ctx, req.ConversationID, userID); err != nil {
			logs.Error(ctx, funcName, "清零@计数失败", zap.Error(err))
		}
	}

	readCounts, err := s.readRecorder.GetReadCountBatch(ctx, req.MessageIDs)
	if err != nil {
		logs.Error(ctx, funcName, "获取已读计数失败", zap.Error(err))
		return nil
	}

	for _, msgID := range req.MessageIDs {
		msg, mErr := s.msgDAO.GetByID(ctx, msgID)
		if mErr != nil || msg.SenderID == userID {
			continue
		}
		count := readCounts[msgID]
		s.pushToUser(ctx, msg.SenderID, "im.message.read.count", map[string]interface{}{
			"message_id":      msgID,
			"conversation_id": req.ConversationID,
			"read_count":      count,
		})
	}

	return nil
}

// GetMessageReadDetail 获取消息已读详情（已读/未读用户列表）
func (s *IMService) GetMessageReadDetail(ctx context.Context, userID int64, messageID int64) (*dto.GetReadDetailResponse, error) {
	funcName := "service.im_service.GetMessageReadDetail"
	logs.Debug(ctx, funcName, "获取已读详情",
		zap.Int64("user_id", userID), zap.Int64("message_id", messageID))

	msg, err := s.msgDAO.GetByID(ctx, messageID)
	if err != nil {
		return nil, ErrMsgNotFound
	}

	member, err := s.convDAO.GetMember(ctx, msg.ConversationID, userID)
	if err != nil || member == nil {
		return nil, ErrNotMember
	}

	memberIDs, err := s.convDAO.GetConversationMemberIDs(ctx, msg.ConversationID)
	if err != nil {
		return nil, err
	}

	readUserIDs := make(map[int64]bool)
	if s.readRecorder != nil {
		ids, rErr := s.readRecorder.GetReadUserIDs(ctx, messageID)
		if rErr != nil {
			logs.Error(ctx, funcName, "获取已读用户列表失败", zap.Error(rErr))
		} else {
			for _, id := range ids {
				readUserIDs[id] = true
			}
		}
	}

	allUserIDs := make([]int64, 0, len(memberIDs))
	for _, id := range memberIDs {
		if id != msg.SenderID {
			allUserIDs = append(allUserIDs, id)
		}
	}

	userMap := make(map[int64]*userBrief)
	if len(allUserIDs) > 0 {
		users, uErr := s.userInfoGetter.GetUsersByIDs(ctx, allUserIDs)
		if uErr != nil {
			logs.Error(ctx, funcName, "批量查询用户信息失败", zap.Error(uErr))
		} else {
			for i := range users {
				u := users[i]
				userMap[u.ID] = &userBrief{Nickname: u.Nickname, Avatar: u.Avatar}
			}
		}
	}

	nicknameMap, nErr := s.convDAO.GetMemberNicknameMap(ctx, msg.ConversationID)
	if nErr != nil {
		logs.Error(ctx, funcName, "获取成员群昵称失败", zap.Error(nErr))
		nicknameMap = make(map[int64]string)
	}

	var readList, unreadList []dto.MessageReadDetailDTO
	for _, uid := range allUserIDs {
		item := dto.MessageReadDetailDTO{
			UserID: uid,
		}
		if brief, ok := userMap[uid]; ok {
			item.UserNickname = brief.Nickname
			item.UserAvatar = brief.Avatar
		}
		if gn, ok := nicknameMap[uid]; ok {
			item.GroupNickname = gn
		}
		if readUserIDs[uid] {
			readList = append(readList, item)
		} else {
			unreadList = append(unreadList, item)
		}
	}

	return &dto.GetReadDetailResponse{
		ReadList:   readList,
		UnreadList: unreadList,
		ReadCount:  len(readList),
		TotalCount: len(allUserIDs),
	}, nil
}

// SetDoNotDisturb 设置/取消会话消息免打扰
func (s *IMService) SetDoNotDisturb(ctx context.Context, userID int64, conversationID int64, isDoNotDisturb bool) error {
	funcName := "service.im_service.SetDoNotDisturb"
	logs.Info(ctx, funcName, "更新免打扰状态",
		zap.Int64("user_id", userID), zap.Int64("conversation_id", conversationID), zap.Bool("is_do_not_disturb", isDoNotDisturb))

	member, err := s.convDAO.GetMember(ctx, conversationID, userID)
	if err != nil || member == nil {
		return ErrNotMember
	}

	return s.convDAO.UpdateMemberDND(ctx, conversationID, userID, isDoNotDisturb)
}

// PinConversation 置顶/取消置顶会话
func (s *IMService) PinConversation(ctx context.Context, userID int64, conversationID int64, isPinned bool) error {
	funcName := "service.im_service.PinConversation"
	logs.Info(ctx, funcName, "更新置顶状态",
		zap.Int64("user_id", userID), zap.Int64("conversation_id", conversationID), zap.Bool("is_pinned", isPinned))

	member, err := s.convDAO.GetMember(ctx, conversationID, userID)
	if err != nil || member == nil {
		return ErrNotMember
	}
	return s.convDAO.UpdateMemberPinned(ctx, conversationID, userID, isPinned)
}

// DeleteConversation 删除会话（软删除，仅影响当前用户视图）
func (s *IMService) DeleteConversation(ctx context.Context, userID int64, conversationID int64) error {
	funcName := "service.im_service.DeleteConversation"
	logs.Info(ctx, funcName, "删除会话",
		zap.Int64("user_id", userID), zap.Int64("conversation_id", conversationID))

	member, err := s.convDAO.GetMember(ctx, conversationID, userID)
	if err != nil || member == nil {
		return ErrNotMember
	}

	if member.UnreadCount > 0 {
		s.decrementTotalUnread(ctx, userID, member.UnreadCount)
	}
	return s.convDAO.SoftDeleteMember(ctx, conversationID, userID)
}

// ClearHistory 清空聊天记录（个人视图操作，仅影响当前用户，不影响对方）
// 通过记录清空截止消息 ID 实现，而非真正删除消息
func (s *IMService) ClearHistory(ctx context.Context, userID int64, conversationID int64) error {
	funcName := "service.im_service.ClearHistory"
	logs.Info(ctx, funcName, "清空聊天记录",
		zap.Int64("user_id", userID), zap.Int64("conversation_id", conversationID))

	member, err := s.convDAO.GetMember(ctx, conversationID, userID)
	if err != nil || member == nil {
		return ErrNotMember
	}

	latestMsgID, err := s.msgDAO.GetLatestMessageID(ctx, conversationID)
	if err != nil {
		logs.Error(ctx, funcName, "获取最新消息 ID 失败", zap.Error(err))
		return err
	}

	if err := s.convDAO.UpdateClearBefore(ctx, conversationID, userID, latestMsgID); err != nil {
		logs.Error(ctx, funcName, "更新清空截止 ID 失败", zap.Error(err))
		return err
	}

	if member.UnreadCount > 0 {
		s.decrementTotalUnread(ctx, userID, member.UnreadCount)
	}

	return nil
}

// SearchMessages 全局消息搜索
func (s *IMService) SearchMessages(ctx context.Context, userID int64, req *dto.SearchMessageRequest) (*dto.SearchMessageResponse, error) {
	funcName := "service.im_service.SearchMessages"
	logs.Debug(ctx, funcName, "全局消息搜索",
		zap.Int64("user_id", userID), zap.String("keyword", req.Keyword))

	limit := req.Limit
	if limit <= 0 {
		limit = 50
	}

	results, err := s.msgDAO.SearchMessages(ctx, userID, req.Keyword, limit)
	if err != nil {
		return nil, err
	}

	senderIDs := make([]int64, 0)
	senderSet := make(map[int64]bool)
	for _, r := range results {
		if !senderSet[r.SenderID] {
			senderIDs = append(senderIDs, r.SenderID)
			senderSet[r.SenderID] = true
		}
	}

	userMap := make(map[int64]*userBrief)
	if len(senderIDs) > 0 {
		users, err := s.userInfoGetter.GetUsersByIDs(ctx, senderIDs)
		if err != nil {
			logs.Error(ctx, funcName, "查询发送者信息失败", zap.Error(err))
		} else {
			for i := range users {
				u := users[i]
				userMap[u.ID] = &userBrief{Nickname: u.Nickname, Avatar: u.Avatar}
			}
		}
	}

	list := make([]dto.MessageSearchItem, 0, len(results))
	for _, r := range results {
		item := dto.MessageSearchItem{
			MessageDTO: dto.MessageDTO{
				ID:             r.ID,
				ConversationID: r.ConversationID,
				SenderID:       r.SenderID,
				Type:           constants.MessageTypeText,
				Content:        r.Content,
				Status:         constants.MessageStatusNormal,
				CreatedAt:      r.CreatedAt,
			},
		}
		if brief, ok := userMap[r.SenderID]; ok {
			item.SenderNickname = brief.Nickname
			item.SenderAvatar = brief.Avatar
		}
		list = append(list, item)
	}

	return &dto.SearchMessageResponse{List: list}, nil
}

// GetTotalUnread 获取用户的全局未读消息总数（从 Redis）
func (s *IMService) GetTotalUnread(ctx context.Context, userID int64) (int64, error) {
	key := fmt.Sprintf("%s%d", unreadKeyPrefix, userID)
	val, err := s.rdb.Get(ctx, key).Int64()
	if err == redis.Nil {
		return 0, nil
	}
	return val, err
}

// GetPeerUserID 获取单聊会话中对方的用户 ID（供 handler 层调用）
func (s *IMService) GetPeerUserID(ctx context.Context, conversationID, userID int64) (int64, error) {
	return s.convDAO.GetPeerUserID(ctx, conversationID, userID)
}

// PushTypingNotification 向对方推送正在输入通知（通过 PubSub 支持跨实例）
func (s *IMService) PushTypingNotification(ctx context.Context, conversationID, senderID int64) {
	peerID, err := s.convDAO.GetPeerUserID(ctx, conversationID, senderID)
	if err != nil {
		logs.Warn(ctx, "service.im_service.PushTypingNotification", "查询对方用户 ID 失败",
			zap.Int64("conversation_id", conversationID), zap.Error(err))
		return
	}
	s.pushToUser(ctx, peerID, "im.typing", map[string]interface{}{
		"conversation_id": conversationID,
		"user_id":         senderID,
	})
}

// ====== 内部辅助方法 ======

// getOrCreatePrivateConversation 查找或创建单聊会话
func (s *IMService) getOrCreatePrivateConversation(ctx context.Context, userID, targetUserID int64) (*model.Conversation, error) {
	funcName := "service.im_service.getOrCreatePrivateConversation"

	conv, err := s.convDAO.FindPrivateConversation(ctx, userID, targetUserID)
	if err != nil {
		return nil, err
	}
	if conv != nil {
		return conv, nil
	}

	logs.Info(ctx, funcName, "创建新的单聊会话",
		zap.Int64("user_id", userID), zap.Int64("target_user_id", targetUserID))

	newConv := &model.Conversation{
		Type:      constants.ConversationTypePrivate,
		CreatorID: userID,
	}
	if err := s.convDAO.CreateWithMembers(ctx, newConv, []int64{userID, targetUserID}); err != nil {
		return nil, err
	}
	return newConv, nil
}

// pushToUser 通过 PubSub 向指定用户推送消息
func (s *IMService) pushToUser(ctx context.Context, userID int64, event string, data interface{}) {
	push := ws.NewPushMessage(event, data)
	bytes, err := ws.MarshalPush(push)
	if err != nil {
		logs.Error(ctx, "service.im_service.pushToUser", "序列化推送消息失败",
			zap.String("event", event), zap.Error(err))
		return
	}
	if err := s.pubsub.Publish(ctx, userID, bytes); err != nil {
		logs.Error(ctx, "service.im_service.pushToUser", "PubSub 发布失败",
			zap.Int64("user_id", userID), zap.Error(err))
	}
}

// incrementTotalUnread Redis 全局未读 +1
func (s *IMService) incrementTotalUnread(ctx context.Context, userID int64) {
	key := fmt.Sprintf("%s%d", unreadKeyPrefix, userID)
	if err := s.rdb.Incr(ctx, key).Err(); err != nil {
		logs.Error(ctx, "service.im_service.incrementTotalUnread", "Redis INCR 失败",
			zap.Int64("user_id", userID), zap.Error(err))
	}
}

// decrementTotalUnread Redis 全局未读 -N（使用 Lua 脚本保证原子性，下限为 0）
func (s *IMService) decrementTotalUnread(ctx context.Context, userID int64, count int) {
	key := fmt.Sprintf("%s%d", unreadKeyPrefix, userID)
	script := redis.NewScript(`
		local current = tonumber(redis.call('GET', KEYS[1]) or '0')
		local decr = tonumber(ARGV[1])
		local newVal = current - decr
		if newVal < 0 then newVal = 0 end
		redis.call('SET', KEYS[1], newVal)
		return newVal
	`)
	if err := script.Run(ctx, s.rdb, []string{key}, count).Err(); err != nil {
		logs.Error(ctx, "service.im_service.decrementTotalUnread", "Redis Lua 脚本执行失败",
			zap.Int64("user_id", userID), zap.Error(err))
	}
}

// toMessageDTO 将 model.Message 转换为 dto.MessageDTO
func (s *IMService) toMessageDTO(m *model.Message) *dto.MessageDTO {
	d := &dto.MessageDTO{
		ID:             m.ID,
		ConversationID: m.ConversationID,
		SenderID:       m.SenderID,
		Type:           m.Type,
		Content:        m.Content,
		Extra:          m.Extra,
		Status:         m.Status,
		ClientMsgID:    m.ClientMsgID,
		CreatedAt:      m.CreatedAt.Format("2006-01-02 15:04:05"),
	}
	if len(m.AtUserIDs) > 0 {
		d.AtUserIDs = []int64(m.AtUserIDs)
	}
	return d
}

// buildMessagePushData 构建消息推送数据（单聊/群聊通用）
func (s *IMService) buildMessagePushData(ctx context.Context, msg *model.Message, senderID int64) map[string]interface{} {
	pushData := map[string]interface{}{
		"id":              msg.ID,
		"conversation_id": msg.ConversationID,
		"sender_id":       senderID,
		"type":            msg.Type,
		"content":         msg.Content,
		"client_msg_id":   msg.ClientMsgID,
		"created_at":      msg.CreatedAt.Format("2006-01-02 15:04:05"),
	}
	if msg.Extra != nil && *msg.Extra != "" {
		pushData["extra"] = *msg.Extra
	}
	if senderUsers, sErr := s.userInfoGetter.GetUsersByIDs(ctx, []int64{senderID}); sErr == nil && len(senderUsers) > 0 {
		pushData["sender_name"] = senderUsers[0].Nickname
		pushData["sender_avatar"] = senderUsers[0].Avatar
	}
	return pushData
}

// userBrief 用户简要信息（内部使用）
type userBrief struct {
	Nickname string
	Avatar   string
}

// generateLastMsgPreview 根据消息类型生成会话列表预览文案
func generateLastMsgPreview(msgType int, content string, extra *string) string {
	switch msgType {
	case constants.MessageTypeImage:
		count := 1
		if extra != nil && *extra != "" {
			var parsed struct {
				Images []json.RawMessage `json:"images"`
			}
			if json.Unmarshal([]byte(*extra), &parsed) == nil && len(parsed.Images) > 1 {
				count = len(parsed.Images)
			}
		}
		if count > 1 {
			return fmt.Sprintf("[图片x%d]", count)
		}
		return "[图片]"
	case constants.MessageTypeVoice:
		duration := 0
		if extra != nil && *extra != "" {
			var parsed struct {
				Voice struct {
					Duration int `json:"duration"`
				} `json:"voice"`
			}
			if json.Unmarshal([]byte(*extra), &parsed) == nil {
				duration = parsed.Voice.Duration
			}
		}
		if duration > 0 {
			return fmt.Sprintf("[语音 %d\"]", duration)
		}
		return "[语音]"
	case constants.MessageTypeFile:
		fileName := ""
		if extra != nil && *extra != "" {
			var parsed struct {
				File struct {
					FileName string `json:"file_name"`
				} `json:"file"`
			}
			if json.Unmarshal([]byte(*extra), &parsed) == nil {
				fileName = parsed.File.FileName
			}
		}
		if fileName != "" {
			return fmt.Sprintf("[文件] %s", fileName)
		}
		return "[文件]"
	default:
		return truncateContent(content, 100)
	}
}

// truncateContent 截断消息内容用于预览
func truncateContent(s string, maxRunes int) string {
	runes := []rune(s)
	if len(runes) <= maxRunes {
		return s
	}
	return string(runes[:maxRunes]) + "..."
}
