Source: stores/modules/chat.js

/**
 * @file stores/modules/chatStore.js
 * @description Pinia Store,用于管理聊天会话、消息、实时通信事件和消息搜索功能。
 * 它是聊天功能的核心状态管理模块。
 * @module ChatStore
 */

import { defineStore } from 'pinia'
import { ref } from 'vue'
import { useUserStore } from '@/stores'
import {
  getMessagesForConversation,
  getUserConversations,
  markAsRead,
  searchMessage,
  getOrCreateConversation
} from '../../api/chat'
import emitter from '../../services/eventBus' // 导入事件总线

/**
 * @function useChatStore
 * @description Pinia Store,用于管理聊天相关的所有状态和操作。
 * 包含会话列表、消息列表、活跃会话、加载状态、消息回复以及消息搜索功能。
 * @returns {{
 * conversations: Ref<Array<object>>,
 * messages: Ref<Array<object>>,
 * activeConversation: Ref<object|null>,
 * isLoadingConversations: Ref<boolean>,
 * currentMessagePage: Ref<number>,
 * totalMessagePages: Ref<number>,
 * canLoadMoreMessages: Ref<boolean>,
 * isLoadingMessages: Ref<boolean>,
 * messageLimitPerPage: Ref<number>,
 * replyingToMessage: Ref<object|null>,
 * searchResults: Ref<Array<object>>,
 * isSearchingMessages: Ref<boolean>,
 * currentSearchPage: Ref<number>,
 * totalSearchPages: Ref<number>,
 * canLoadMoreSearchResults: Ref<boolean>,
 * currentSearchTerm: Ref<string>,
 * activeConversationPartner: Ref<object>,
 * handleNewRealTimeMessage: Function,
 * getConversations: Function,
 * getMessages: Function,
 * setActiveConversation: Function,
 * selectFriendToChat: Function,
 * selectGroupChannelToChat: Function,
 * clearActiveConversation: Function,
 * setActiveFriend: Function,
 * setReplyingTo: Function,
 * clearReplyingTo: Function,
 * searchMessagesInConversation: Function,
 * loadMoreSearchResults: Function,
 * clearSearchResults: Function
 * }}
 * @property {Ref<Array<object>>} conversations - 用户的所有会话列表。
 * @property {Ref<Array<object>>} messages - 当前活跃会话的消息列表。
 * @property {Ref<object|null>} activeConversation - 当前活跃的会话对象。
 * @property {Ref<boolean>} isLoadingConversations - 会话列表是否正在加载中。
 * @property {Ref<number>} currentMessagePage - 当前消息列表已加载的页码。
 * @property {Ref<number>} totalMessagePages - 当前活跃会话的总消息页数。
 * @property {Ref<boolean>} canLoadMoreMessages - 是否可以加载更多消息。
 * @property {Ref<boolean>} isLoadingMessages - 消息列表是否正在加载中。
 * @property {Ref<number>} messageLimitPerPage - 每页加载的消息数量限制。
 * @property {Ref<object|null>} replyingToMessage - 当前正在回复的消息对象。
 * @property {Ref<Array<object>>} searchResults - 消息搜索结果列表。
 * @property {Ref<boolean>} isSearchingMessages - 消息是否正在搜索中。
 * @property {Ref<number>} currentSearchPage - 当前搜索结果已加载的页码。
 * @property {Ref<number>} totalSearchPages - 搜索结果的总页数。
 * @property {Ref<boolean>} canLoadMoreSearchResults - 是否可以加载更多搜索结果。
 * @property {Ref<string>} currentSearchTerm - 当前的搜索关键词。
 * @property {Ref<object>} activeConversationPartner - 当前活跃私聊会话的伙伴用户信息。
 * @property {Function} handleNewRealTimeMessage - 处理实时接收到的新消息。
 * @property {Function} getConversations - 获取用户所有会话列表。
 * @property {Function} getMessages - 获取指定会话的消息列表。
 * @property {Function} setActiveConversation - 设置当前活跃的会话。
 * @property {Function} selectFriendToChat - 通过好友信息选择并激活聊天会话。
 * @property {Function} selectGroupChannelToChat - 通过群组频道信息选择并激活聊天会话。
 * @property {Function} clearActiveConversation - 清除当前活跃会话状态。
 * @property {Function} setActiveFriend - 设置活跃会话伙伴(用于私聊)。
 * @property {Function} setReplyingTo - 设置要回复的消息。
 * @property {Function} clearReplyingTo - 清除回复消息状态。
 * @property {Function} searchMessagesInConversation - 在指定会话中搜索消息。
 * @property {Function} loadMoreSearchResults - 加载更多消息搜索结果。
 * @property {Function} clearSearchResults - 清空消息搜索结果。
 */
export const useChatStore = defineStore(
  'chat',
  () => {
    /** @type {Ref<Array<object>>} */
    const conversations = ref([]) // 用户的所有会话列表
    /** @type {Ref<Array<object>>} */
    const messages = ref([]) // 当前活跃会话的消息列表
    /** @type {Ref<object|null>} */
    const activeConversation = ref(null) // 当前活跃的会话对象
    const userStore = useUserStore()
    /** @type {Ref<boolean>} */
    const isLoadingConversations = ref(false) // 会话列表是否正在加载中
    /** @type {Ref<number>} */
    const currentMessagePage = ref(1) // 当前消息列表已加载的页码
    /** @type {Ref<number>} */
    const totalMessagePages = ref(1) // 当前活跃会话的总消息页数
    /** @type {Ref<boolean>} */
    const canLoadMoreMessages = ref(false) // 是否可以加载更多消息
    /** @type {Ref<boolean>} */
    const isLoadingMessages = ref(false) // 消息列表是否正在加载中
    /** @type {Ref<number>} */
    const messageLimitPerPage = ref(20) // 每页加载的消息数量限制
    /** @type {Ref<object|null>} */
    const replyingToMessage = ref(null) // 当前正在回复的消息内容

    // 消息搜索相关状态
    /** @type {Ref<Array<object>>} */
    const searchResults = ref([]) // 消息搜索结果列表
    /** @type {Ref<boolean>} */
    const isSearchingMessages = ref(false) // 消息是否正在搜索中
    /** @type {Ref<number>} */
    const currentSearchPage = ref(1) // 当前搜索结果已加载的页码
    /** @type {Ref<number>} */
    const totalSearchPages = ref(1) // 搜索结果的总页数
    /** @type {Ref<boolean>} */
    const canLoadMoreSearchResults = ref(false) // 是否可以加载更多搜索结果
    /** @type {Ref<string>} */
    const currentSearchTerm = ref('') // 当前的搜索关键词

    /**
     * @type {Ref<object>}
     * @description 当前活跃私聊会话的伙伴用户信息。
     */
    const activeConversationPartner = ref({
      _id: '',
      username: '你的好友',
      avatar: '/images/defaultUserAvatar.png'
    })

    /**
     * @function handleNewRealTimeMessage
     * @description 处理实时接收到的新消息。
     * 更新当前会话的消息列表,并更新会话列表中的最新消息和未读计数,同时触发桌面通知。
     * @param {object} newMessage - 新消息数据对象。
     * @param {string} newMessage.conversationId - 消息所属会话的 ID。
     * @param {string} newMessage.content - 消息内容。
     * @param {string} newMessage.messageType - 消息类型(如 'text', 'image')。
     * @param {string} newMessage.sender._id - 消息发送者的用户 ID。
     * @param {string} newMessage.sender.username - 消息发送者的用户名。
     * @param {string} newMessage.conversationType - 会话类型('private' 或 'group')。
     * @param {object} [newMessage.groupInfo] - 如果是群组消息,包含群组信息。
     * @returns {void}
     */
    const handleNewRealTimeMessage = (newMessage) => {
      // 传进来的消息必须要是当前打开的会话
      if (newMessage.conversationId === activeConversation.value?._id) {
        // 检测是否有一样 id 的信息,防止重复添加
        const exists = messages.value.some((m) => m._id === newMessage._id)
        if (!exists) {
          messages.value.push(newMessage)
        }
      }

      // 更新会话列表中的对应会话
      const conversationIndex = conversations.value.findIndex(
        (c) => c._id === newMessage.conversationId
      )

      if (conversationIndex !== -1) {
        // 拿到新消息所在的 conversation 的副本
        const updatedConversation = {
          ...conversations.value[conversationIndex]
        }

        updatedConversation.lastMessage = newMessage // 更新最后一条消息
        // 更新最后一条消息的片段,避免过长
        updatedConversation.lastMessageContentSnippet =
          newMessage.content.length > 30
            ? newMessage.content.substring(0, 27) + '...'
            : newMessage.content
        updatedConversation.lastMessageAt = newMessage.createdAt // 更新最后消息时间

        let notificationTitle = ''
        let notificationMessage = ''

        // 根据会话类型(私聊或群聊)构造通知标题
        if (newMessage.conversationType === 'group') {
          notificationTitle = `群组 [${newMessage.groupInfo?.name}] 有新消息,来自 ${newMessage.sender.username}`
        } else {
          notificationTitle = `有来自 ${newMessage.sender.username} 的新消息`
        }

        // 根据消息类型(图片或文本)构造通知内容
        if (newMessage.messageType === 'image') {
          notificationMessage = '[图片]'
        } else {
          notificationMessage = newMessage.content
        }

        // 判断是否需要增加未读消息计数和发送桌面通知
        // 条件:消息不属于当前打开的会话 且 消息不是当前用户自己发送的
        if (
          newMessage.conversationId !== activeConversation.value?._id &&
          newMessage.sender._id !== userStore.userInfo.userId
        ) {
          updatedConversation.unreadCount =
            (updatedConversation.unreadCount || 0) + 1 // 未读数加1
          // 如果会话未被静音,则发送通知
          if (!userStore.isMuted(newMessage.conversationId)) {
            emitter.emit('show-notification', {
              title: notificationTitle,
              message: notificationMessage
            })
          }
        }

        // 移除旧的会话条目,并将更新后的会话放在列表最前面(实现“最新消息置顶”效果)
        conversations.value.splice(conversationIndex, 1)
        conversations.value.unshift(updatedConversation)
      } else {
        console.warn(
          `收到新消息,但会话 ${newMessage.conversationId} 不在当前会话列表内。`
        )
      }
    }

    /**
     * @function getConversations
     * @description 获取当前用户的所有会话列表。
     * 在用户未登录时,清空会话列表。
     * @returns {Promise<void>}
     * @throws {Error} 如果获取会话列表失败,会打印错误信息。
     */
    const getConversations = async () => {
      if (!userStore.isLoggedIn) {
        conversations.value = [] // 未登录清空会话
        return
      }
      isLoadingConversations.value = true // 设置加载状态
      try {
        const response = await getUserConversations() // 调用 API 获取会话
        conversations.value = response.data // 更新会话列表
      } catch (error) {
        console.error('获取会话列表失败:', error)
      } finally {
        isLoadingConversations.value = false // 结束加载状态
      }
    }

    /**
     * @function getMessages
     * @description 获取指定会话的消息列表。
     * 支持分页加载,新消息会追加到现有列表。
     * @param {string} conversationId - 要获取消息的会话 ID。
     * @param {number} [pageToLoad=1] - 要加载的页码。
     * @returns {Promise<void>}
     * @throws {Error} 如果获取消息失败,会打印错误信息并清空消息列表。
     */
    const getMessages = async (conversationId, pageToLoad = 1) => {
      if (!conversationId) {
        console.warn('getMessages: conversationId 为空')
        isLoadingMessages.value = false
        return
      }
      isLoadingMessages.value = true // 设置消息加载状态
      try {
        const response = await getMessagesForConversation(
          // 调用 API 获取消息
          conversationId,
          pageToLoad,
          messageLimitPerPage.value
        )
        if (pageToLoad === 1) {
          messages.value = response.data.messages // 如果是第一页,替换所有消息
        } else {
          // 如果是加载更多页,将新消息添加到现有消息列表的开头
          messages.value = [...response.data.messages, ...messages.value]
        }
        currentMessagePage.value = response.data.currentPage // 更新当前页码
        totalMessagePages.value = response.data.totalPages // 更新总页数
        // 判断是否还有更多消息可加载
        canLoadMoreMessages.value =
          response.data.currentPage < response.data.totalPages
      } catch (error) {
        console.error('获取消息失败:', error)
        if (pageToLoad === 1) {
          messages.value = [] // 第一页加载失败时清空消息列表
        }
      } finally {
        isLoadingMessages.value = false // 结束消息加载状态
      }
    }

    /**
     * @function setActiveConversation
     * @description 设置当前活跃的会话,并加载其消息列表。
     * 如果会话有未读消息,会标记为已读。
     * @param {object} conversation - 要激活的会话对象。
     * @returns {Promise<void>}
     */
    const setActiveConversation = async (conversation) => {
      // 如果点击的是已经激活的会话,或者会话对象为空,则不重复加载
      if (conversation._id === activeConversation.value?._id || !conversation) {
        return
      }

      if (conversation._id) {
        activeConversation.value = conversation // 设置活跃会话
        messages.value = [] // 清空旧消息
        currentMessagePage.value = 1 // 重置分页
        totalMessagePages.value = 1
        canLoadMoreMessages.value = false
        isLoadingMessages.value = true // 设置加载状态
      }

      // 清除会话的未读数(前端UI更新)
      const convInList = conversations.value.find(
        (c) => c._id === conversation._id
      )
      if (convInList && convInList.unreadCount > 0) {
        convInList.unreadCount = 0 // 在前端UI上清除未读消息标志
        // 调用 API 标记消息为已读
        markAsRead(conversation._id).catch((err) => {
          console.error('标记已读失败:', err)
        })
      }

      try {
        await getMessages(conversation._id, 1) // 获取新会话的第一页消息
      } catch (error) {
        console.error('激活会话并加载消息失败:', error)
      } finally {
        isLoadingMessages.value = false // 结束加载状态
      }
    }

    /**
     * @function selectFriendToChat
     * @description 通过好友信息选择一个私聊会话。
     * 如果会话不存在,则创建新的会话。
     * @param {object} friend - 好友对象,包含 _id, username, avatar 等。
     * @returns {Promise<void>}
     */
    const selectFriendToChat = async (friend) => {
      if (!friend || !friend._id) {
        console.warn('selectFriendToChat: 好友信息不完整')
        return
      }

      // 如果当前已经与该好友处于活跃会话,则不重复操作
      if (activeConversation.value?.targetParticipant?._id === friend._id) {
        return
      }

      try {
        // 获取或创建与该好友的私聊会话
        const response = await getOrCreateConversation(friend._id)
        const newConversation = response.data

        // 如果新会话不在当前会话列表中,则添加到最前面
        const existInList = conversations.value.some(
          (c) => c._id === newConversation._id
        )
        if (!existInList) {
          conversations.value.unshift(newConversation)
        }
        await setActiveConversation(newConversation) // 激活该会话
      } catch (error) {
        console.error('选择好友聊天失败:', error)
      }
    }

    /**
     * @function selectGroupChannelToChat
     * @description 通过群组频道信息选择一个群组会话。
     * @param {object} channel - 群组频道对象,包含 _id, name 等信息。
     * @returns {Promise<void>}
     */
    const selectGroupChannelToChat = async (channel) => {
      if (!channel || !channel._id) {
        console.warn('selectGroupChannelToChat: 频道信息不完整')
        return
      }
      // 如果当前已经与该频道处于活跃会话,则不重复操作
      if (activeConversation.value?._id === channel._id) {
        return
      }
      // 构造一个会话对象并激活
      const channelConversation = { ...channel, type: 'group' }
      await setActiveConversation(channelConversation)
    }

    /**
     * @function clearActiveConversation
     * @description 清除当前活跃会话的状态。
     * @returns {void}
     */
    const clearActiveConversation = () => {
      activeConversation.value = null
    }

    /**
     * @function setActiveFriend
     * @description 设置活跃会话的伙伴信息(主要用于私聊显示)。
     * @param {object} friend - 好友对象。
     * @returns {void}
     */
    const setActiveFriend = (friend) => {
      activeConversationPartner.value = {
        _id: friend._id,
        username: friend.username,
        avatar: friend.avatar
      }
    }

    /**
     * @function setReplyingTo
     * @description 设置要回复的消息内容。
     * @param {object} message - 要回复的消息对象。
     * @returns {void}
     */
    const setReplyingTo = (message) => {
      replyingToMessage.value = message
    }

    /**
     * @function clearReplyingTo
     * @description 清除当前回复的消息状态。
     * @returns {void}
     */
    const clearReplyingTo = () => {
      replyingToMessage.value = null
    }

    /**
     * @function searchMessagesInConversation
     * @description 在指定会话中搜索消息。
     * 支持分页和加载更多搜索结果。
     * @param {string} conversationId - 要搜索的会话 ID。
     * @param {string} searchTerm - 搜索关键词。
     * @param {number} [page=1] - 要加载的搜索结果页码。
     * @returns {Promise<void>}
     */
    const searchMessagesInConversation = async (
      conversationId,
      searchTerm,
      page = 1
    ) => {
      if (!conversationId) {
        console.warn('searchMessagesInConversation: 会话 ID 不能为空')
        // return; // 根据实际需求决定是否中断
      }
      // 如果搜索词为空或只有空格,清空搜索结果并重置状态
      if (!searchTerm || searchTerm.trim() === '') {
        searchResults.value = []
        currentSearchPage.value = 1
        totalSearchPages.value = 1
        canLoadMoreSearchResults.value = false
        isSearchingMessages.value = false
        currentSearchTerm.value = ''
        return
      }

      isSearchingMessages.value = true // 设置搜索中状态
      currentSearchTerm.value = searchTerm // 更新当前搜索词

      try {
        const response = await searchMessage({
          conversationId,
          searchTerm,
          page
        })

        if (page === 1) {
          searchResults.value = response.data.messages // 第一页,替换所有结果
        } else {
          searchResults.value.push(...response.data.messages) // 加载更多,追加结果
        }

        currentSearchPage.value = response.data.currentPage // 更新当前搜索页码
        totalSearchPages.value = response.data.totalPages // 更新搜索结果总页数
        // 判断是否可以加载更多搜索结果
        canLoadMoreSearchResults.value =
          response.data.currentPage < response.data.totalPages
      } catch (error) {
        console.error('搜索消息失败:', error)
        // 搜索失败时清空结果并重置分页状态
        searchResults.value = []
        currentSearchPage.value = 1
        totalSearchPages.value = 1
        canLoadMoreSearchResults.value = false
      } finally {
        isSearchingMessages.value = false // 结束搜索中状态
      }
    }

    /**
     * @function loadMoreSearchResults
     * @description 加载更多消息搜索结果。
     * 仅在还有更多结果且当前未在搜索中时触发。
     * @param {string} conversationId - 当前活跃会话 ID。
     * @param {string} searchTerm - 当前搜索关键词。
     * @returns {Promise<void>}
     */
    const loadMoreSearchResults = async (conversationId, searchTerm) => {
      // 如果没有更多结果可加载或者当前正在搜索中,则返回
      if (!canLoadMoreSearchResults.value || isSearchingMessages.value) {
        return
      }
      await searchMessagesInConversation(
        conversationId,
        searchTerm,
        currentSearchPage.value + 1 // 加载下一页
      )
    }

    /**
     * @function clearSearchResults
     * @description 清空所有消息搜索结果并重置搜索状态。
     * @returns {void}
     */
    const clearSearchResults = () => {
      searchResults.value = []
      isSearchingMessages.value = false
      currentSearchPage.value = 1
      totalSearchPages.value = 1
      canLoadMoreSearchResults.value = false
      currentSearchTerm.value = ''
    }

    return {
      conversations,
      activeConversation,
      messages,
      isLoadingConversations,
      currentMessagePage,
      totalMessagePages,
      canLoadMoreMessages,
      isLoadingMessages,
      activeConversationPartner,
      replyingToMessage,
      searchResults,
      isSearchingMessages,
      currentSearchPage,
      totalSearchPages,
      canLoadMoreSearchResults,
      currentSearchTerm,
      handleNewRealTimeMessage,
      getConversations,
      getMessages,
      setActiveConversation,
      selectFriendToChat,
      selectGroupChannelToChat,
      clearActiveConversation,
      setActiveFriend,
      setReplyingTo,
      clearReplyingTo,
      searchMessagesInConversation,
      loadMoreSearchResults,
      clearSearchResults
    }
  },
  {
    // Pinia 持久化配置
    persist: {
      paths: ['activeConversation']
    }
  }
)