/**
 * 简化消息解析器
 */

import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
import { RawMessage, MessageElement, NTMsgType } from 'NapCatQQ/src/core/types.js';
import { parseMultiForwardXml, looksLikeMultiForwardXml } from './multiForwardXmlParser.js';

/* ------------------------------ 内部高性能工具 ------------------------------ */

/** 并发限流 map（保持顺序） */
async function mapLimit<T, R>(
  arr: readonly T[],
  limit: number,
  mapper: (item: T, index: number) => Promise<R>
): Promise<R[]> {
  const len = arr.length;
  const out = new Array<R>(len);
  if (len === 0) return out;

  const workers = Math.min((limit >>> 0) || 1, len);
  let next = 0;

  async function worker() {
    while (true) {
      const i = next++;
      if (i >= len) break;
      out[i] = await mapper(arr[i]!, i);
    }
  }
  const tasks = new Array(workers);
  for (let i = 0; i < workers; i++) tasks[i] = worker();
  await Promise.all(tasks);
  return out;
}

function resolveConcurrency(): number {
  try {
    // eslint-disable-next-line @typescript-eslint/no-var-requires
    const os = require('os');
    const cores = (os?.cpus?.() || []).length || 4;
    return Math.max(4, Math.min(32, cores * 2));
  } catch {
    return 8;
  }
}

/** 让出事件循环 */
function yieldToEventLoop(): Promise<void> {
  return new Promise((resolve) => {
    if (typeof setImmediate === 'function') setImmediate(resolve);
    else setTimeout(resolve, 0);
  });
}

/** Chunked 字符串构建器 */
class ChunkedBuilder {
  private chunks: string[] = [];
  push(s: string | undefined | null) {
    if (s) this.chunks.push(s);
  }
  toString() {
    return this.chunks.join('');
  }
  clear() {
    this.chunks.length = 0;
  }
}

const NEED_ESCAPE_RE = /[&<>"']/;
function escapeHtmlFast(text: string): string {
  if (!text) return '';
  if (!NEED_ESCAPE_RE.test(text)) return text;
  const len = text.length;
  let out = '';
  let last = 0;
  for (let i = 0; i < len; i++) {
    const c = text.charCodeAt(i);
    let rep: string | null = null;
    if (c === 38) rep = '&amp;';
    else if (c === 60) rep = '&lt;';
    else if (c === 62) rep = '&gt;';
    else if (c === 34) rep = '&quot;';
    else if (c === 39) rep = '&#39;';
    if (rep) {
      if (i > last) out += text.slice(last, i);
      out += rep;
      last = i + 1;
    }
  }
  if (last < len) out += text.slice(last);
  return out;
}

/** RFC3339（UTC）格式化工具 */
function pad2(n: number) {
  return n < 10 ? '0' + n : '' + n;
}
function pad3(n: number) {
  if (n >= 100) return '' + n;
  if (n >= 10) return '0' + n;
  return '00' + n;
}
function pad4(n: number) {
  if (n >= 1000) return '' + n;
  if (n >= 100) return '0' + n;
  if (n >= 10) return '00' + n;
  return '000' + n;
}
function rfc3339FromMillis(ms: number): string {
  const d = new Date(ms);
  return `${pad4(d.getUTCFullYear())}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}T${pad2(
    d.getUTCHours()
  )}:${pad2(d.getUTCMinutes())}:${pad2(d.getUTCSeconds())}.${pad3(d.getUTCMilliseconds())}Z`;
}
function rfc3339FromUnixSeconds(sec: number | string | bigint): string {
  try {
    if (typeof sec === 'bigint') {
      const n = Number(sec * 1000n);
      return Number.isFinite(n) ? rfc3339FromMillis(n) : '1970-01-01T00:00:00.000Z';
    }
    const n = typeof sec === 'string' ? parseInt(sec, 10) : sec;
    if (!Number.isFinite(n)) return '1970-01-01T00:00:00.000Z';
    return rfc3339FromMillis(Math.trunc(n * 1000));
  } catch {
    return '1970-01-01T00:00:00.000Z';
  }
}
function millisFromUnixSeconds(sec: number | string | bigint): number {
  try {
    if (typeof sec === 'bigint') {
      const n = Number(sec * 1000n);
      return Number.isFinite(n) ? n : 0;
    }
    const n = typeof sec === 'string' ? parseInt(sec, 10) : sec;
    return Number.isFinite(n) ? Math.trunc(n * 1000) : 0;
  } catch {
    return 0;
  }
}

/** 高性能 JSON 解析（SIMD 优先） */
type FastJsonParser = (s: string) => any;
let fastJsonParse: FastJsonParser = (s) => JSON.parse(s);
(function tryLoadSimdJson() {
  try {
    // eslint-disable-next-line @typescript-eslint/no-var-requires
    const mod = typeof require !== 'undefined' ? require('simdjson') : null;
    if (mod && typeof mod.parse === 'function') {
      fastJsonParse = (s) => mod.parse(s);
    }
  } catch {
    // 静默降级
  }
})();

/* ------------------------------ 导出类型（保持不变） ------------------------------ */

export interface CleanMessage {
  id: string;
  seq: string;
  timestamp: number;
  time: string;
  sender: {
    uid: string;
    uin?: string;
    name: string;          // 优先显示的名称（群昵称 > 备注 > QQ昵称）
    nickname?: string;     // QQ昵称（原始昵称）
    groupCard?: string;    // 群名片/群昵称
    remark?: string;       // 好友备注
    title?: string;        // 群头衔（仅群聊）
  };
  type: string;
  content: MessageContent;
  recalled: boolean;
  system: boolean;
}

export interface MessageContent {
  text: string;
  html: string;
  elements: MessageElementData[];
  resources: ResourceData[];
  mentions: MentionData[];
}

export interface MentionData {
  uid: string;
  uin?: string;
  name: string;
  type: 'user' | 'all';
}

export interface MessageElementData {
  type: string;
  data: any;
}

/**
 * issue #128 子项：回复框（reply 元素）里被引用消息的结构化预览片段。
 * 文本字段 `content` 仍保持「[图片]」「[表情341]」等占位串以兼容
 * JSON / TXT / Excel 等纯文本导出；HTML 导出额外消费 previewElements
 * 来渲染图片缩略图 / 表情图等。
 *
 * `localPath` / `url` 字段在解析阶段写不进来——picElement.sourcePath 是 NT
 * 自身的缓存路径，而不是导出的 resources/ 路径。这两个字段会在
 * `updateSingleMessageResourcePaths` 的二次回填里被写上。
 */
export interface ReplyPreviewElement {
  type: 'text' | 'image' | 'video' | 'audio' | 'file' | 'face' | 'marketFace';
  text: string;
  /** image: picElement.md5HexStr，用于跨消息匹配下载后的资源条目 */
  md5?: string;
  /** image: picElement.originImageUrl，回退兜底（QQ 临时签名 URL，会过期） */
  originUrl?: string;
  /** image / video / file: 原始文件名 */
  fileName?: string;
  /** marketFace: 表情包名 */
  faceName?: string;
  /** marketFace: 表情包静态 URL */
  url?: string;
  /** face: 系统小表情 id */
  faceIndex?: number;
  /** updateResourcePaths 二次回填出来的导出包内相对路径，例：images/abc.jpg */
  localPath?: string;
}

/**
 * 合并转发卡片里嵌套的子消息。
 * 字段刻意保持扁平，方便直接序列化到 JSON / JSONL，也方便 TXT 导出按行渲染。
 */
export interface ForwardInnerMessage {
  /** 子消息的 msgId，缺失时为空字符串 */
  id: string;
  /** 子消息时间戳（ms），无法解析时为 0 */
  timestamp: number;
  /** ISO 时间，缺失时退到 UNIX 0 */
  time: string;
  sender: {
    uid?: string;
    uin?: string;
    name: string;
  };
  content: {
    text: string;
    elements: MessageElementData[];
  };
}

export interface ResourceData {
  type: string;
  filename: string;
  size: number;
  url?: string;
  localPath?: string;
  width?: number;
  height?: number;
  duration?: number;
}

export interface MessageStatistics {
  total: number;
  byType: Record<string, number>;
  bySender: Record<string, { uid: string; uin?: string; count: number }>;
  resources: {
    total: number;
    byType: Record<string, number>;
    totalSize: number;
  };
  timeRange: {
    start: string;
    end: string;
    durationDays: number;
  };
}

/**
 * 发件人群头衔解析器：根据 senderUid 或 senderUin 返回该发件人在当前群里的头衔。
 * 调用侧（例如 ApiServer）负责在导出前预拉取群成员信息。
 */
export type SenderTitleResolver = (uid: string | undefined, uin: string | undefined) => string | undefined;

/** 轻量解析器配置 */
export interface SimpleParserOptions {
  concurrency?: number;
  progressEvery?: number;
  yieldEvery?: number;
  html?: 'full' | 'none';
  preferGroupMemberName?: boolean;
  /** 可选的发件人群头衔解析器，错误会被吞掉。返回空字符串或 undefined 代表“无头衔”。 */
  senderTitleResolver?: SenderTitleResolver;
  onProgress?: (processed: number, total: number) => void;
}

const DEFAULT_SIMPLE_OPTIONS: Required<Omit<SimpleParserOptions, 'onProgress' | 'senderTitleResolver'>> = {
  concurrency: resolveConcurrency(),
  progressEvery: 100,
  yieldEvery: 1000,
  html: 'full',
  preferGroupMemberName: true
};

/* ---------------------------------- 主类 ---------------------------------- */

export class SimpleMessageParser {
  /** 嵌套合并转发递归深度上限。三层基本足够，再深就不展开避免栈/性能爆炸。 */
  private static readonly MAX_FORWARD_DEPTH = 3;

  private readonly options: Required<Omit<SimpleParserOptions, 'onProgress' | 'senderTitleResolver'>> & {
    senderTitleResolver?: SenderTitleResolver;
  };
  private readonly onProgress?: (processed: number, total: number) => void;

  private readonly concurrency: number;

  // 全局消息映射，用于查找被引用的消息
  private messageMap: Map<string, RawMessage> = new Map();

  // 发件人显示信息缓存：senderUid / senderUin → { groupCard, remark, nickname }
  // 用于在某条消息缺失全部昵称字段时，回退到同一发件人在其他消息上出现过的可读名字。
  private senderInfoCache: Map<string, {
    groupCard?: string;
    remark?: string;
    nickname?: string;
  }> = new Map();

  // QQ表情映射表
  private faceMap: Map<string, string> = new Map();

  constructor(opts: SimpleParserOptions = {}) {
    this.options = { ...DEFAULT_SIMPLE_OPTIONS, ...opts };
    this.onProgress = opts.onProgress;
    this.concurrency = this.options.concurrency ?? resolveConcurrency();
    this.initializeFaceMap();
  }

  /**
   * 解析消息列表（高并发 + 有序输出）
   */
  async parseMessages(messages: RawMessage[]): Promise<CleanMessage[]> {
    const total = messages.length;
    let processed = 0;

    // 先建立全局消息映射
    this.messageMap.clear();
    this.senderInfoCache.clear();
    for (const msg of messages) {
      if (msg && msg.msgId) {
        this.messageMap.set(msg.msgId, msg);
        this.cacheSenderInfo(msg);
        // 同时将 records 数组中的消息也添加到映射中
        if (msg.records && msg.records.length > 0) {
          for (const record of msg.records) {
            if (record && record.msgId) {
              this.messageMap.set(record.msgId, record);
              this.cacheSenderInfo(record);
            }
          }
        }
      }
    }

    const results = await mapLimit(messages, this.concurrency, async (message, idx) => {
      try {
        const cm = await this.parseMessage(message);

        processed++;
        if (this.onProgress) {
          this.onProgress(processed, total);
        } else if (processed % this.options.progressEvery === 0) {
          console.log(`[SimpleMessageParser] 已解析 ${processed}/${total}`);
        }

        if (this.options.yieldEvery > 0 && (idx + 1) % this.options.yieldEvery === 0) {
          await yieldToEventLoop();
        }

        return cm;
      } catch (error) {
        console.error('解析消息失败:', error, message?.msgId);
        return this.createErrorMessage(message, error);
      }
    });
    
    // 清理映射
    this.messageMap.clear();
    this.senderInfoCache.clear();

    return results;
  }

  /**
   * 把消息上的群名片 / 备注 / 昵称写入按 senderUid 与 senderUin 索引的缓存。
   *
   * 同一群成员的不同消息中，往往只有一部分会携带 sendMemberName / sendNickName，
   * 单条消息都可能因为底层数据漏字段而显示为 QQ 号。这里在解析前先把所有出现过的
   * 名字按发件人聚合，后续 getSenderDisplayInfo 在本地字段全空时会回退到这里查表。
   */
  private cacheSenderInfo(message: RawMessage): void {
    const groupCard = this.getTrimmedText(message.sendMemberName);
    const remark = this.getTrimmedText(message.sendRemarkName);
    const nickname = this.getTrimmedText(message.sendNickName);
    if (!groupCard && !remark && !nickname) return;

    const keys: string[] = [];
    const uid = this.getTrimmedText(message.senderUid);
    if (uid) keys.push(uid);
    const uin = this.getTrimmedText(message.senderUin);
    if (uin) keys.push(uin);

    for (const key of keys) {
      const existing = this.senderInfoCache.get(key) ?? {};
      this.senderInfoCache.set(key, {
        groupCard: existing.groupCard ?? groupCard,
        remark: existing.remark ?? remark,
        nickname: existing.nickname ?? nickname
      });
    }
  }

  /**
   * 在按 senderUid → senderUin 顺序查表，命中即返回。供 getSenderDisplayInfo 在
   * 本条消息字段全空时使用，避免出现把 QQ 号当昵称的情况（参见 #274）。
   */
  private lookupCachedSenderInfo(message: RawMessage): {
    groupCard?: string;
    remark?: string;
    nickname?: string;
  } | undefined {
    const uid = this.getTrimmedText(message.senderUid);
    if (uid) {
      const hit = this.senderInfoCache.get(uid);
      if (hit) return hit;
    }
    const uin = this.getTrimmedText(message.senderUin);
    if (uin) {
      const hit = this.senderInfoCache.get(uin);
      if (hit) return hit;
    }
    return undefined;
  }

  /**
   * 【流式版本】解析消息生成器 - 逐条解析并yield，实现低内存占用
   * 适用于大量消息的场景，配合流式导出可实现全程低内存
   */
  async *parseMessagesStream(
    messages: RawMessage[],
    resourceMap?: Map<string, any>
  ): AsyncGenerator<CleanMessage, void, undefined> {
    const total = messages.length;
    let processed = 0;

    for (let i = 0; i < messages.length; i++) {
      const message = messages[i];
      if (!message) continue; // 跳过undefined元素
      
      try {
        const cleanMessage = await this.parseMessage(message);
        
        // 如果提供了resourceMap，立即更新这条消息的资源路径
        if (resourceMap && resourceMap.has(message.msgId)) {
          const resources = resourceMap.get(message.msgId);
          if (resources && cleanMessage.content.elements) {
            this.updateSingleMessageResourcePaths(cleanMessage, resources);
          }
        }

        processed++;
        if (this.onProgress) {
          this.onProgress(processed, total);
        } else if (processed % this.options.progressEvery === 0) {
          console.log(`[SimpleMessageParser] 已解析 ${processed}/${total}`);
        }

        if (this.options.yieldEvery > 0 && (i + 1) % this.options.yieldEvery === 0) {
          await yieldToEventLoop();
        }

        yield cleanMessage;
      } catch (error) {
        console.error('解析消息失败:', error, message.msgId);
        yield this.createErrorMessage(message, error);
      }
    }
  }

  /**
   * 解析单条消息（公开）
   */
  async parseSingleMessage(message: RawMessage): Promise<CleanMessage> {
    return this.parseMessage(message);
  }

  /**
   * 解析单条消息（内部）
   */
  private async parseMessage(message: RawMessage): Promise<CleanMessage> {
    const tsMs = millisFromUnixSeconds(message.msgTime as any);
    const timestamp = tsMs > 0 ? tsMs : Date.now();
    const senderInfo = this.getSenderDisplayInfo(message);

    const content = await this.parseMessageContent(message);

    const cleanMessage: CleanMessage = {
      id: message.msgId,
      seq: message.msgSeq,
      timestamp,
      // RFC3339（UTC）
      time: rfc3339FromMillis(timestamp),
      sender: {
        uid: message.senderUid || '未知',
        uin: message.senderUin,
        name: senderInfo.name,
        nickname: senderInfo.nickname,
        groupCard: senderInfo.groupCard,
        remark: senderInfo.remark,
        title: this.resolveSenderTitle(message)
      },
      type: this.getMessageTypeString(message.msgType),
      content,
      recalled: message.recallTime !== '0',
      system: this.isSystemMessage(message)
    };

    return cleanMessage;
  }

  /**
   * 调用 senderTitleResolver 获取发件人头衔。仅群聊（chatType=2）生效。
   * 解析器抛错不应该令整个解析流程走不下去，这里丝果干净地吞掉。
   */
  private resolveSenderTitle(message: RawMessage): string | undefined {
    if (!this.options.senderTitleResolver) return undefined;
    if (message.chatType !== 2) return undefined;
    try {
      const title = this.options.senderTitleResolver(message.senderUid, message.senderUin);
      const trimmed = this.getTrimmedText(title);
      return trimmed;
    } catch (error) {
      console.warn('[SimpleMessageParser] senderTitleResolver 抛出异常，已忽略:', error);
      return undefined;
    }
  }

  private getMessageTypeString(msgType: NTMsgType | number): string {
    // NTMsgType 枚举值（兼容两种定义方式）
    // 原始枚举: KMSGTYPENULL=1, KMSGTYPEMIX=2, KMSGTYPEFILE=3, KMSGTYPESTRUCT=4, KMSGTYPEGRAYTIPS=5, KMSGTYPEPTT=6, KMSGTYPEVIDEO=7, KMSGTYPEMULTIMSGFORWARD=8, KMSGTYPEREPLY=9, KMSGTYPEARKSTRUCT=11
    // 简化版: Text=1, Picture=2, File=3, Video=4, Voice=5, Reply=7
    const typeNum = typeof msgType === 'number' ? msgType : Number(msgType);
    switch (typeNum) {
      case 1: // KMSGTYPENULL / Text
      case 2: // KMSGTYPEMIX / Picture (混合消息，通常包含文本)
        return 'text';
      case 3: // KMSGTYPEFILE / File
        return 'file';
      case 4: // KMSGTYPESTRUCT / Video (简化版)
      case 7: // KMSGTYPEVIDEO (原始枚举)
        return 'video';
      case 5: // KMSGTYPEGRAYTIPS / Voice (简化版)
        return 'system';
      case 6: // KMSGTYPEPTT
        return 'audio';
      case 8: // KMSGTYPEMULTIMSGFORWARD
        return 'forward';
      case 9: // KMSGTYPEREPLY
        return 'reply';
      case 11: // KMSGTYPEARKSTRUCT
        return 'json';
      default:
        return `type_${typeNum}`;
    }
  }

  /**
   * 单趟解析消息内容
   */
  private async parseMessageContent(message: RawMessage, forwardDepth: number = 0): Promise<MessageContent> {
    const elements = message.elements || [];
    const parsedElements: MessageElementData[] = new Array(elements.length);
    const resources: ResourceData[] = [];
    const mentions: MentionData[] = [];

    const textB = new ChunkedBuilder();
    const htmlB = new ChunkedBuilder();
    const htmlEnabled = this.options.html !== 'none';

    let count = 0;
    for (let i = 0; i < elements.length; i++) {
      const element = elements[i]!;
      const parsed = await this.parseElement(element, message, forwardDepth);
      if (!parsed) continue;
      parsedElements[count++] = parsed;

      // 资源抽取
      const resource = this.extractResource(parsed);
      if (resource) resources.push(resource);

      // 提取 @ 提及信息
      if (parsed.type === 'at') {
        mentions.push({
          uid: parsed.data.uid || 'unknown',
          uin: parsed.data.uin,
          name: parsed.data.name || '某人',
          type: parsed.data.uid === 'all' ? 'all' : 'user'
        });
      }

      // 文本/HTML
      const { text, html } = this.elementToText(parsed, htmlEnabled);
      textB.push(text);
      if (htmlEnabled) htmlB.push(html);
    }
    // 压缩 parsedElements 实际长度
    parsedElements.length = count;

    return {
      text: textB.toString().trim(),
      html: htmlEnabled ? htmlB.toString().trim() : '',
      elements: parsedElements,
      resources,
      mentions
    };
  }

  /**
   * 元素解析（尽量同步，无额外中间对象）
   *
   * forwardDepth 用于跟踪当前消息已经嵌套在多少层合并转发里，超过 MAX_FORWARD_DEPTH
   * 后再遇到 multiForwardMsgElement 时只保留外壳信息，不再递归拉取内层消息。
   */
  private async parseElement(
    element: MessageElement,
    message: RawMessage,
    forwardDepth: number = 0
  ): Promise<MessageElementData | null> {
    // 文本 / @ 提及
    if (element.textElement) {
      const te = element.textElement;
      // atType: 0=普通文本, 1=@全体成员, 2=@某人
      if (te.atType === 1) {
        return {
          type: 'at',
          data: {
            uid: 'all',
            uin: '0',
            name: '全体成员',
            atType: 1
          }
        };
      } else if (te.atType === 2) {
        return {
          type: 'at',
          data: {
            uid: te.atNtUid || te.atUid || 'unknown',
            uin: te.atUid || '0',
            name: (te.content || '').replace(/^@/, ''),
            atType: 2
          }
        };
      }
      // 普通文本
      return {
        type: 'text',
        data: { text: te.content || '' }
      };
    }

    // 表情
    if (element.faceElement) {
      const faceId = element.faceElement.faceIndex?.toString() || '';
      const faceName = element.faceElement.faceText || this.faceMap.get(faceId) || `表情${faceId}`;
      return {
        type: 'face',
        data: {
          id: faceId,
          name: faceName
        }
      };
    }

    // 商城表情
    if (element.marketFaceElement) {
      const emojiId = element.marketFaceElement.emojiId || '';
      const key = element.marketFaceElement.key || '';
      const url = emojiId ? this.generateMarketFaceUrl(emojiId) : '';

      return {
        type: 'market_face',
        data: {
          name: element.marketFaceElement.faceName || '商城表情',
          tabName: (element.marketFaceElement as any).tabName || '',
          key,
          emojiId,
          emojiPackageId: element.marketFaceElement.emojiPackageId,
          url
        }
      };
    }

    // 图片
    if (element.picElement) {
      return {
        type: 'image',
        data: {
          filename: element.picElement.fileName || '图片',
          size: this.parseSizeString(element.picElement.fileSize),
          width: element.picElement.picWidth,
          height: element.picElement.picHeight,
          md5: element.picElement.md5HexStr,
          url: element.picElement.originImageUrl || ''
        }
      };
    }

    // 文件
    if (element.fileElement) {
      return {
        type: 'file',
        data: {
          filename: element.fileElement.fileName || '文件',
          size: this.parseSizeString(element.fileElement.fileSize),
          md5: element.fileElement.fileMd5
        }
      };
    }

    // 视频
    if (element.videoElement) {
      return {
        type: 'video',
        data: {
          filename: element.videoElement.fileName || '视频',
          size: this.parseSizeString(element.videoElement.fileSize),
          duration: (element.videoElement as any).duration || 0,
          thumbSize: this.parseSizeString(element.videoElement.thumbSize)
        }
      };
    }

    // 语音
    if (element.pttElement) {
      return {
        type: 'audio',
        data: {
          filename: element.pttElement.fileName || '语音',
          size: this.parseSizeString(element.pttElement.fileSize),
          duration: element.pttElement.duration || 0
        }
      };
    }

    // 回复
    if (element.replyElement) {
      const replyData = this.extractReplyContent(element.replyElement, message);
      return {
        type: 'reply',
        data: {
          messageId: replyData.messageId,
          referencedMessageId: replyData.referencedMessageId,  // 被引用消息的实际messageId
          senderUin: replyData.senderUin,
          senderName: replyData.senderName,
          content: replyData.content,
          timestamp: replyData.timestamp,
          // issue #128 子项：回复框里的图片 / 表情等元素，HTML 导出时拿来
          // 渲染缩略图。文本字段 `content` 保持「[图片]」「[表情341]」不变，
          // JSON / TXT / Excel 等纯文本导出零影响。
          previewElements: replyData.previewElements
        }
      };
    }

    // 转发
    if (element.multiForwardMsgElement) {
      const resId = element.multiForwardMsgElement.resId || '';
      const xmlContent = element.multiForwardMsgElement.xmlContent || '';

      // 拉合并转发卡片里的真实消息列表，导出 JSON / TXT 时把内容也带上（issue #161）。
      // 失败时降级为只保留 XML summary，绝不阻断主导出；嵌套过深也直接停在外壳。
      const innerMessages =
        forwardDepth >= SimpleMessageParser.MAX_FORWARD_DEPTH
          ? []
          : await this.fetchForwardInnerMessages(message, resId, forwardDepth + 1);

      // issue #128 子项 3：拉子消息失败时不再把 XML 原文塞进 summary，
      // 改解析 QQ 客户端的卡片 XML（带 <title>/<summary> 标记）抠出可读预览。
      // 拉成功的情况下也留一份 xmlPreview，方便 fallback 到 fallback 时还能用。
      const xmlInfo = parseMultiForwardXml(xmlContent);
      const cardTitle = xmlInfo.header || '聊天记录';
      const cardSummary = xmlInfo.summary
        || (innerMessages.length > 0 ? `查看${innerMessages.length}条转发消息` : '查看转发消息');
      const messageCount = innerMessages.length > 0
        ? innerMessages.length
        : xmlInfo.messageCount;

      return {
        type: 'forward',
        data: {
          title: cardTitle,
          resId,
          summary: cardSummary,
          preview: xmlInfo.previewLines,
          messageCount,
          messages: innerMessages
        }
      };
    }

    // JSON 卡片
    if (element.arkElement) {
      const jsonContent = element.arkElement.bytesData || '{}';
      const parsedJson = this.parseJsonContent(jsonContent);
      return {
        type: 'json',
        data: {
          content: jsonContent,
          title: parsedJson.title || 'JSON消息',
          description: parsedJson.description,
          url: parsedJson.url,
          preview: parsedJson.preview,
          appName: parsedJson.appName,
          summary: parsedJson.title || parsedJson.description || 'JSON消息'
        }
      };
    }

    // 位置
    if (element.shareLocationElement) {
      return {
        type: 'location',
        data: {
          title: '位置消息',
          summary: '分享了位置'
        }
      };
    }

    // 小灰条（系统提示）
    if (element.grayTipElement) {
      return this.parseGrayTipElement(element.grayTipElement);
    }

    // 长消息 (ElementType 13 - STRUCTLONGMSG)
    if (element.structLongMsgElement) {
      return {
        type: 'long_message',
        data: {
          summary: '长消息',
          resId: element.structLongMsgElement.resId || '',
          xmlContent: element.structLongMsgElement.xmlContent || ''
        }
      };
    }

    // 音视频通话记录 (ElementType 21 - AVRECORD)
    if (element.avRecordElement) {
      const avRecord = element.avRecordElement;
      const typeText = avRecord.type === 1 ? '语音通话' : avRecord.type === 2 ? '视频通话' : '通话';
      const statusText = avRecord.text || '已结束';
      return {
        type: 'av_record',
        data: {
          summary: `${typeText} - ${statusText}`,
          type: avRecord.type,
          time: avRecord.time || '0',
          text: statusText,
          mainType: avRecord.mainType,
          extraType: avRecord.extraType
        }
      };
    }

    // Markdown (ElementType 14 - MARKDOWN)
    if (element.markdownElement) {
      return {
        type: 'markdown',
        data: {
          content: element.markdownElement.content || '',
          summary: 'Markdown消息'
        }
      };
    }

    // Giphy动图 (ElementType 15 - GIPHY)
    if (element.giphyElement) {
      return {
        type: 'giphy',
        data: {
          id: element.giphyElement.id || '',
          width: element.giphyElement.width || 0,
          height: element.giphyElement.height || 0,
          isClip: element.giphyElement.isClip || false,
          summary: 'Giphy动图'
        }
      };
    }

    // 内联键盘 (ElementType 17 - INLINEKEYBOARD)
    if (element.inlineKeyboardElement) {
      return {
        type: 'inline_keyboard',
        data: {
          botAppid: element.inlineKeyboardElement.botAppid || '',
          rows: element.inlineKeyboardElement.rows || [],
          summary: '内联键盘'
        }
      };
    }

    // 日历 (ElementType 19 - CALENDAR)
    if (element.calendarElement) {
      return {
        type: 'calendar',
        data: {
          summary: element.calendarElement.summary || '日历',
          msg: element.calendarElement.msg || '',
          expireTimeMs: element.calendarElement.expireTimeMs || '0',
          schemaType: element.calendarElement.schemaType || 0
        }
      };
    }

    // YOLO游戏结果 (ElementType 20 - YOLOGAMERESULT)
    if (element.yoloGameResultElement) {
      return {
        type: 'yolo_game_result',
        data: {
          userInfo: element.yoloGameResultElement.UserInfo || [],
          summary: 'YOLO游戏结果'
        }
      };
    }

    // 表情气泡 (ElementType 27 - FACEBUBBLE)
    if (element.faceBubbleElement) {
      return {
        type: 'face_bubble',
        data: {
          faceCount: element.faceBubbleElement.faceCount || 0,
          faceSummary: element.faceBubbleElement.faceSummary || '',
          summary: element.faceBubbleElement.faceSummary || '表情气泡'
        }
      };
    }

    // 豆腐记录 (ElementType 23 - TOFURECORD)
    if (element.tofuRecordElement) {
      return {
        type: 'tofu_record',
        data: {
          type: element.tofuRecordElement.type || 0,
          descriptionContent: element.tofuRecordElement.descriptionContent || '',
          summary: element.tofuRecordElement.descriptionContent || '豆腐记录'
        }
      };
    }

    // 置顶任务消息 (ElementType 29 - TASKTOPMSG)
    if (element.taskTopMsgElement) {
      return {
        type: 'task_top_msg',
        data: {
          msgTitle: element.taskTopMsgElement.msgTitle || '',
          msgSummary: element.taskTopMsgElement.msgSummary || '',
          iconUrl: element.taskTopMsgElement.iconUrl || '',
          summary: element.taskTopMsgElement.msgTitle || '置顶消息'
        }
      };
    }

    // 推荐消息 (ElementType 43 - RECOMMENDEDMSG)
    if (element.recommendedMsgElement) {
      return {
        type: 'recommended_msg',
        data: {
          botAppid: (element.recommendedMsgElement as any).botAppid || '',
          summary: '推荐消息'
        }
      };
    }

    // 操作栏 (ElementType 44 - ACTIONBAR)
    if (element.actionBarElement) {
      return {
        type: 'action_bar',
        data: {
          botAppid: element.actionBarElement.botAppid || '',
          rows: element.actionBarElement.rows || [],
          summary: '操作栏'
        }
      };
    }

    // 未知类型
    console.warn(`[SimpleMessageParser] 未知消息元素类型: ${element.elementType}`, element);
    return {
      type: 'system',
      data: {
        elementType: element.elementType,
        summary: this.getSystemMessageSummary(element),
        text: this.getSystemMessageSummary(element)
      }
    };
  }

  private extractResource(element: MessageElementData): ResourceData | null {
    if (!['image', 'file', 'video', 'audio'].includes(element.type)) return null;
    const d = element.data || {};
    return {
      type: element.type,
      filename: d.filename || '未知',
      size: d.size || 0,
      url: d.url,
      localPath: d.localPath, // 包含本地路径信息
      width: d.width,
      height: d.height,
      duration: d.duration
    };
  }

  private elementToText(element: MessageElementData, htmlEnabled: boolean): { text: string; html: string } {
    switch (element.type) {
      case 'text': {
        const t = element.data.text || '';
        return { text: t, html: htmlEnabled ? escapeHtmlFast(t) : '' };
      }
      case 'face': {
        const t = `[表情${element.data.id}]`;
        return { text: t, html: htmlEnabled ? t : '' };
      }
      case 'market_face': {
        const t = `[${element.data.name || '表情'}]`;
        return { text: t, html: htmlEnabled ? t : '' };
      }
      case 'image': {
        const t = `[图片:${element.data.filename}]`;
        return { text: t, html: htmlEnabled ? `<img alt="${escapeHtmlFast(element.data.filename)}" class="image">` : '' };
      }
      case 'file': {
        const t = `[文件:${element.data.filename}]`;
        return { text: t, html: htmlEnabled ? `<span class="file">${escapeHtmlFast(t)}</span>` : '' };
      }
      case 'video': {
        const t = `[视频:${element.data.filename}]`;
        return { text: t, html: htmlEnabled ? `<span class="video">${escapeHtmlFast(t)}</span>` : '' };
      }
      case 'audio': {
        const t = `[语音:${element.data.duration}秒]`;
        return { text: t, html: htmlEnabled ? `<span class="audio">${escapeHtmlFast(t)}</span>` : '' };
      }
      case 'at': {
        const name = element.data.name || '某人';
        const t = `@${name}`;
        const uid = element.data.uid || 'unknown';
        if (uid === 'all') {
          return { text: t, html: htmlEnabled ? `<span class="mention mention-all">${escapeHtmlFast(t)}</span>` : '' };
        }
        return { text: t, html: htmlEnabled ? `<span class="mention" data-uid="${uid}">${escapeHtmlFast(t)}</span>` : '' };
      }
      case 'reply': {
        const t = `[回复消息]`;
        return { text: t, html: htmlEnabled ? `<div class="reply">${t}</div>` : '' };
      }
      case 'forward': {
        const inner: ForwardInnerMessage[] = Array.isArray(element.data?.messages) ? element.data.messages : [];
        const count = element.data?.messageCount ?? inner.length;
        // issue #128：拉子消息失败时退回到 multiForwardMsg XML 抠出来的卡片预览行（已 unescape，无 XML）。
        const xmlPreview: string[] = Array.isArray(element.data?.preview)
          ? element.data.preview.filter((s: unknown): s is string => typeof s === 'string' && s.trim().length > 0)
          : [];

        // 预览前几条作者+文本，方便扫一眼能看到合并转发里到底是什么内容。
        const innerPreviewLines = inner.slice(0, 3).map((m) => {
          const name = m?.sender?.name || (m?.sender?.uin ? String(m.sender.uin) : '');
          const body = (m?.content?.text || '').replace(/\s+/g, ' ').trim();
          const trimmedBody = body.length > 40 ? body.slice(0, 40) + '…' : body;
          if (name && trimmedBody) return `${name}: ${trimmedBody}`;
          if (name) return name;
          return trimmedBody;
        }).filter(Boolean);
        const previewLines = innerPreviewLines.length > 0
          ? innerPreviewLines
          : xmlPreview.slice(0, 3).map((l) => l.length > 60 ? l.slice(0, 60) + '…' : l);

        const header = count > 0 ? `[转发消息: ${count}条]` : `[转发消息]`;
        const text = previewLines.length > 0 ? `${header}\n${previewLines.map((l) => `  ${l}`).join('\n')}` : header;

        if (!htmlEnabled) {
          return { text, html: '' };
        }

        let innerHtml = '';
        if (inner.length > 0) {
          innerHtml = `<ul class="forward-inner">${inner.map((m) => {
            const name = escapeHtmlFast(m?.sender?.name || (m?.sender?.uin ? String(m.sender.uin) : '未知'));
            const body = escapeHtmlFast((m?.content?.text || '').replace(/\s+/g, ' ').trim());
            return `<li><span class="forward-inner-sender">${name}</span><span class="forward-inner-text">${body}</span></li>`;
          }).join('')}</ul>`;
        } else if (xmlPreview.length > 0) {
          innerHtml = `<ul class="forward-inner">${xmlPreview.slice(0, 5).map((line) => {
            return `<li><span class="forward-inner-text">${escapeHtmlFast(line)}</span></li>`;
          }).join('')}</ul>`;
        }
        return {
          text,
          html: `<div class="forward">${escapeHtmlFast(header)}${innerHtml}</div>`
        };
      }
      case 'location': {
        const t = `[位置消息]`;
        return { text: t, html: htmlEnabled ? `<div class="location">${t}</div>` : '' };
      }
      case 'json': {
        const t = `[JSON消息]`;
        return { text: t, html: htmlEnabled ? `<div class="json">${t}</div>` : '' };
      }
      case 'long_message': {
        const t = `[长消息]`;
        return { text: t, html: htmlEnabled ? `<div class="long-message">${t}</div>` : '' };
      }
      case 'av_record': {
        const t = element.data.summary || '[通话记录]';
        return { text: t, html: htmlEnabled ? `<div class="av-record">${escapeHtmlFast(t)}</div>` : '' };
      }
      case 'markdown': {
        const t = `[Markdown消息]`;
        return { text: t, html: htmlEnabled ? `<div class="markdown">${t}</div>` : '' };
      }
      case 'giphy': {
        const t = `[Giphy动图]`;
        return { text: t, html: htmlEnabled ? `<div class="giphy">${t}</div>` : '' };
      }
      case 'inline_keyboard': {
        const t = `[内联键盘]`;
        return { text: t, html: htmlEnabled ? `<div class="inline-keyboard">${t}</div>` : '' };
      }
      case 'calendar': {
        const t = element.data.summary || '[日历]';
        return { text: t, html: htmlEnabled ? `<div class="calendar">${escapeHtmlFast(t)}</div>` : '' };
      }
      case 'yolo_game_result': {
        const t = `[YOLO游戏结果]`;
        return { text: t, html: htmlEnabled ? `<div class="yolo-game">${t}</div>` : '' };
      }
      case 'face_bubble': {
        const t = element.data.summary || '[表情气泡]';
        return { text: t, html: htmlEnabled ? `<div class="face-bubble">${escapeHtmlFast(t)}</div>` : '' };
      }
      case 'tofu_record': {
        const t = element.data.summary || '[豆腐记录]';
        return { text: t, html: htmlEnabled ? `<div class="tofu-record">${escapeHtmlFast(t)}</div>` : '' };
      }
      case 'task_top_msg': {
        const t = element.data.summary || '[置顶消息]';
        return { text: t, html: htmlEnabled ? `<div class="task-top">${escapeHtmlFast(t)}</div>` : '' };
      }
      case 'recommended_msg': {
        const t = `[推荐消息]`;
        return { text: t, html: htmlEnabled ? `<div class="recommended">${t}</div>` : '' };
      }
      case 'action_bar': {
        const t = `[操作栏]`;
        return { text: t, html: htmlEnabled ? `<div class="action-bar">${t}</div>` : '' };
      }
      case 'system': {
        const t = element.data.text || element.data.summary || '系统消息';
        return { text: t, html: htmlEnabled ? `<div class="system">${escapeHtmlFast(t)}</div>` : '' };
      }
      default: {
        const rawText = element.data.text || element.data.summary || element.data.content || '';
        return { text: rawText, html: htmlEnabled ? (rawText ? `<span>${escapeHtmlFast(rawText)}</span>` : '') : '' };
      }
    }
  }

  private parseSizeString(size: string | number | undefined): number {
    if (typeof size === 'number') return size;
    if (typeof size === 'string') {
      const n = parseInt(size, 10);
      return Number.isFinite(n) ? n : 0;
    }
    return 0;
  }

  /**
   * 拉合并转发卡片里的子消息列表，并把每条子消息扁平化成 ForwardInnerMessage（issue #161）。
   *
   * 数据来源优先级：
   *   1) message.records（NapCat 推消息时偶尔会顺手填上）
   *   2) bridge 的 NapCatCore.apis.MsgApi.getMultiMsg(...)
   *
   * 任何一步抛错都吞掉返回空数组，外层在 summary / xmlContent 上还是有兜底信息。
   */
  private async fetchForwardInnerMessages(
    message: RawMessage,
    resId: string,
    depth: number
  ): Promise<ForwardInnerMessage[]> {
    if (depth > SimpleMessageParser.MAX_FORWARD_DEPTH) return [];

    let raws: RawMessage[] = [];
    const inlineRecords = (message as any).records;
    if (Array.isArray(inlineRecords) && inlineRecords.length > 0) {
      raws = inlineRecords;
    }

    if (raws.length === 0) {
      try {
        const bridge = (globalThis as any).__NAPCAT_BRIDGE__;
        const core = bridge?.core;
        const msgApi = core?.apis?.MsgApi || core?.apis?.msg;
        if (msgApi && typeof msgApi.getMultiMsg === 'function') {
          const peer = {
            chatType: message.chatType,
            peerUid: message.peerUid,
            guildId: ''
          };
          const result = await msgApi.getMultiMsg({
            peer,
            rootMsgId: message.msgId,
            parentMsgId: message.msgId,
            forwardId: resId,
            resId
          });
          if (result && Array.isArray(result.msgList)) {
            raws = result.msgList;
          }
        }
      } catch {
        // 拉取失败时只能丢掉子消息，外层卡片仍然会给出 [转发消息: N条] 占位。
      }
    }

    if (raws.length === 0) return [];

    const out: ForwardInnerMessage[] = [];
    for (const raw of raws) {
      if (!raw) continue;
      try {
        const tsMs = millisFromUnixSeconds(raw.msgTime as any);
        this.cacheSenderInfo(raw);
        const senderInfo = this.getSenderDisplayInfo(raw);

        const elementsArr: MessageElementData[] = [];
        const textParts: string[] = [];

        const els = raw.elements || [];
        for (const el of els) {
          const parsed = await this.parseElement(el, raw, depth);
          if (!parsed) continue;
          elementsArr.push(parsed);
          const rendered = this.elementToText(parsed, false);
          if (rendered.text) textParts.push(rendered.text);
        }

        out.push({
          id: raw.msgId || '',
          timestamp: tsMs,
          time: rfc3339FromMillis(tsMs),
          sender: {
            uid: raw.senderUid || undefined,
            uin: raw.senderUin || undefined,
            name: senderInfo.name
          },
          content: {
            text: textParts.join(''),
            elements: elementsArr
          }
        });
      } catch {
        // 单条子消息解析失败时跳过，避免拖死整批。
      }
    }
    return out;
  }

  private isSystemMessage(message: RawMessage): boolean {
    return message.msgType === NTMsgType.KMSGTYPEGRAYTIPS;
  }

  private createErrorMessage(message: RawMessage, error: any): CleanMessage {
    const tsMs = millisFromUnixSeconds(message.msgTime as any);
    const timestamp = tsMs > 0 ? tsMs : Date.now();
    const senderInfo = this.getSenderDisplayInfo(message);

    const errMsg = (error && (error.message || error.toString?.())) || 'Unknown';
    return {
      id: message.msgId,
      seq: message.msgSeq,
      timestamp,
      time: rfc3339FromMillis(timestamp),
      sender: {
        uid: message.senderUid || '未知',
        uin: message.senderUin,
        name: senderInfo.name,
        nickname: senderInfo.nickname,
        groupCard: senderInfo.groupCard,
        remark: senderInfo.remark,
        title: this.resolveSenderTitle(message)
      },
      type: 'error',
      content: {
        text: `[解析失败: ${errMsg}]`,
        html: `<span class="error">[解析失败: ${escapeHtmlFast(errMsg)}]</span>`,
        elements: [],
        resources: [],
        mentions: []
      },
      recalled: false,
      system: false
    };
  }

  private getTrimmedText(value: unknown): string | undefined {
    if (value === null || value === undefined) return undefined;
    const text = String(value).trim();
    return text || undefined;
  }

  private getSenderDisplayInfo(message: RawMessage): {
    name: string;
    nickname: string | undefined;
    groupCard: string | undefined;
    remark: string | undefined;
  } {
    let groupCard = this.getTrimmedText(message.sendMemberName);
    let remark = this.getTrimmedText(message.sendRemarkName);
    let nickname = this.getTrimmedText(message.sendNickName);
    const isGroupChat = message.chatType === 2;
    const preferGroupMemberName = isGroupChat && this.options.preferGroupMemberName !== false;

    // #274：当本条消息没有任何可读名字时，回退到同发件人在其他消息上出现过的名字。
    if (!groupCard && !remark && !nickname) {
      const cached = this.lookupCachedSenderInfo(message);
      if (cached) {
        groupCard = cached.groupCard;
        remark = cached.remark;
        nickname = cached.nickname;
      }
    }

    const name = (
      isGroupChat
        ? (preferGroupMemberName ? groupCard || remark || nickname : nickname)
        : remark || nickname
    ) || this.getTrimmedText(message.senderUin) || this.getTrimmedText(message.senderUid) || '未知用户';

    return {
      name,
      nickname,
      groupCard,
      remark
    };
  }

  /** @deprecated 使用 isPureMediaMessage 代替 */
  isPureImageMessage(message: CleanMessage): boolean {
    return this.isPureMediaMessage(message);
  }

  isPureMediaMessage(message: CleanMessage): boolean {
    const els = message.content.elements || [];
    
    // 必须包含媒体元素
    const hasMedia = els.some((e) => ['image', 'video', 'audio', 'file', 'face'].includes(e.type));
    if (!hasMedia) return false;

    // 最可靠的判断：检查实际的文本内容
    // content.text已经是所有元素解析后的纯文本内容
    const actualText = (message.content.text || '').trim();
    
    // 如果有实际的文本内容，则不是纯媒体消息
    if (actualText.length > 0) {
      // 进一步检查是否只包含CQ码
      const withoutCQ = actualText.replace(/\[CQ:[^\]]+\]/g, '').trim();
      if (withoutCQ.length > 0) {
        return false; // 有实际文字内容，不过滤
      }
    }

    // 没有实际文字内容，判定为纯媒体消息
    return true;
  }

  private hasRealTextContent(message: CleanMessage): boolean {
    const textEls = message.content.elements.filter((e) => e.type === 'text');
    for (let i = 0; i < textEls.length; i++) {
      const t = textEls[i]!.data?.text || '';
      if (t.trim().length > 0 && !this.isOnlyCQCode(t)) return true;
    }
    return false;
  }

  private isOnlyCQCode(text: string): boolean {
    if (!text || text.trim().length === 0) return true;
    // 移除所有 CQ 码，检测是否还有实际文字
    const without = text.replace(/\[CQ:[^\]]+\]/g, '').trim();
    return without.length === 0;
  }

  filterMessages(messages: CleanMessage[], includePureImages: boolean = true): CleanMessage[] {
    if (includePureImages) return messages;
    return messages.filter((m) => !this.isPureMediaMessage(m));
  }

  calculateStatistics(messages: CleanMessage[]): MessageStatistics {
    const stats: MessageStatistics = {
      total: messages.length,
      byType: {},
      bySender: {},
      resources: {
        total: 0,
        byType: {},
        totalSize: 0
      },
      timeRange: {
        start: '',
        end: '',
        durationDays: 0
      }
    };

    if (messages.length === 0) return stats;

    // 时间范围
    const ts = messages.map((m) => m.timestamp).filter((t) => t > 0).sort((a, b) => a - b);
    if (ts.length > 0) {
      const start = new Date(ts[0]!);
      const end = new Date(ts[ts.length - 1]!);
      stats.timeRange = {
        start: start.toISOString(),
        end: end.toISOString(),
        durationDays: Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24))
      };
    }

    // 统计
    for (let i = 0; i < messages.length; i++) {
      const m = messages[i]!;
      if (!m || !m.content) continue;

      // 类型
      stats.byType[m.type] = (stats.byType[m.type] || 0) + 1;

      // 发送者
      const senderKey = m.sender?.name || m.sender?.uid || '未知用户';
      if (!stats.bySender[senderKey]) {
        stats.bySender[senderKey] = {
          uid: m.sender?.uid || 'unknown',
          uin: m.sender?.uin,
          count: 0
        };
      }
      if (!stats.bySender[senderKey]!.uin && m.sender?.uin) {
        stats.bySender[senderKey]!.uin = m.sender.uin;
      }
      stats.bySender[senderKey]!.count++;

      // 资源
      const res = m.content.resources || [];
      for (let j = 0; j < res.length; j++) {
        const r = res[j]!;
        stats.resources.total++;
        const t = r.type || 'unknown';
        stats.resources.byType[t] = (stats.resources.byType[t] || 0) + 1;
        stats.resources.totalSize += r.size || 0;
      }
    }

    return stats;
  }

  async updateResourcePaths(messages: CleanMessage[], resourceMap: Map<string, any[]>): Promise<void> {
    for (let mi = 0; mi < messages.length; mi++) {
      const message = messages[mi]!;
      const resources = resourceMap.get(message.id);
      if (resources && resources.length > 0) {
        this.updateSingleMessageResourcePaths(message, resources);
      }
    }
    // issue #128：所有消息的资源路径写完之后，再回头把 reply 元素里的
    // previewElements.localPath 拉齐，让 HTML 导出能直接渲染缩略图。
    this.backfillReplyPreviewLocalPaths(messages);
  }

  /**
   * 二次回填：扫一遍 messages，把每条 reply 元素的 previewElements 里的
   * 图片片段补上 `localPath`，方便 HTML 导出器直接渲染缩略图。
   *
   * 匹配规则：先按 md5（picElement.md5HexStr 一对一稳定映射），匹配不到时
   * 按顺序兜底（极端老快照里 md5 可能缺失）。被引用消息不在导出范围内时
   * 自然找不到，留空让 HTML 端走「[图片]」文字回退。
   */
  public backfillReplyPreviewLocalPaths(messages: CleanMessage[]): void {
    if (messages.length === 0) return;
    // 第一步：建 msgId → image[] 索引
    const imagesByMsgId = new Map<string, Array<{ md5: string; localPath: string }>>();
    for (const m of messages) {
      const imgs: Array<{ md5: string; localPath: string }> = [];
      for (const el of m.content.elements) {
        if (el.type !== 'image' || !el.data || typeof el.data !== 'object') continue;
        const data = el.data as { md5?: string; localPath?: string };
        if (!data.localPath) continue;
        imgs.push({ md5: String(data.md5 || ''), localPath: data.localPath });
      }
      if (imgs.length > 0) imagesByMsgId.set(m.id, imgs);
    }
    if (imagesByMsgId.size === 0) return;
    // 第二步：扫所有 reply 元素，按 referencedMessageId 找回原消息的图片
    for (const m of messages) {
      for (const el of m.content.elements) {
        if (el.type !== 'reply' || !el.data || typeof el.data !== 'object') continue;
        const data = el.data as { referencedMessageId?: string; previewElements?: ReplyPreviewElement[] };
        const refId = data.referencedMessageId ? String(data.referencedMessageId) : '';
        if (!refId || !Array.isArray(data.previewElements)) continue;
        const refImgs = imagesByMsgId.get(refId);
        if (!refImgs || refImgs.length === 0) continue;
        let fallbackIdx = 0;
        for (const pe of data.previewElements) {
          if (pe.type !== 'image') continue;
          const byMd5 = pe.md5 ? refImgs.find(r => r.md5 && r.md5 === pe.md5) : null;
          const candidate = byMd5 || refImgs[fallbackIdx];
          if (candidate?.localPath) pe.localPath = candidate.localPath;
          fallbackIdx++;
        }
      }
    }
  }

  /**
   * 更新单条消息的资源路径（供批量、流式以及 JSON / TXT / Excel 等
   * 非 HTML 导出器逐条调用）。issue #277。
   */
  public updateSingleMessageResourcePaths(message: CleanMessage, resources: any[]): void {
    console.log(`[SimpleMessageParser] 更新消息 ${message.id} 的资源路径，资源数量: ${resources.length}`);
    
    // 更新 message.content.resources
    const resArr = message.content.resources;
    const n = Math.min(resArr.length, resources.length);
    for (let i = 0; i < n; i++) {
      const info = resources[i];
      if (info && info.localPath) {
        const fileName = path.basename(info.localPath);
        const typeDir = info.type + 's';  // image -> images, video -> videos
        // 修复 Issue #30: 保留类型子目录，让导出器能正确找到文件
        resArr[i]!.localPath = `${typeDir}/${fileName}`;
        resArr[i]!.url = `resources/${typeDir}/${fileName}`;
        resArr[i]!.type = info.type;
        console.log(`[SimpleMessageParser] 资源 ${i}: type=${info.type}, path=${typeDir}/${fileName}`);
      } else {
        console.warn(`[SimpleMessageParser] 资源 ${i} 无localPath:`, info);
      }
    }

    // 更新 elements 中的 URL
    // 按类型和顺序匹配，而不是按文件名（因为下载后文件名可能改变）
    const els = message.content.elements;
    let resourceIndex = 0;
    
    console.log(`[SimpleMessageParser] 消息有 ${els.length} 个元素`);
    
    for (let i = 0; i < els.length; i++) {
      const el = els[i]!;
      if (!el.data || typeof el.data !== 'object') continue;
      
      // 只处理媒体类型元素
      if (el.type === 'image' || el.type === 'video' || el.type === 'audio' || el.type === 'file') {
        console.log(`[SimpleMessageParser] 元素 ${i}: type=${el.type}, filename=${(el.data as any).filename}`);
        
        // 按顺序匹配对应类型的资源
        const matchingResource = resources.find((r, idx) => 
          idx >= resourceIndex && r.type === el.type
        );
        
        if (matchingResource && matchingResource.localPath) {
          const fileName = path.basename(matchingResource.localPath);
          const typeDir = matchingResource.type + 's';
          // 修复 Issue #30: 保留类型子目录，让导出器能正确找到文件
          (el.data as any).localPath = `${typeDir}/${fileName}`;
          (el.data as any).url = `resources/${typeDir}/${fileName}`;
          
          console.log(`[SimpleMessageParser] ✓ 元素 ${i} 匹配到资源: ${typeDir}/${fileName}`);
          
          // 更新资源索引
          resourceIndex = resources.indexOf(matchingResource) + 1;
        } else {
          console.warn(`[SimpleMessageParser] ✗ 元素 ${i} (type=${el.type}) 未找到匹配资源`);
        }
      }
    }
  }

  private parseJsonContent(jsonString: string): any {
    try {
      const json = fastJsonParse(jsonString);
      const result: any = {};

      // 标题
      if (json.prompt) result.title = json.prompt;
      else if (json.meta?.detail_1?.title) result.title = json.meta.detail_1.title;
      else if (json.meta?.news?.title) result.title = json.meta.news.title;

      // 描述
      if (json.meta?.detail_1?.desc) result.description = json.meta.detail_1.desc;
      else if (json.meta?.news?.desc) result.description = json.meta.news.desc;

      // URL
      if (json.meta?.detail_1?.qqdocurl) result.url = json.meta.detail_1.qqdocurl;
      else if (json.meta?.detail_1?.url) result.url = json.meta.detail_1.url;
      else if (json.meta?.news?.jumpUrl) result.url = json.meta.news.jumpUrl;

      // 预览图
      if (json.meta?.detail_1?.preview) result.preview = json.meta.detail_1.preview;
      else if (json.meta?.news?.preview) result.preview = json.meta.news.preview;

      // 应用名称
      if (json.meta?.detail_1?.title && json.app) result.appName = json.meta.detail_1.title;
      else if (json.app === 'com.tencent.miniapp_01') result.appName = '小程序';

      return result;
    } catch (error) {
      console.warn('[SimpleMessageParser] JSON解析失败:', error);
      return {};
    }
  }

  private extractReplyContent(replyElement: any, message: RawMessage): any {
    // 使用 replayMsgId 作为被引用消息的真实ID（但要排除 "0" 的情况）
    const replayMsgId = replyElement.replayMsgId;
    let referencedMessageId: string | undefined = (replayMsgId && replayMsgId !== '0') ? replayMsgId : undefined;
    
    // sourceMsgIdInRecords 用于内部查找（在 records 数组中）
    const sourceMsgId = replyElement.sourceMsgIdInRecords;
    let referencedMessage: RawMessage | undefined;
    let source: 'messageMap' | 'records' | 'sourceMsgText' | 'sourceMsgTextElems' | 'referencedMsg' | 'seq' | 'none' = 'none';
    
    // 1. 尝试用 replayMsgId 从全局消息映射中查找（replayMsgId才是真实被引用消息ID）
    if (referencedMessageId && this.messageMap.has(referencedMessageId)) {
      referencedMessage = this.messageMap.get(referencedMessageId);
      source = 'messageMap';
    }
    
    // 2. 如果全局映射中找不到，再从当前消息的 records 数组中查找
    if (!referencedMessage && sourceMsgId && sourceMsgId !== '0' && message.records && message.records.length > 0) {
      referencedMessage = message.records.find((record: RawMessage) => record.msgId === sourceMsgId);
      if (referencedMessage) {
        referencedMessageId = referencedMessage.msgId;
        source = 'records';
      }
    }
    
    // 3. 如果还是找不到，尝试用 replayMsgSeq 匹配 msgSeq
    if (!referencedMessage && replyElement.replayMsgSeq) {
      for (const [msgId, msg] of this.messageMap.entries()) {
        if (msg.msgSeq === replyElement.replayMsgSeq) {
          referencedMessage = msg;
          referencedMessageId = msg.msgId;
          source = 'seq';
          break;
        }
      }
    }
    
    // 4. 如果还找不到，尝试用 replyMsgClientSeq 匹配
    if (!referencedMessage && replyElement.replyMsgClientSeq) {
      for (const [msgId, msg] of this.messageMap.entries()) {
        if (msg.clientSeq === replyElement.replyMsgClientSeq) {
          referencedMessage = msg;
          referencedMessageId = msg.msgId;
          source = 'seq';
          break;
        }
      }
    }

    // #289：被引用消息发件人显示名解析。
    // 历史上这里在很多分支会落到 senderUidStr (`u_xxxxxxxx` 形式) 或 senderUin（QQ 号）。
    // 现在统一走 resolveReplySenderName：
    //   - 找得到原消息：套用 getSenderDisplayInfo 的群名片 / 备注 / 昵称优先级；
    //   - 找不到原消息：先用 replyElement 自带的字段，再回退到本批消息里同一发件人
    //     已被 cacheSenderInfo 收录的可读名字；
    //   - 全部失败时优先用 senderUin（QQ 号）而不是 senderUidStr（u_xxx）。
    const senderName = this.resolveReplySenderName(replyElement, message, referencedMessage);
    const result: {
      messageId: string;
      referencedMessageId: string | undefined;
      senderUin: string;
      senderName: string;
      content: string;
      timestamp: number;
      previewElements: ReplyPreviewElement[];
    } = {
      messageId: sourceMsgId || replyElement.replayMsgId || replyElement.replayMsgSeq || '0',
      referencedMessageId: referencedMessageId || undefined,  // 确保不会是 "0"
      senderUin: replyElement.senderUin || (referencedMessage?.senderUin ?? ''),
      senderName,
      content: '原消息',
      timestamp: 0,
      previewElements: []
    };

    // 如果找到了被引用的消息，从中提取内容
    if (referencedMessage) {
      // 保持messageId为sourceMsgId，referencedMessageId已经在前面设置
      result.senderUin = referencedMessage.senderUin;

      // 提取被引用消息的文本内容 + 结构化的 previewElements
      if (referencedMessage.elements && referencedMessage.elements.length > 0) {
        const parts: string[] = [];
        for (const element of referencedMessage.elements) {
          if (element.textElement?.content) {
            parts.push(element.textElement.content);
            result.previewElements.push({ type: 'text', text: element.textElement.content });
          } else if (element.picElement) {
            parts.push('[图片]');
            result.previewElements.push({
              type: 'image',
              text: '[图片]',
              md5: element.picElement.md5HexStr || '',
              originUrl: element.picElement.originImageUrl || '',
              fileName: element.picElement.fileName || ''
            });
          } else if (element.videoElement) {
            // issue #128：视频 / 文件元素带文件名时直接挂在占位符里，
            // JSON / TXT / Excel 这些纯文本导出能看出原消息引用的是哪个视频。
            const videoName = element.videoElement.fileName || '';
            const videoText = videoName ? `[视频:${videoName}]` : '[视频]';
            parts.push(videoText);
            result.previewElements.push({
              type: 'video',
              text: videoText,
              fileName: videoName
            });
          } else if (element.pttElement) {
            parts.push('[语音]');
            result.previewElements.push({ type: 'audio', text: '[语音]' });
          } else if (element.fileElement) {
            const fileName = element.fileElement.fileName || '';
            const fileText = fileName ? `[文件:${fileName}]` : '[文件]';
            parts.push(fileText);
            result.previewElements.push({
              type: 'file',
              text: fileText,
              fileName
            });
          } else if (element.faceElement) {
            // issue #128：表情元素优先用 faceText / faceMap 给出的可读名（如 /微 /奋），
            // 而不是抛"[表情341]"这种调用方还得自己查表的占位文本。
            const faceId = element.faceElement.faceIndex?.toString() || '';
            const faceText = element.faceElement.faceText || this.faceMap.get(faceId) || `表情${faceId}`;
            const facePart = faceText.startsWith('[') ? faceText : `[${faceText}]`;
            parts.push(facePart);
            result.previewElements.push({
              type: 'face',
              text: facePart,
              faceIndex: element.faceElement.faceIndex
            });
          } else if (element.marketFaceElement) {
            const faceName = element.marketFaceElement.faceName || '超级表情';
            parts.push(`[${faceName}]`);
            result.previewElements.push({
              type: 'marketFace',
              text: `[${faceName}]`,
              faceName,
              url: this.generateMarketFaceUrl(element.marketFaceElement.emojiId || '')
            });
          }
        }
        if (parts.length > 0) {
          result.content = parts.join('');
        }
      }
      
      if (referencedMessage.msgTime) {
        result.timestamp = parseInt(referencedMessage.msgTime) || 0;
      }
    } else {
      // 如果没有找到被引用的消息，尝试从 replyElement 中提取内容（备用方案）
    if (replyElement.sourceMsgText) {
      result.content = replyElement.sourceMsgText;
        source = 'sourceMsgText';
    } else if (replyElement.sourceMsgTextElems && replyElement.sourceMsgTextElems.length > 0) {
      const parts = [];
      for (let i = 0; i < replyElement.sourceMsgTextElems.length; i++) {
        const e = replyElement.sourceMsgTextElems[i];
        if (e?.textElement?.content) parts.push(e.textElement.content);
      }
        if (parts.length > 0) {
          result.content = parts.join('');
          source = 'sourceMsgTextElems';
        }
    } else if (replyElement.referencedMsg && replyElement.referencedMsg.msgBody) {
      result.content = replyElement.referencedMsg.msgBody;
        source = 'referencedMsg';
      }
    }

    if (replyElement.replayMsgTime) result.timestamp = replyElement.replayMsgTime;

    return result;
  }

  /**
   * 把被引用消息（reply）发件人解析成一个尽量可读的名字。
   *
   * 解析顺序：
   *  1. 命中 messageMap / records 的原消息：复用 getSenderDisplayInfo（群名片 > 备注 > 昵称）。
   *  2. 否则用 replyElement 自带的 senderMemberName / senderNick 字段。
   *  3. 否则按 senderUid / senderUin 查 senderInfoCache，取本批消息里同发件人的可读名字。
   *  4. 最后退回 senderUin（QQ 号），再退回 senderUidStr（`u_xxx` 形式）。
   *
   * 历史代码会先把 senderName 设为 `replyElement.senderUidStr`，再有条件覆盖；当原消息
   * 不在导出范围内、且 replyElement 也没有 senderNick 字段时就直接显示 `u_xxx`，
   * 即 #289 报告的情况。
   */
  private resolveReplySenderName(
    replyElement: any,
    message: RawMessage,
    referencedMessage: RawMessage | undefined
  ): string {
    if (referencedMessage) {
      const display = this.getSenderDisplayInfo(referencedMessage);
      if (display.name && display.name !== '未知用户') return display.name;
    }

    const senderMemberName = this.getTrimmedText(replyElement.senderMemberName);
    const senderNick = this.getTrimmedText(replyElement.senderNick);
    const isGroupChat = message.chatType === 2;
    const preferGroupMemberName = isGroupChat && this.options.preferGroupMemberName !== false;
    if (preferGroupMemberName && senderMemberName) return senderMemberName;
    if (senderNick) return senderNick;
    if (senderMemberName) return senderMemberName;

    const cached = this.lookupCachedSenderInfo({
      senderUid: replyElement.senderUid,
      senderUin: replyElement.senderUin
    } as RawMessage);
    if (cached) {
      const fromCache = preferGroupMemberName
        ? (cached.groupCard || cached.remark || cached.nickname)
        : (cached.remark || cached.nickname || cached.groupCard);
      if (fromCache) return fromCache;
    }

    const senderUin = this.getTrimmedText(replyElement.senderUin);
    if (senderUin) return senderUin;
    const senderUidStr = this.getTrimmedText(replyElement.senderUidStr);
    if (senderUidStr) return senderUidStr;
    return '';
  }

  private generateMarketFaceUrl(emojiId: string): string {
    if (emojiId.length < 2) return '';
    const prefix = emojiId.substring(0, 2);
    return `https://gxh.vip.qq.com/club/item/parcel/item/${prefix}/${emojiId}/raw300.gif`;
  }

  private parseGrayTipElement(grayTip: any): MessageElementData {
    const subType = grayTip.subElementType;
    let summary = '系统消息';
    let text = '';

    try {
      if (subType === 1 && grayTip.revokeElement) {
        const revokeInfo = grayTip.revokeElement;
        const operatorName = revokeInfo.operatorName || '用户';
        const originalSenderName = revokeInfo.origMsgSenderName || '用户';

        if (revokeInfo.isSelfOperate) {
          text = `${operatorName} 撤回了一条消息`;
        } else if (operatorName === originalSenderName) {
          text = `${operatorName} 撤回了一条消息`;
        } else {
          text = `${operatorName} 撤回了 ${originalSenderName} 的消息`;
        }
        if (revokeInfo.wording) text = revokeInfo.wording;
        summary = text;
      } else if (subType === 4 && grayTip.groupElement) {
        text = grayTip.groupElement.content || '群聊更新';
        summary = text;
      } else if (subType === 17 && grayTip.jsonGrayTipElement) {
        const jsonContent = grayTip.jsonGrayTipElement.jsonStr || '{}';
        try {
          const parsed = fastJsonParse(jsonContent);
          text = parsed.prompt || parsed.content || '系统提示';
        } catch {
          text = '系统提示';
        }
        summary = text;
      } else if (grayTip.aioOpGrayTipElement) {
        const aioOp = grayTip.aioOpGrayTipElement;
        if (aioOp.operateType === 1) {
          const fromUser = aioOp.peerName || '用户';
          const toUser = aioOp.targetName || '用户';
          text = `${fromUser} 拍了拍 ${toUser}`;
          if (aioOp.suffix) text += ` ${aioOp.suffix}`;
        } else {
          text = aioOp.content || '互动消息';
        }
        summary = text;
      } else {
        const content = grayTip.content || grayTip.text || grayTip.wording;
        if (content) {
          text = content;
          summary = content;
        } else {
          text = `系统提示 (类型: ${subType})`;
          summary = text;
        }
      }
    } catch (error) {
      console.warn('[SimpleMessageParser] 解析灰条消息失败:', error, grayTip);
      text = '系统消息';
      summary = text;
    }

    return {
      type: 'system',
      data: {
        subType,
        text,
        summary,
        originalData: grayTip
      }
    };
  }

  private getSystemMessageSummary(element: any): string {
    const t = element.elementType;
    switch (t) {
      case 8:  // ElementType.GreyTip
        return '系统提示消息';
      case 9:  // ElementType.WALLET
        return '钱包/红包消息';
      case 10: // ElementType.ARK
        return 'Ark卡片消息';
      case 11: // ElementType.MFACE
        return '商城表情';
      case 12: // ElementType.LIVEGIFT
        return '直播礼物';
      case 13: // ElementType.STRUCTLONGMSG
        return '长消息';
      case 14: // ElementType.MARKDOWN
        return 'Markdown消息';
      case 15: // ElementType.GIPHY
        return 'Giphy动图';
      case 16: // ElementType.MULTIFORWARD
        return '合并转发';
      case 17: // ElementType.INLINEKEYBOARD
        return '内联键盘';
      case 18: // ElementType.INTEXTGIFT
        return '文内礼物';
      case 19: // ElementType.CALENDAR
        return '日历';
      case 20: // ElementType.YOLOGAMERESULT
        return 'YOLO游戏结果';
      case 21: // ElementType.AVRECORD
        return '音视频通话记录';
      case 22: // ElementType.FEED
        return '动态';
      case 23: // ElementType.TOFURECORD
        return '豆腐记录';
      case 24: // ElementType.ACEBUBBLE
        return 'ACE气泡';
      case 25: // ElementType.ACTIVITY
        return '活动';
      case 26: // ElementType.TOFU
        return '豆腐';
      case 27: // ElementType.FACEBUBBLE
        return '表情气泡';
      case 28: // ElementType.SHARELOCATION
        return '位置分享';
      case 29: // ElementType.TASKTOPMSG
        return '置顶任务消息';
      case 43: // ElementType.RECOMMENDEDMSG
        return '推荐消息';
      case 44: // ElementType.ACTIONBAR
        return '操作栏';
      default:
        return `系统消息 (类型: ${t})`;
    }
  }

  /**
   * 初始化QQ表情映射表
   */
  private initializeFaceMap(): void {
    try {
      // ES模块中获取当前文件目录
      const __filename = fileURLToPath(import.meta.url);
      const __dirname = path.dirname(__filename);
      
      const faceConfigPath = path.join(__dirname, 'face_config.json');
      if (fs.existsSync(faceConfigPath)) {
        const faceConfig = JSON.parse(fs.readFileSync(faceConfigPath, 'utf-8'));
        if (faceConfig.sysface && Array.isArray(faceConfig.sysface)) {
          for (const face of faceConfig.sysface) {
            if (face.QSid && face.QDes) {
              this.faceMap.set(face.QSid.toString(), face.QDes);
            }
          }
        }
      }
    } catch (error) {
      // 加载失败时静默失败，使用默认的"表情{ID}"格式
      console.warn('[SimpleMessageParser] 加载表情映射失败:', error);
    }
  }
}
