import type { Conversation, GroupLite, Message, QuoteMessage, User } from '../home/types'

import { useUserStore } from '..\..\..\packages\stores\src'

import { getCurrentUserId } from '#/views/im/utils/auth'

import { useFriendStore } from '../home/store/friendStore'
import { useGroupStore } from '../home/store/groupStore'
import {
  ImContentType,
  ImConversationType,
  type ImConversationTypeValue,
  ImRtcCallEndReason
} from './constants'
import { formatCallDuration } from './time'

export type { QuoteMessage } from '../home/types'

// ====================================================================
// IM 消息 content 编解码 & 展示工具
// ====================================================================
// 约定：消息的 content 字段统一存 JSON 字符串，字段名、结构对齐后端
// cn.iocoder.yudao.module.im.service.websocket.notification.message.* 下的 DTO。
// 各类消息 payload interface 字段对齐后端；解析统一用 parseMessage<T>，
// 序列化直接 JSON.stringify(payload)。
// ====================================================================

// ==================== 客户端 ID ====================

/** 生成客户端消息 ID（纯 UUID），用于前端去重 & ACK 回写 */
export const generateClientMessageId = (): string => {
  if (globalThis.crypto?.randomUUID) {
    return globalThis.crypto.randomUUID()
  }
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replaceAll(/[xy]/g, (char) => {
    const random = Math.trunc(Math.random() * 16)
    const value = char === 'x' ? random : (random & 0x3) | 0x8
    return value.toString(16)
  })
}

// ==================== 私聊对端 userId ====================

/**
 * 私聊消息 / DTO 的对端 userId：自己发的对端是 receiver，别人发的对端是 sender
 *
 * 收口 4 处旧 inline（websocketStore.convertPrivateMessage / handlePrivateMessage / computeFriendPeerId
 * 和 useMessagePuller.getPrivatePeerId），结构类型只要 senderId / receiverId 两个字段，REST 与 WS DTO 都满足
 */
export function getPrivateMessagePeerId(
  message: { receiverId: number; senderId: number; },
  currentUserId: number
): number {
  return message.senderId === currentUserId ? message.receiverId : message.senderId
}

// ==================== 文本片段（tip 文案 + TEXT 气泡共用） ====================
// 既用于灰条 tip（"XX 邀请 YY 加入群聊"），也用于 TEXT 气泡正文（@xxx 高亮 + URL 自动识别）。
// mention 段携带 userId 用于挂点击弹 UserInfoCard；link 段携带 href 用于 <a> 跳转；
// text 段是纯文本兜底。渲染层（tip-segments.vue）按 type 分发统一处理。

export type TipSegment =
  | { href: string; text: string; type: 'link'; }
  | { text: string; type: 'mention'; userId: number; }
  | { text: string; type: 'text'; }

export const tipText = (text: string): TipSegment => ({ type: 'text', text })

export const tipMention = (userId: number, text: string): TipSegment => ({
  type: 'mention',
  userId,
  text
})

export const tipLink = (href: string, text: string): TipSegment => ({
  type: 'link',
  href,
  text
})

export const segmentsToText = (segments: TipSegment[]): string =>
  segments.map((s) => s.text).join('')

/** 多个 userId 用同一个分隔符插值成 segments，每个 user 单独成 mention 段 */
export function joinMentionSegments(
  userIds: number[],
  separator: string,
  resolveName: (userId: number) => string
): TipSegment[] {
  return userIds.flatMap((id, index) =>
    index === 0
      ? [tipMention(id, resolveName(id))]
      : [tipText(separator), tipMention(id, resolveName(id))]
  )
}

/** mention 候选；name 用于匹配文本字面量，displayName 用于覆盖渲染（不传则按 name 渲染） */
export interface MentionCandidate {
  userId: number
  /** 用来在 content 中前缀匹配的字面量（不含 @） */
  name: string
  /**
   * 渲染时强制使用的显示名（不含 @）
   *
   * 用于「历史消息 content 里写的是备注名 / 群昵称，但接收端要按真实昵称渲染」的场景；
   * 候选可以携带多种字面量（match 用），但 displayName 统一指向 nickname，
   * 让所有历史 / 新消息的 @ 都收敛到一致的展示
   */
  displayName?: string
  /**
   * 歧义标记：同 name 对应多个 userId 时为 true
   *
   * parser 仍会按 name 命中（最长优先），但命中后整段吃成普通文本——
   * 避免「@张三」被剔除后，短前缀候选「@张」抢匹配错绑
   */
  ambiguous?: boolean
}

/** URL 锚定正则；终止于空白、中文、@（避免吞掉下一个 mention）、< > " '（防 HTML / 引号串入）；y 标志走 sticky 匹配，省 text.slice */
const URL_STICKY_REGEX = /(https?:\/\/[^\s一-龥@<>"']+|www\.[^\s一-龥@<>"']+)/iy

/** URL 末尾常见标点剔除，避免吞掉句末中英文标点 */
const URL_TRAILING_PUNCTUATION = /[.,!?;:)\]、，。！？；：）】]+$/

/** 最短 URL：`www.ab` / `http:/` 这种孤段不算链接 */
const URL_MIN_LENGTH = 6

/**
 * 文本气泡 content 拆段：mention 段按候选最长前缀匹配，URL 段走锚定正则，剩余归 text
 *
 * mentions 不传或匹配不上时，@xxx 退化为普通 text；解析与渲染解耦，工具函数本身不依赖 store
 */
export function parseTextSegments(text: string, mentions: MentionCandidate[] = []): TipSegment[] {
  if (!text) {
    return []
  }
  // 「@张三丰」不能被「@张三」截胡，候选按 name 长度倒序
  const sortedMentions =
    mentions.length > 1 ? mentions.toSorted((a, b) => b.name.length - a.name.length) : mentions
  const out: TipSegment[] = []
  let buffer = ''

  const flush = () => {
    if (buffer) {
      out.push(tipText(buffer))
      buffer = ''
    }
  }

  let i = 0
  while (i < text.length) {
    if (text[i] === '@' && sortedMentions.length > 0) {
      const matched = sortedMentions.find((m) => m.name && text.startsWith(m.name, i + 1))
      if (matched) {
        if (matched.ambiguous) {
          // 同字面量对应多 userId，无法判定意图；整段累入 buffer 让短前缀候选没机会再扫这段
          buffer += '@' + matched.name
        } else {
          flush()
          // 渲染统一走 displayName（即真实昵称），让历史 content 里残留的备注 / 群昵称也收敛到 nickname
          out.push(tipMention(matched.userId, '@' + (matched.displayName || matched.name)))
        }
        i += 1 + matched.name.length
        continue
      }
    }
    const head = text[i]
    if (head === 'h' || head === 'H' || head === 'w' || head === 'W') {
      URL_STICKY_REGEX.lastIndex = i
      const linkMatch = URL_STICKY_REGEX.exec(text)
      if (linkMatch) {
        const urlText = linkMatch[0].replace(URL_TRAILING_PUNCTUATION, '')
        if (urlText.length >= URL_MIN_LENGTH) {
          flush()
          const href = /^https?:\/\//i.test(urlText) ? urlText : `https://${urlText}`
          out.push(tipLink(href, urlText))
          i += urlText.length
          continue
        }
      }
    }
    buffer += text[i]
    i += 1
  }
  flush()
  return out
}

// ==================== 引用消息 ====================

/** 引用容器：5 种普通消息(TEXT / IMAGE / FILE / VOICE / VIDEO)都可携带 quote */
interface Quotable {
  quote?: QuoteMessage
}

// ==================== 消息 payload ====================

/** 文本消息 payload（对齐后端 TextMessage） */
export interface TextMessage extends Quotable {
  content: string
}

/** 图片消息 payload（对齐后端 ImageMessage） */
export interface ImageMessage extends Quotable {
  url: string
  /** 缩略图 URL */
  thumbnailUrl?: string
  /** 图片宽度 */
  width?: number
  /** 图片高度 */
  height?: number
  /** 文件大小（字节） */
  size?: number
}

/** 语音消息 payload（对齐后端 AudioMessage；ImContentType 保留 VOICE 命名） */
export interface AudioMessage extends Quotable {
  url: string
  /** 时长（秒） */
  duration: number
  /** 文件大小（字节） */
  size?: number
}

/** 文件消息 payload（对齐后端 FileMessage） */
export interface FileMessage extends Quotable {
  url: string
  name: string
  size: number
  /** MIME 类型 */
  type?: string
}

/** 视频消息 payload（对齐后端 VideoMessage；暂未接入渲染） */
export interface VideoMessage extends Quotable {
  url: string
  /** 封面 URL */
  coverUrl?: string
  /** 时长（秒） */
  duration?: number
  width?: number
  height?: number
  /** 文件大小（字节） */
  size?: number
}

/**
 * 名片消息 payload（对齐后端 CardMessage）
 *
 * 按 targetType 区分用户名片 / 群名片：
 * - PRIVATE：targetId = 用户编号，name = 用户昵称（取真实昵称，非备注）
 * - GROUP：targetId = 群编号，name = 群名，可带 memberCount
 */
export interface CardMessage extends Quotable {
  /** 名片对象类型 */
  targetType: ImConversationTypeValue
  /** 目标对象编号：PRIVATE 时 = userId；GROUP 时 = groupId */
  targetId: number
  /** 显示名快照：PRIVATE 时 = 用户昵称；GROUP 时 = 群名 */
  name: string
  /** 头像（快照） */
  avatar?: string
  /** 群成员数（仅 targetType = GROUP；接收端展示「N 人群聊」） */
  memberCount?: number
}

/**
 * 名片转发的源对象（RecommendCardDialog 入参）；字段与 CardMessage 1:1，无 quote
 */
export type CardTarget = Omit<CardMessage, 'quote'>

/** 用户对象 → 用户名片源（PRIVATE）；缺 id 返回 null 让调用方 v-bind 绑定为不渲染 */
export function toUserCardTarget(user: null | undefined | User): CardTarget | null {
  if (!user?.id) {
    return null
  }
  return {
    targetType: ImConversationType.PRIVATE,
    targetId: user.id,
    name: user.nickname || '',
    avatar: user.avatar
  }
}

/** 群对象 → 群名片源（GROUP）；缺 id 返回 null */
export function toGroupCardTarget(group: GroupLite | null | undefined): CardTarget | null {
  if (!group?.id) {
    return null
  }
  return {
    targetType: ImConversationType.GROUP,
    targetId: group.id,
    name: group.name || '',
    avatar: group.showImage || group.showImageThumb,
    memberCount: group.memberCount
  }
}

/**
 * 名片标签 + 图标（按 targetType 二分），统一聊天气泡 / 引用预览 / 历史摘要 / 后台预览的文案与图标
 *
 * 缺失 / 非法 targetType 走「个人名片」兜底，避免老消息或脏数据导致 UI 空白
 */
export function getCardLabelInfo(card: null | undefined | { targetType?: number }): {
  icon: string
  label: string
} {
  if (card?.targetType === ImConversationType.GROUP) {
    return { label: '群名片', icon: 'ant-design:usergroup-outlined' }
  }
  return { label: '个人名片', icon: 'ant-design:user-outlined' }
}

/** 表情消息 payload（对齐后端 FaceMessage；Unicode emoji 仍走 TEXT，本类型只承载贴图 / 自定义表情包） */
export interface FaceMessage extends Quotable {
  /** 表情图 URL */
  url: string
  /** 渲染宽度（像素），避免布局抖动；可选，缺失时调用方走 CSS max-w 兜底 */
  width?: number
  /** 渲染高度（像素），可选 */
  height?: number
  /** 表情名（系统包通常有，个人表情包通常无） */
  name?: string
}

/** 合并转发的单条内嵌消息快照（对齐后端 MergeMessage.Item） */
export interface MergeMessageItem {
  /** 原消息编号；仅做溯源标识 */
  messageId: number
  /** 发送人编号 */
  senderId: number
  /** 发送人昵称快照；对端可能不在原会话里，无法实时查到 */
  senderNickname: string
  /** 发送人头像快照 */
  senderAvatar?: string
  /** 内容类型，对齐 ImContentType */
  type: number
  /** 原消息 content（JSON）；嵌套合并消息时仍按本结构层层展开 */
  content: string
  /** 发送时间戳（毫秒） */
  sendTime: number
}

/** 合并转发消息 payload（对齐后端 MergeMessage） */
export interface MergeMessage {
  title: string // 合并标题；例：「张三和李四的聊天记录」「群聊的聊天记录」
  messages: MergeMessageItem[] // 内嵌的完整消息快照
}

/** 频道素材消息 payload（对齐后端 MaterialMessage） */
export interface MaterialMessage {
  materialId?: number
  channelId?: number // 频道编号；转发后渲染卡片底部的频道头像 + 名称
  title?: string
  coverUrl?: string
  summary?: string
  url?: string // 跳转链接；为空时点击在客户端内置详情页按 materialId 拉 content 渲染；非空则跳 url
}

// ==================== 合并转发 payload 构造 ====================

/** 单个发送人的快照昵称 / 头像 */
interface SenderSnapshot {
  nickname: string
  avatar: string
}

/**
 * 一次性构造 senderId → SenderSnapshot 映射；避免 N 条消息逐条 find 群成员
 *
 * 群聊从 group.members 一次性 indexBy；私聊只需 self + friend；外加全体 senderId 上做 friend 兜底
 */
function buildSenderSnapshotMap(
  senderIds: number[],
  conversation: Conversation
): Map<number, SenderSnapshot> {
  const userStore = useUserStore()
  const friendStore = useFriendStore()
  const selfUserId = getCurrentUserId()
  const result = new Map<number, SenderSnapshot>()

  let groupMembers: Map<number, { avatar?: string; nickname: string; }> | null = null
  if (conversation.type === ImConversationType.GROUP) {
    const group = useGroupStore().getGroup(conversation.targetId)
    groupMembers = new Map((group?.members || []).map((m) => [m.userId, m]))
  }

  for (const senderId of senderIds) {
    if (result.has(senderId)) {
      continue
    }
    if (senderId === selfUserId) {
      result.set(senderId, {
        nickname: userStore.userInfo?.nickname || String(senderId),
        avatar: userStore.userInfo?.avatar || ''
      })
      continue
    }
    const member = groupMembers?.get(senderId)
    if (member?.nickname) {
      result.set(senderId, { nickname: member.nickname, avatar: member.avatar || '' })
      continue
    }
    const friend = friendStore.getFriend(senderId)
    result.set(senderId, {
      nickname: friend?.nickname || String(senderId),
      avatar: friend?.avatar || ''
    })
  }
  return result
}

/** 把单条 Message 转成 MergeMessageItem 快照；剥离 quote 防引用穿透 */
function mapMessageToMergeItem(
  message: Message,
  senderSnapshots: Map<number, SenderSnapshot>
): MergeMessageItem {
  const snapshot = senderSnapshots.get(message.senderId)
  return {
    messageId: message.id || 0,
    senderId: message.senderId,
    senderNickname: snapshot?.nickname ?? String(message.senderId),
    senderAvatar: snapshot?.avatar ?? '',
    type: message.type,
    content: removeQuotePayload(message.content),
    sendTime: message.sendTime
  }
}

/** 合并转发标题：私聊「{对方} 和 {自己} 的聊天记录」；群聊「{群名} 的聊天记录」 */
export function buildMergeTitle(conversation: Conversation): string {
  if (conversation.type === ImConversationType.GROUP) {
    return `${conversation.name || '群聊'} 的聊天记录`
  }
  const myName = useUserStore().userInfo?.nickname || '我'
  return `${conversation.name || '对方'} 和 ${myName} 的聊天记录`
}

/** 把一组 Message 打包成 MergeMessage payload；调用方负责按 sendTime 排序后传入 */
export function buildMergeMessagePayload(
  messages: Message[],
  conversation: Conversation
): MergeMessage {
  const senderIds = messages.map((m) => m.senderId)
  const senderSnapshots = buildSenderSnapshotMap(senderIds, conversation)
  return {
    title: buildMergeTitle(conversation),
    messages: messages.map((m) => mapMessageToMergeItem(m, senderSnapshots))
  }
}

/** 「添加到表情」的可发起源：FACE / IMAGE 都允许（GIF 图片也常被收藏） */
export interface AddableFacePayload {
  url: string
  width: number
  height: number
  name?: string
}

/**
 * 从消息抽取「添加到表情」的 payload；当前内容类型不可添加返回 null
 *
 * 调用方（MessageItem 的右键菜单）按 nullable 决定是否展示「添加到表情」入口
 */
export function extractAddableFace(message: Message): AddableFacePayload | null {
  if (message.type === ImContentType.FACE) {
    const face = parseMessage<FaceMessage>(message.content)
    if (!face?.url) {
      return null
    }
    return { url: face.url, width: face.width || 200, height: face.height || 200, name: face.name }
  }
  if (message.type === ImContentType.IMAGE) {
    const image = parseMessage<ImageMessage>(message.content)
    if (!image?.url) {
      return null
    }
    return { url: image.url, width: image.width || 200, height: image.height || 200 }
  }
  return null
}

/** 解析消息 content（JSON 字符串）为指定 payload，非法 JSON 返回 null */
export const parseMessage = <T>(content: string): null | T => {
  try {
    return JSON.parse(content) as T
  } catch {
    return null
  }
}

/** 序列化消息 payload 为 content JSON 字符串；与 parseMessage 对称 */
export const serializeMessage = <T>(payload: T): string => JSON.stringify(payload)

/** `URL.createObjectURL(file)` 生成的 URL 前缀；占位 / revoke / 重传旧值识别共用 */
export const BLOB_URL_PREFIX = 'blob:'

/**
 * 媒体 payload 里可能包含 blob URL 的字段（图片/文件/视频/语音都对齐这套 url 字段命名）
 *
 * 跟随 ImageMessage / VideoMessage / FileMessage / AudioMessage interface 定义同步：
 * - url：主体资源（占位时是 blob URL，ack 后是真实 URL）
 * - coverUrl：视频封面（commit 后是真实 URL；占位阶段不设以避免传 blob 当 poster 在部分浏览器退化）
 * - thumbnailUrl：图片缩略图（当前未占位时使用 blob，预留）
 */
const MEDIA_BLOB_URL_FIELDS = ['url', 'coverUrl', 'thumbnailUrl'] as const

/**
 * 释放 content 中媒体 payload 字段上的 blob URL 内存映射
 *
 * 仅扫描 url / coverUrl / thumbnailUrl 三个已知字段，避免 regex 全文 grep 误伤 quote.content 里嵌套的同名 blob URL。
 * 仅对当前 document 内创建的 blob URL 有效；IndexedDB 恢复出来的旧 blob URL 已随旧 document 失效，调它无害但无意义
 */
export const revokeBlobUrlsInContent = (content: string): void => {
  if (!content || !content.includes(BLOB_URL_PREFIX)) {
    return
  }
  const payload = parseMessage<Record<string, unknown>>(content)
  if (!payload) {
    return
  }
  for (const field of MEDIA_BLOB_URL_FIELDS) {
    const value = payload[field]
    if (typeof value === 'string' && value.startsWith(BLOB_URL_PREFIX)) {
      URL.revokeObjectURL(value)
    }
  }
}

// ==================== 引用消息 helper ====================

/** 把 quote 合进 payload(序列化前调用);quote 缺失时原样返回 */
export const withQuotePayload = <T extends Quotable>(payload: T, quote?: QuoteMessage): T => {
  if (!quote) {
    return payload
  }
  return { ...payload, quote }
}

/**
 * 从 content JSON 字符串里清掉 quote 字段
 *
 * 客户端乐观渲染构造 quote 时调用,避免"回复一条带引用的消息"造成 quote 嵌套滚雪球;
 * 与后端 ImMessageUtils.removeQuote 对齐
 */
export const removeQuotePayload = (content: string): string => {
  if (!content || !content.includes('"quote"')) {
    return content
  }
  const parsed = parseMessage<Record<string, unknown>>(content)
  if (!parsed || !('quote' in parsed)) {
    return content
  }
  delete parsed.quote
  return JSON.stringify(parsed)
}

/** 由 Message 派生 QuoteMessage 用于乐观渲染;ack 后会被服务端权威版本覆盖 */
export const buildQuoteFromMessage = (message: Message): QuoteMessage => {
  return {
    messageId: message.id || 0,
    senderId: message.senderId,
    type: message.type,
    content: removeQuotePayload(message.content)
  }
}

/** 从已序列化 message.content 中解出 quote;非 JSON / 无 quote 返回 null */
export const getQuoteFromMessage = (content: string): null | QuoteMessage => {
  // 长会话每条消息渲染都走 quote computed,非引用消息字符串预扫直接返回,免一次 JSON.parse
  if (!content || !content.includes('"quote"')) {
    return null
  }
  const parsed = parseMessage<Quotable>(content)
  return parsed?.quote ?? null
}

// ==================== 撤回 ====================

/**
 * 从后端下发的撤回 RecallMessage content 中解析出被撤回的原消息 id
 * content 形如 `{"messageId": 123}`，若不含 messageId 则返回 0（表示这条不是撤回消息）
 */
export const parseRecallMessageId = (content: string): number => {
  try {
    const parsed = JSON.parse(content)
    return parsed?.messageId == null ? 0 : Number(parsed.messageId)
  } catch {
    return 0
  }
}

// ==================== 新消息提示音 ====================

import tipAudioUrl from '#/assets/audio/im/message-tip.mp3'

/**
 * 新消息提示音，带 1 秒节流：短时间内多条消息只响一次，避免疲劳轰炸。
 *
 * 浏览器自动播放策略要求页面有过用户交互；若无法播放会静默失败。
 */
let __lastPlayAudioTipAt = 0
let __tipAudio: HTMLAudioElement | null = null
export const playAudioTip = () => {
  const now = Date.now()
  if (now - __lastPlayAudioTipAt < 1000) {
    return
  }
  __lastPlayAudioTipAt = now
  try {
    if (!__tipAudio) {
      __tipAudio = new Audio(tipAudioUrl)
      __tipAudio.preload = 'auto'
    }
    __tipAudio.currentTime = 0
    const playPromise = __tipAudio.play()
    if (playPromise && typeof playPromise.catch === 'function') {
      playPromise.catch((error) => console.debug('[IM] playAudioTip 失败', error))
    }
  } catch (error) {
    console.debug('[IM] playAudioTip 失败', error)
  }
}

// ==================== 文件图标 ====================

/**
 * 按文件扩展名挑文件图标 + 颜色，对齐微信观感
 *
 * message-item.vue（主气泡）和 reply-preview.vue（引用预览）共用同一份映射，避免视觉两处不一致
 */
export function getFileIconInfo(filename: string | undefined): { color: string; icon: string; } {
  const ext = (filename || '').split('.').pop()?.toLowerCase() || ''
  if (ext === 'pdf') {
    return { icon: 'ant-design:file-pdf-filled', color: '#ed5757' }
  }
  if (['doc', 'docx'].includes(ext)) {
    return { icon: 'ant-design:file-word-filled', color: '#2b7cd3' }
  }
  if (['xls', 'xlsx'].includes(ext)) {
    return { icon: 'ant-design:file-excel-filled', color: '#1f7244' }
  }
  if (['ppt', 'pptx'].includes(ext)) {
    return { icon: 'ant-design:file-ppt-filled', color: '#d24726' }
  }
  if (['7z', 'gz', 'rar', 'tar', 'zip'].includes(ext)) {
    return { icon: 'ant-design:file-zip-filled', color: '#f0ad4e' }
  }
  if (['bmp', 'gif', 'jpeg', 'jpg', 'png', 'svg', 'webp'].includes(ext)) {
    return { icon: 'ant-design:file-image-filled', color: '#9c27b0' }
  }
  if (['avi', 'flv', 'mkv', 'mov', 'mp4', 'wmv'].includes(ext)) {
    return { icon: 'ant-design:video-camera-filled', color: '#9c27b0' }
  }
  if (['aac', 'flac', 'mp3', 'ogg', 'wav'].includes(ext)) {
    return { icon: 'ant-design:audio-filled', color: '#9c27b0' }
  }
  if (['json', 'log', 'md', 'txt', 'xml'].includes(ext)) {
    return { icon: 'ant-design:file-text-filled', color: '#909399' }
  }
  return { icon: 'ant-design:file-filled', color: '#909399' }
}

// ==================== 管理后台展示工具 ====================

/** 详情弹窗里把 content JSON 美化成 2 缩进 */
export const formatJson = (content?: string): string => {
  if (!content) return ''
  try {
    return JSON.stringify(JSON.parse(content), null, 2)
  } catch {
    return content
  }
}

// ==================== 群广播事件 payload + 文案 ====================

// 群广播事件 payload；对齐后端 GroupNotificationMessage 子类聚合字段
export type GroupNotificationPayload = {
  addSource?: number
  // 全群封禁
  banned?: boolean
  displayUserName?: string
  // 自由进群事件
  entrantUserId?: number
  memberUserIds?: number[]
  // PIN 事件携带的被置顶消息展示数据
  message?: {
    atUserIds?: number[]
    content: string
    groupId: number
    id: number
    receiverUserIds?: number[]
    senderId: number
    sendTime: string
    type: number
  }
  messageId?: number
  // 禁言事件
  mutedUserId?: number
  muteEndTime?: string
  newAvatar?: string
  newJoinApproval?: boolean
  newName?: string
  newNotice?: string
  newOwnerUserId?: number
  oldAvatar?: string
  oldJoinApproval?: boolean
  oldName?: string
  oldNotice?: string
  operatorUserId?: number
}

/**
 * 群广播事件 segments
 *
 * resolveName 由调用方注入（默认场景传 getSenderDisplayName）；
 * operatorNameOverride 仅覆盖 operator 段文案，mention userId 仍用 payload.operatorUserId
 */
export function resolveGroupNotificationSegments(
  message: { content?: string; targetId?: number; type?: number; },
  resolveName: (userId: number) => string,
  operatorNameOverride?: string
): TipSegment[] {
  let payload: GroupNotificationPayload
  try {
    payload = JSON.parse(message.content || '{}')
  } catch {
    return []
  }
  // ENTER 主语是 entrant 而非 operator，独立处理；其它 case 都以 operatorUserId 为主语
  if (message.type === ImContentType.GROUP_MEMBER_ENTER) {
    const entrantId = payload.entrantUserId ?? payload.operatorUserId
    return entrantId ? [tipMention(entrantId, resolveName(entrantId)), tipText(' 加入了群聊')] : []
  }
  if (!payload.operatorUserId) {
    return []
  }
  const operatorSegment = tipMention(
    payload.operatorUserId,
    operatorNameOverride ?? resolveName(payload.operatorUserId)
  )
  const memberSegments = joinMentionSegments(payload.memberUserIds || [], '、', resolveName)

  switch (message.type) {
    case ImContentType.GROUP_ADMIN_ADD: {
      return [operatorSegment, tipText(' 将 '), ...memberSegments, tipText(' 设为管理员')]
    }
    case ImContentType.GROUP_ADMIN_REMOVE: {
      return [operatorSegment, tipText(' 撤销了 '), ...memberSegments, tipText(' 的管理员身份')]
    }
    case ImContentType.GROUP_BANNED: {
      return [operatorSegment, tipText(payload.banned ? ' 封禁了该群' : ' 解封了该群')]
    }
    case ImContentType.GROUP_CANCEL_MUTED: {
      return [operatorSegment, tipText(' 关闭了全群禁言')]
    }
    case ImContentType.GROUP_CREATE: {
      return [operatorSegment, tipText(' 创建了群聊')]
    }
    case ImContentType.GROUP_DISSOLVE: {
      return [operatorSegment, tipText(' 解散了群聊')]
    }
    case ImContentType.GROUP_INFO_UPDATE: {
      return payload.newAvatar
        ? [operatorSegment, tipText(' 更换了群头像')]
        : [operatorSegment, tipText(' 更新了群信息')]
    }
    case ImContentType.GROUP_MEMBER_CANCEL_MUTED: {
      return payload.mutedUserId
        ? [
            operatorSegment,
            tipText(' 解除了 '),
            tipMention(payload.mutedUserId, resolveName(payload.mutedUserId)),
            tipText(' 的禁言')
          ]
        : []
    }
    case ImContentType.GROUP_MEMBER_INVITE: {
      return [operatorSegment, tipText(' 邀请 '), ...memberSegments, tipText(' 加入群聊')]
    }
    case ImContentType.GROUP_MEMBER_KICK: {
      return [operatorSegment, tipText(' 移出了 '), ...memberSegments]
    }
    case ImContentType.GROUP_MEMBER_MUTED: {
      return payload.mutedUserId
        ? [
            operatorSegment,
            tipText(' 将 '),
            tipMention(payload.mutedUserId, resolveName(payload.mutedUserId)),
            tipText(' 禁言')
          ]
        : []
    }
    case ImContentType.GROUP_MEMBER_NICKNAME_UPDATE: {
      return [operatorSegment, tipText(` 修改群昵称为 "${payload.displayUserName ?? ''}"`)]
    }
    case ImContentType.GROUP_MEMBER_QUIT: {
      return [operatorSegment, tipText(' 退出了群聊')]
    }
    case ImContentType.GROUP_MESSAGE_PIN: {
      return [operatorSegment, tipText(' 置顶了一条消息')]
    }
    case ImContentType.GROUP_MESSAGE_UNPIN: {
      return [operatorSegment, tipText(' 取消了一条置顶消息')]
    }
    case ImContentType.GROUP_MUTED: {
      return [operatorSegment, tipText(' 开启了全群禁言')]
    }
    case ImContentType.GROUP_NAME_UPDATE: {
      return [operatorSegment, tipText(` 将群名修改为 "${payload.newName ?? ''}"`)]
    }
    case ImContentType.GROUP_NOTICE_UPDATE: {
      return [operatorSegment, tipText(' 更新了群公告')]
    }
    case ImContentType.GROUP_OWNER_TRANSFER: {
      return payload.newOwnerUserId
        ? [
            operatorSegment,
            tipText(' 已将群主转让给 '),
            tipMention(payload.newOwnerUserId, resolveName(payload.newOwnerUserId))
          ]
        : []
    }
    default: {
      return []
    }
  }
}

/** 群广播事件中文文案 */
export function resolveGroupNotificationText(
  message: { content?: string; targetId?: number; type?: number; },
  resolveName: (userId: number) => string,
  operatorNameOverride?: string
): string {
  return segmentsToText(
    resolveGroupNotificationSegments(message, resolveName, operatorNameOverride)
  )
}

// ==================== 好友事件 ====================

/** 会话内好友事件 segments */
export function resolveFriendNotificationSegments(message: { type?: number }): TipSegment[] {
  switch (message.type) {
    case ImContentType.FRIEND_ADD: {
      return [tipText('你们已经是好友了，开始聊天吧')]
    }
    case ImContentType.FRIEND_DELETE: {
      return [tipText('你已删除好友')]
    }
    default: {
      return []
    }
  }
}

/** 会话内好友事件文案：FRIEND_ADD / FRIEND_DELETE 渲染成灰色提示气泡，文案固定不依赖 payload */
export function resolveFriendNotificationText(message: { type?: number }): string {
  return segmentsToText(resolveFriendNotificationSegments(message))
}

// ==================== RTC 通话事件 ====================

// RTC_CALL_START payload；仅群聊；用于聊天 tip 文案「{inviter} 发起了{voice/video}通话」
export type RtcCallStartPayload = {
  conversationType?: number
  inviterAvatar?: string
  inviterNickname?: string
  inviterUserId?: number
  mediaType?: number
  room?: string
}

// RTC_CALL_END payload；私聊准气泡 + 群聊「通话已结束 [时长 X]」共用
export type RtcCallEndPayload = {
  conversationType?: number
  durationSeconds?: number
  endReason?: number
  mediaType?: number
  operatorAvatar?: string
  operatorNickname?: string
  operatorUserId?: number
  room?: string
}

/** 解析 RTC_CALL_START / RTC_CALL_END 消息 content；解析失败返回 null */
export function parseRtcCallPayload(
  content?: string
): null | (RtcCallEndPayload & RtcCallStartPayload) {
  return content ? parseMessage<RtcCallEndPayload & RtcCallStartPayload>(content) : null
}

/**
 * 会话内通话事件 segments（RTC_CALL_START / RTC_CALL_END）
 * <p>
 * 群聊两段式：START「{inviter} 发起了语音通话」+ END「语音通话已经结束」
 * <p>
 * 私聊气泡走 {@link resolveRtcCallPrivateBubbleText}
 */
export function resolveRtcCallTipSegments(message: {
  content?: string
  selfSend?: boolean
  type?: number
}): TipSegment[] {
  const payload = parseRtcCallPayload(message.content)
  if (!payload) {
    return []
  }
  if (message.type === ImContentType.RTC_CALL_START) {
    return payload.inviterUserId
      ? [
          tipMention(payload.inviterUserId, resolveRtcInviterLabel(payload)),
          tipText(' 发起了语音通话')
        ]
      : []
  }
  if (message.type === ImContentType.RTC_CALL_END) {
    return [tipText('语音通话已经结束')]
  }
  return []
}

/** 取 RTC 通话发起人展示名；昵称为空时回退「用户 {id}」 */
function resolveRtcInviterLabel(payload: RtcCallStartPayload): string {
  return payload.inviterNickname?.trim() || `用户 ${payload.inviterUserId ?? ''}`
}

/**
 * 会话列表最后一条预览文案（RTC_CALL_START / RTC_CALL_END）
 * <p>
 * 私聊：START / END 都展示「[语音通话]」，对齐 [图片] / [语音] 风格
 * <p>
 * 群聊：START「{inviter} 发起了语音通话」、END「语音通话已经结束」，与聊天 tip 同源
 */
export function resolveRtcCallLastContent(
  message: { content?: string; type?: number; },
  conversationType: number
): string {
  if (conversationType === ImConversationType.PRIVATE) {
    return '[语音通话]'
  }
  if (message.type === ImContentType.RTC_CALL_END) {
    return '语音通话已经结束'
  }
  if (message.type === ImContentType.RTC_CALL_START) {
    const payload = parseRtcCallPayload(message.content)
    if (payload) {
      return `${resolveRtcInviterLabel(payload)} 发起了语音通话`
    }
  }
  return ''
}

/**
 * 私聊 RTC_CALL_END 气泡内文案；按 operatorUserId 是不是自己渲染两端不同文案（对齐微信）
 * <p>
 * 文案规则：
 *   HANGUP duration > 0 → 「通话时长 N」（双方一致）
 *   HANGUP duration ≤ 0 → 「通话中断」（未接通的兜底）
 *   CANCEL → 操作者「已取消」/ 另一方「对方已取消」
 *   REJECT → 操作者「已拒绝」/ 另一方「对方已拒绝」
 *   BUSY → 操作者「忙线未接听」/ 另一方「对方忙线中」
 *   NO_ANSWER → 操作者「未接听」/ 另一方「对方未接听」（振铃超时 Job 触发）
 *   ERROR → 「通话中断 [N]」（接通后异常断开；duration > 0 时带时长）
 */
export function resolveRtcCallPrivateBubbleText(payload: null | RtcCallEndPayload): string {
  if (!payload) {
    return '通话已结束'
  }
  const duration = payload.durationSeconds ?? 0
  const hasDuration = duration > 0
  const isOperator = payload.operatorUserId === getCurrentUserId()
  switch (payload.endReason) {
    case ImRtcCallEndReason.BUSY: {
      return isOperator ? '忙线未接听' : '对方忙线中'
    }
    case ImRtcCallEndReason.CANCEL: {
      return isOperator ? '已取消' : '对方已取消'
    }
    case ImRtcCallEndReason.ERROR: {
      return hasDuration ? `通话中断 ${formatCallDuration(duration)}` : '通话中断'
    }
    case ImRtcCallEndReason.HANGUP: {
      return hasDuration ? `通话时长 ${formatCallDuration(duration)}` : '通话中断'
    }
    case ImRtcCallEndReason.NO_ANSWER: {
      return isOperator ? '未接听' : '对方未接听'
    }
    case ImRtcCallEndReason.REJECT: {
      return isOperator ? '已拒绝' : '对方已拒绝'
    }
    default: {
      return hasDuration ? `通话时长 ${formatCallDuration(duration)}` : '通话已结束'
    }
  }
}

/** 会话内通话事件文案 */
export function resolveRtcCallTipText(message: {
  content?: string
  selfSend?: boolean
  type?: number
}): string {
  return segmentsToText(resolveRtcCallTipSegments(message))
}

/**
 * RTC_CALL_END 结束原因兜底文案；前端 toast / console 兜底用
 * <p>
 * 缺 operator 信息（同步响应 + WS push 兜底场景）时的通用文案；细分文案（按发送方视角）走 {@link resolveRtcCallPrivateBubbleText}
 */
export function resolveCallEndReasonText(reason: number | undefined): string {
  switch (reason) {
    case ImRtcCallEndReason.BUSY: {
      return '对方忙线中'
    }
    case ImRtcCallEndReason.CANCEL: {
      return '对方已取消'
    }
    case ImRtcCallEndReason.ERROR: {
      return '通话异常'
    }
    case ImRtcCallEndReason.HANGUP: {
      return '通话已结束'
    }
    case ImRtcCallEndReason.REJECT: {
      return '对方已拒绝'
    }
    default: {
      return '通话已断开'
    }
  }
}
