/**
 * ChatArea - 聊天区域组件
 */

import { state, KEYS } from '../state/state.js';
import { logError, logSession } from '../services/log-service.js';
import { marked } from '../utils/marked-config.js';
import { escapeHtml, formatTime } from '../utils/utils.js';
import { estimateTokens } from '../services/context-manager.js';
import { getToolIcon, formatToolName } from '../services/tool-registry.js';
import type { ChatSession, ChatMessage, ToolCallRecord } from '../types.js';

function getFileIcon(filename: string): string {
  const name = filename.toLowerCase();
  if (name === 'dockerfile') return '🐳';
  if (name === 'makefile' || name === 'gnumakefile') return '🔨';
  const ext = filename.split('.').pop()?.toLowerCase() || '';
  const map: Record<string, string> = {
    py: '🐍', js: '💛', ts: '🔷', tsx: '⚛️', jsx: '⚛️',
    java: '☕', go: '🐹', rs: '🦀', rb: '💎', php: '🐘',
    cpp: '⚙️', c: '⚙️', sh: '🐚', html: '🌐', css: '🎨',
    json: '📋', yaml: '📋', yml: '📋', md: '📝', txt: '📄',
    sql: '🗃️', xml: '📰', csv: '📊', diff: '🔀',
  };
  return map[ext] || '📄';
}

let chatAreaEl: HTMLElement;
let messagesContainerEl: HTMLElement;
let emptyStateEl: HTMLElement;
let scrollBtnEl: HTMLElement;
let autoScroll = true;
let currentPlaceholder: HTMLElement | null = null;

/** 渲染可折叠系统提示词卡片 */
let _sysPromptIdCounter = 0;
function renderSystemPromptCard(sysPrompt: string): string {
  const id = `sysprompt_${++_sysPromptIdCounter}`;
  const tokenEstimate = estimateTokens(sysPrompt);
  const preview = sysPrompt.length > 120 ? sysPrompt.slice(0, 120).replace(/\n/g, ' ') + '…' : sysPrompt.replace(/\n/g, ' ');
  return `<div class="system-prompt-card">
    <div class="system-prompt-header" onclick="
      var el=document.getElementById('${id}');
      var chev=this.querySelector('.sp-chevron');
      el.style.display=el.style.display==='none'?'':'none';
      chev.classList.toggle('sp-expanded');
    ">
      <span class="sp-icon">📋</span>
      <span class="sp-title">系统提示词</span>
      <span class="sp-tokens">~${tokenEstimate} tokens</span>
      <span class="sp-preview">${escapeHtml(preview)}</span>
      <svg class="sp-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
    </div>
    <div class="system-prompt-body" id="${id}" style="display:none;">
      <pre>${escapeHtml(sysPrompt)}</pre>
    </div>
  </div>`;
}

export function initChatArea(): void {
  chatAreaEl = document.querySelector('#chatArea')!;
  messagesContainerEl = document.querySelector('#messagesContainer')!;
  emptyStateEl = document.querySelector('#emptyState')!;
  scrollBtnEl = document.querySelector('#scrollToBottom')!;

  chatAreaEl.addEventListener('scroll', () => {
    const atBottom = isAtBottom();
    if (atBottom) {
      autoScroll = true;
      scrollBtnEl.style.display = 'none';
    } else {
      autoScroll = false;
      scrollBtnEl.style.display = '';
    }
  });

  scrollBtnEl.addEventListener('click', () => {
    autoScroll = true;
    scrollBtnEl.style.display = 'none';
    chatAreaEl.scrollTo({ top: chatAreaEl.scrollHeight, behavior: 'smooth' });
  });
}

function isAtBottom(): boolean {
  return chatAreaEl.scrollHeight - chatAreaEl.scrollTop - chatAreaEl.clientHeight < 60;
}

export function safeMarkdown(text: unknown): string {
  if (!text && text !== 0) return '';
  const str = typeof text === 'string' ? text : String(text);
  if (!str) return '';
  try {
    return marked(str);
  } catch {
    return escapeHtml(str);
  }
}

export function scrollToBottom(): void {
  if (!autoScroll) return;
  requestAnimationFrame(() => {
    chatAreaEl.scrollTop = chatAreaEl.scrollHeight;
  });
}

/** 发送新消息时重置自动滚动，确保新消息可见 */
export function resetAutoScroll(): void {
  autoScroll = true;
  if (scrollBtnEl) scrollBtnEl.style.display = 'none';
}

/** P1-1: 已渲染消息索引集合，用于稳定增量 diff（替代 DOM querySelectorAll 计数） */
const _renderedMsgIndices = new Set<number>();

/** R31: 最大渲染消息数（超过此数时仅渲染最近的消息，避免 DOM 过载） */
const MAX_RENDERED_MESSAGES = 200;

/** 标记当前渲染批次中系统提示词卡片是否已渲染（每会话仅首条 assistant 消息展示） */
let _sysPromptRendered = false;

/** R31: 增量渲染 — 仅追加新消息，避免全量重建 */
export function renderMessages(): void {
  const currentSession = state.get<ChatSession | null>(KEYS.CURRENT_SESSION);
  const msgs = currentSession ? currentSession.messages : [];

  if (msgs.length === 0) {
    messagesContainerEl.innerHTML = '';
    currentPlaceholder = null;
    _renderedMsgIndices.clear();
    _sysPromptRendered = false;
    emptyStateEl.style.display = '';
    messagesContainerEl.style.display = 'none';
    return;
  }

  emptyStateEl.style.display = 'none';
  messagesContainerEl.style.display = '';

  // R31: 增量渲染 — 检查是否需要全量重建
  const needsFullRebuild = _renderedMsgIndices.size === 0
    || _renderedMsgIndices.size > msgs.length
    || Array.from(_renderedMsgIndices).some(idx => idx >= msgs.length);

  if (needsFullRebuild) {
    // 全量重建（首次加载或消息被截断/压缩后）
    messagesContainerEl.innerHTML = '';
    currentPlaceholder = null;
    _renderedMsgIndices.clear();
    _sysPromptRendered = false;

    // R31: 仅渲染最近 MAX_RENDERED_MESSAGES 条消息，避免 DOM 过载
    const startIdx = Math.max(0, msgs.length - MAX_RENDERED_MESSAGES);
    if (startIdx > 0) {
      const noticeDiv = document.createElement('div');
      noticeDiv.className = 'msg-truncated-notice';
      noticeDiv.style.cssText = 'text-align:center;padding:8px;font-size:12px;color:var(--text-muted);';
      noticeDiv.textContent = `⋯ 已折叠前 ${startIdx} 条消息，仅显示最近 ${MAX_RENDERED_MESSAGES} 条 ⋯`;
      messagesContainerEl.appendChild(noticeDiv);
    }

    for (let i = startIdx; i < msgs.length; i++) {
      const msg = msgs[i];
      if (msg.role === 'system') continue;
      if (msg.role === 'assistant' && !msg.content && !msg.think && (!msg.toolCalls || msg.toolCalls.length === 0)) continue;
      appendMessageDOM(msgs[i], i);
      _renderedMsgIndices.add(i);
    }
  } else {
    // R31: 增量追加 — 只渲染新增的消息
    for (let i = 0; i < msgs.length; i++) {
      if (_renderedMsgIndices.has(i)) continue;
      const msg = msgs[i];
      if (msg.role === 'system') continue;
      if (msg.role === 'assistant' && !msg.content && !msg.think && (!msg.toolCalls || msg.toolCalls.length === 0)) continue;
      appendMessageDOM(msgs[i], i);
      _renderedMsgIndices.add(i);
    }
  }

  // R38: 为代码块添加复制按钮
  addCodeBlockCopyButtons();
  // R40: 为消息添加操作按钮
  addMessageActions();

  scrollToBottom();
}

export function appendMessageDOM(msg: ChatMessage, index: number): void {
  const div = document.createElement('div');
  div.className = `message ${msg.role}`;
  div.dataset.index = String(index);

  const avatar = msg.role === 'user' ? '👤' : '🤖';
  const roleLabel = msg.role === 'user' ? '你' : 'AI';
  const modelTag = (msg.role === 'assistant' && msg.model)
    ? `<span class="model-tag">${escapeHtml(msg.model)}</span>`
    : '';

  let contentHtml = '';
  let safeContent = (msg.content != null) ? String(msg.content) : '';

  if (msg.role === 'assistant') {
    // ── 系统提示词折叠卡片（仅会话首条 assistant 消息展示，避免重复）──
    if (!_sysPromptRendered) {
      const sysPrompt = state.get<string>('_lastSystemPrompt', '');
      if (sysPrompt) {
        contentHtml += renderSystemPromptCard(sysPrompt);
      }
      _sysPromptRendered = true;
    }

    if (msg.think) {
      contentHtml += `
        <div class="think-block">
          <div class="think-header" onclick="this.nextElementSibling.style.display = this.nextElementSibling.style.display === 'none' ? '' : 'none'; this.querySelector('.chevron').classList.toggle('expanded');">
            <span>🤔 思考过程</span>
            <svg class="chevron expanded" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
          </div>
          <div class="think-content"><pre>${escapeHtml(msg.think)}</pre></div>
        </div>`;
    }
    if (safeContent) {
      contentHtml += `<div class="msg-content">${safeMarkdown(safeContent)}</div>`;
    } else {
      contentHtml += `<div class="msg-content"></div>`;
    }
  } else {
    contentHtml += `<div class="msg-content">${escapeHtml(safeContent)}</div>`;
  }

  if (msg.images && msg.images.length > 0) {
    contentHtml += '<div class="msg-images">';
    for (const img of msg.images) {
      contentHtml += `<img class="msg-img" src="data:image/png;base64,${img}" alt="用户图片" data-lightbox="true">`;
    }
    contentHtml += '</div>';
  }

  if ((msg as any)._videos && (msg as any)._videos.length > 0) {
    for (const v of (msg as any)._videos) {
      contentHtml += `<div class="msg-video-indicator">🎬 ${v.fileName} · ${v.frameCount} 帧 · 1fps</div>`;
    }
  } else if ((msg as any)._videoFrames && (msg as any)._videoFrames.length > 0) {
    const n = (msg as any)._videoFrames.length;
    contentHtml += `<div class="msg-video-indicator">🎬 ${n} 帧 · 1fps</div>`;
  }

  if (msg.files && msg.files.length > 0) {
    contentHtml += '<div class="msg-files">';
    for (const f of msg.files) {
      const icon = getFileIcon(f.name);
      contentHtml += `<div class="file-chip"><span class="file-icon">${icon}</span><span class="file-name">${escapeHtml(f.name)}</span></div>`;
    }
    contentHtml += '</div>';
  }

  if (msg.role === 'user' && (!msg.content || msg.content.trim() === '') && (msg.images?.length || 0) > 0) {
    contentHtml = contentHtml.replace('<div class="msg-content"></div>', '');
  }

  // ── 工具调用历史记录（可折叠）──
  if (msg.role === 'assistant' && msg.toolCalls && msg.toolCalls.length > 0) {
    const tcCount = msg.toolCalls.length;
    const successCount = msg.toolCalls.filter(t => t.status === 'success').length;
    const errorCount = msg.toolCalls.filter(t => t.status === 'error').length;
    const summary = msg.toolCalls.map(t => {
      const names: Record<string, string> = {
        read_file: 'read_file', write_file: 'write_file', list_directory: 'list_directory',
        search_files: 'search_files', run_command: 'run_command', web_fetch: 'web_fetch',
        web_search: 'web_search', git: 'git', browser_open: 'browser_open',
        memory_search: 'memory_search', memory_add: 'memory_add', spawn_task: 'spawn_task',
      };
      return names[t.name] || t.name;
    }).slice(0, 5).join(', ') + (tcCount > 5 ? ` +${tcCount - 5}` : '');

    contentHtml += `<div class="tool-calls-history">
      <div class="tool-calls-toggle" onclick="this.parentElement.classList.toggle('expanded')">
        <span class="tool-calls-chevron">▸</span>
        <span class="tool-calls-summary">🔧 ${tcCount} 个工具调用（✅${successCount} ❌${errorCount}）</span>
      </div>
      <div class="tool-calls-list">
        ${msg.toolCalls.map(tc => renderToolCallCard(tc)).join('')}
      </div>
    </div>`;
  }

  if (msg.timestamp) {
    contentHtml += `<div class="msg-stats"><span>${formatTime(msg.timestamp)}</span></div>`;
  }

  div.innerHTML = `
    <div class="msg-avatar">${avatar}</div>
    <div class="msg-body">
      <div class="msg-role">${roleLabel}${modelTag}</div>
      ${contentHtml}
    </div>
  `;

  messagesContainerEl.appendChild(div);
}

function renderToolCallCard(tc: ToolCallRecord): string {
  const statusLabels: Record<string, string> = {
    pending: '📝 准备中…', running: '🔄 执行中', success: '✅ 完成', error: '❌ 失败', cancelled: '🚫 已取消'
  };

  const icon = getToolIcon(tc.name);
  const name = formatToolName(tc.name);
  const status = statusLabels[tc.status] || tc.status;
  const args = tc.arguments || {};

  let paramsHtml = '';
  if (args.path) paramsHtml += `<div class="tool-param">路径: <code>${escapeHtml(String(args.path))}</code></div>`;
  if (args.command) paramsHtml += `<div class="tool-param">命令: <code>${escapeHtml(String(args.command))}</code></div>`;
  if (args.query) paramsHtml += `<div class="tool-param">查询: <code>${escapeHtml(String(args.query))}</code></div>`;
  if (args.url) paramsHtml += `<div class="tool-param">URL: <code>${escapeHtml(String(args.url))}</code></div>`;
  if (args.source) paramsHtml += `<div class="tool-param">源: <code>${escapeHtml(String(args.source))}</code></div>`;
  if (args.destination) paramsHtml += `<div class="tool-param">目标: <code>${escapeHtml(String(args.destination))}</code></div>`;
  if (args.selector) paramsHtml += `<div class="tool-param">选择器: <code>${escapeHtml(String(args.selector))}</code></div>`;
  if (args.action) paramsHtml += `<div class="tool-param">操作: <code>${escapeHtml(String(args.action))}</code></div>`;

  // 失败/取消时显示错误信息（成功结果在工作空间工具页签查看）
  let errorHtml = '';
  if (tc.status === 'error' && tc.result?.error) {
    errorHtml = `<div class="tool-call-error">❌ ${escapeHtml(String(tc.result.error))}</div>`;
  } else if (tc.status === 'cancelled') {
    errorHtml = `<div class="tool-call-error">🚫 用户取消了操作</div>`;
  }

  return `<div class="tool-call-card tool-call-${tc.status}">
    <div class="tool-call-header">
      <span class="tool-call-icon">${icon}</span>
      <span class="tool-call-name">${name}</span>
      <span class="tool-call-status">${status}</span>
    </div>
    ${paramsHtml ? `<div class="tool-call-params">${paramsHtml}</div>` : ''}
    ${errorHtml}
  </div>`;
}

/**
 * 流式 Markdown 智能补全：检测并修复流式传输中不完整的 Markdown 语法
 * - 未闭合的代码块（``` / ~~~）→ 追加闭合标记
 * - 未闭合的行内代码（`）→ 追加闭合 `
 * - 未闭合的粗体（**）→ 追加闭合 **
 * 返回补全后的字符串（不修改原始内容，仅用于渲染）
 */
function patchIncompleteMarkdown(md: string): string {
  let patched = md;

  // ── 代码块围栏检测（``` 或 ~~~）──
  // 统计未闭合的围栏对
  const fenceRegex = /^([`~]{3,})/gm;
  const openFences: string[] = [];
  let match: RegExpExecArray | null;
  while ((match = fenceRegex.exec(patched)) !== null) {
    openFences.push(match[1]);
  }
  // 如果有奇数个围栏，说明最后一个未闭合
  if (openFences.length % 2 === 1) {
    const lastFence = openFences[openFences.length - 1];
    patched += '\n' + lastFence;
  }

  // ── 行内代码反引号检测（仅当不在代码块内时才有意义）──
  // 简化处理：统计不在代码围栏内的奇数个反引号
  // 这里用一个保守的检测：如果末尾有奇数个 `（且不是围栏），补一个
  const inlineBackticks = (patched.match(/(?<!`)`(?!`)/g) || []).length;
  if (inlineBackticks % 2 === 1) {
    patched += '`';
  }

  return patched;
}

// ── 流式渲染节流状态 ──
let _streamRenderPending = false;
let _streamRenderContent = '';
let _streamRenderThink: string | null = null;
let _streamRenderModel: string | undefined;
// R58: 流式渲染优化 — 增量阈值 + 内容指纹，避免高频小片段冗余重渲染
let _streamLastRenderedLen = 0;     // 上次渲染时的内容长度
let _streamLastRenderedHash = 0;     // 上次渲染时的内容哈希（简单 DJB2）
const STREAM_MIN_DELTA = 15;         // 最小增量字符数（不足则跳过本次渲染）

/** R58: 快速 DJB2 哈希 */
function _djb2(str: string): number {
  let hash = 5381;
  for (let i = 0; i < str.length; i++) {
    hash = ((hash << 5) + hash + str.charCodeAt(i)) & 0x7fffffff;
  }
  return hash;
}

/**
 * 执行实际的流式 Markdown 渲染
 * R58: 添加增量阈值检查 — 内容增长不足 STREAM_MIN_DELTA 字符时跳过渲染
 */
function _doStreamRender(): void {
  _streamRenderPending = false;
  const lastMsg = currentPlaceholder;
  if (!lastMsg) return;

  // R58: 增量阈值检查 — 内容变化不足时跳过本次渲染
  const contentLen = _streamRenderContent.length;
  const contentHash = _djb2(_streamRenderContent);
  if (contentLen > 0 && contentHash === _streamLastRenderedHash) return; // 内容完全相同
  if (contentLen - _streamLastRenderedLen < STREAM_MIN_DELTA && contentLen > 100) {
    // 内容增长不足且已有内容 → 跳过本次，等下次 rAF
    return;
  }
  _streamLastRenderedLen = contentLen;
  _streamLastRenderedHash = contentHash;

  const contentDiv = lastMsg.querySelector('.msg-content');
  if (contentDiv && _streamRenderContent) {
    // 智能补全不完整的 Markdown 语法后渲染
    const patched = patchIncompleteMarkdown(_streamRenderContent);
    contentDiv.innerHTML = safeMarkdown(patched);
  }
  scrollToBottom();
}

export function updateLastAssistantMessage(
  content: string, think: string | null, _stats: unknown, model?: string,
  timestamp?: number, toolCalls?: ToolCallRecord[]
): void {
  const lastMsg = currentPlaceholder;
  if (!lastMsg) return;

  const isFinal = toolCalls !== undefined || timestamp !== undefined;

  if (lastMsg.classList.contains('loading')) {
    lastMsg.classList.remove('loading');
    const loadingDots = lastMsg.querySelector('.loading-dots');
    const loadingText = lastMsg.querySelector('.loading-text');
    if (loadingDots) loadingDots.remove();
    if (loadingText) loadingText.remove();

    // 流式消息首次转正：注入系统提示词折叠卡片（仅当会话中无前序 assistant 消息时）
    const sysPrompt = state.get<string>('_lastSystemPrompt', '');
    if (sysPrompt && !lastMsg.querySelector('.system-prompt-card')) {
      const session = state.get<ChatSession | null>(KEYS.CURRENT_SESSION);
      const hasPrevAssistant = session?.messages.some(m => m.role === 'assistant') ?? false;
      if (!hasPrevAssistant) {
        const msgBody = lastMsg.querySelector('.msg-body');
        const tempDiv = document.createElement('div');
        tempDiv.innerHTML = renderSystemPromptCard(sysPrompt);
        const card = tempDiv.firstElementChild!;
        const thinkBlock = msgBody?.querySelector('.think-block');
        if (thinkBlock) {
          msgBody!.insertBefore(card, thinkBlock);
        } else {
          msgBody!.insertBefore(card, msgBody!.firstChild);
        }
      }
    }
  }

  if (model) {
    let modelTag = lastMsg.querySelector('.model-tag');
    if (!modelTag) {
      modelTag = document.createElement('span');
      modelTag.className = 'model-tag';
      const roleEl = lastMsg.querySelector('.msg-role');
      if (roleEl) roleEl.appendChild(modelTag);
    }
    modelTag.textContent = model;
  }

  const contentDiv = lastMsg.querySelector('.msg-content');
  const safeContent = (content != null) ? String(content) : '';
  if (contentDiv && safeContent) {
    if (isFinal) {
      // 最终渲染：完整 Markdown + 代码块复制按钮
      contentDiv.innerHTML = safeMarkdown(safeContent);
      addCodeBlockCopyButtons(contentDiv);
    } else {
      // 流式期间：实时 Markdown 渲染 + rAF 节流
      _streamRenderContent = safeContent;
      _streamRenderThink = think;
      _streamRenderModel = model;
      if (!_streamRenderPending) {
        _streamRenderPending = true;
        requestAnimationFrame(_doStreamRender);
      }
    }
  } else if (isFinal) {
    // R58: 最终渲染时重置增量追踪状态
    _streamLastRenderedLen = 0;
    _streamLastRenderedHash = 0;
  }

  let thinkBlock = lastMsg.querySelector('.think-block');
  if (think) {
    if (!thinkBlock) {
      const msgBody = lastMsg.querySelector('.msg-body')!;
      thinkBlock = document.createElement('div');
      thinkBlock.className = 'think-block';
      thinkBlock.innerHTML = `
        <div class="think-header" onclick="this.nextElementSibling.style.display = this.nextElementSibling.style.display === 'none' ? '' : 'none'; this.querySelector('.chevron').classList.toggle('expanded');">
          <span>🤔 思考过程</span>
          <svg class="chevron expanded" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
        </div>
        <div class="think-content"><pre></pre></div>
      `;
      msgBody.insertBefore(thinkBlock, contentDiv);
    }
    thinkBlock.querySelector('pre')!.textContent = think;
  }

  // ── 工具调用历史记录（可折叠）──
  if (toolCalls && toolCalls.length > 0) {
    let existingHistory = lastMsg.querySelector('.tool-calls-history');
    if (existingHistory) existingHistory.remove();

    const tcCount = toolCalls.length;
    const successCount = toolCalls.filter(t => t.status === 'success').length;
    const errorCount = toolCalls.filter(t => t.status === 'error').length;

    const historyDiv = document.createElement('div');
    historyDiv.className = 'tool-calls-history';
    historyDiv.innerHTML = `
      <div class="tool-calls-toggle" onclick="this.parentElement.classList.toggle('expanded')">
        <span class="tool-calls-chevron">▸</span>
        <span class="tool-calls-summary">🔧 ${tcCount} 个工具调用（✅${successCount} ❌${errorCount}）</span>
      </div>
      <div class="tool-calls-list">
        ${toolCalls.map(tc => renderToolCallCard(tc)).join('')}
      </div>
    `;
    const msgBody = lastMsg.querySelector('.msg-body');
    if (msgBody) {
      const refNode = contentDiv?.nextSibling || null;
      msgBody.insertBefore(historyDiv, refNode);
    }
  }

  // ── 时间戳 ──
  if (timestamp) {
    let statsDiv = lastMsg.querySelector('.msg-stats');
    if (!statsDiv) {
      statsDiv = document.createElement('div');
      statsDiv.className = 'msg-stats';
      const msgBody = lastMsg.querySelector('.msg-body');
      if (msgBody) msgBody.appendChild(statsDiv);
    }
    statsDiv.innerHTML = `<span>${formatTime(timestamp)}</span>`;
  }

  scrollToBottom();
}

/** 实时追加工具调用卡片到当前 placeholder（直播聊天时使用） */
export function appendToolCallCardToPlaceholder(tc: ToolCallRecord): void {
  if (!currentPlaceholder) return;
  let container = currentPlaceholder.querySelector('.tool-calls-container');
  if (!container) {
    container = document.createElement('div');
    container.className = 'tool-calls-container';
    const msgBody = currentPlaceholder.querySelector('.msg-body');
    if (msgBody) msgBody.appendChild(container);
  }
  const cardDiv = document.createElement('div');
  cardDiv.innerHTML = renderToolCallCard(tc);
  const card = cardDiv.firstElementChild as HTMLElement;
  card.dataset.toolName = tc.name;
  container.appendChild(card);
  scrollToBottom();
}

/** 更新 placeholder 中已有的工具卡片状态（running → success/error） */
export function updateToolCallCardInPlaceholder(tc: ToolCallRecord): void {
  if (!currentPlaceholder) return;
  const container = currentPlaceholder.querySelector('.tool-calls-container');
  if (!container) return;
  // 找到第一个同名且状态为 running 的卡片（CSS.escape 防特殊字符）
  const escapedName = CSS.escape(tc.name);
  const existing = container.querySelector(`.tool-call-card[data-tool-name="${escapedName}"].tool-call-running`) as HTMLElement;
  if (existing) {
    const cardDiv = document.createElement('div');
    cardDiv.innerHTML = renderToolCallCard(tc);
    const newCard = cardDiv.firstElementChild as HTMLElement;
    newCard.dataset.toolName = tc.name;
    existing.replaceWith(newCard);
  } else {
    // 没找到 running 的卡片，直接追加
    appendToolCallCardToPlaceholder(tc);
  }
}

export function appendAssistantPlaceholder(): void {
  const div = document.createElement('div');
  div.className = 'message assistant loading';
  // R34: 增强加载状态 — 添加旋转提示文本
  div.innerHTML = `
    <div class="msg-avatar">🤖</div>
    <div class="msg-body">
      <div class="msg-role">AI</div>
      <div class="msg-content">
        <div class="loading-dots"><span></span><span></span><span></span></div>
        <span class="loading-text" data-hint-index="0">正在思考...</span>
      </div>
    </div>
  `;
  messagesContainerEl.appendChild(div);
  currentPlaceholder = div;

  // R34: 启动旋转提示
  startLoadingHintRotation(div);

  scrollToBottom();
}

// R34: 加载提示旋转动画
const LOADING_HINTS = [
  '正在思考...',
  '正在分析问题...',
  '正在组织回答...',
  '正在查找信息...',
  '正在生成回复...',
];
let _loadingHintInterval: ReturnType<typeof setInterval> | null = null;

function startLoadingHintRotation(el: HTMLElement): void {
  if (_loadingHintInterval) clearInterval(_loadingHintInterval);
  let idx = 0;
  _loadingHintInterval = setInterval(() => {
    if (!el.isConnected) {
      if (_loadingHintInterval) clearInterval(_loadingHintInterval);
      _loadingHintInterval = null;
      return;
    }
    idx = (idx + 1) % LOADING_HINTS.length;
    const hintEl = el.querySelector('.loading-text');
    if (hintEl) {
      hintEl.textContent = LOADING_HINTS[idx];
    }
  }, 3000);
}

export function appendSystemMessage(text: string, tempClass?: string): void {
  const div = document.createElement('div');
  div.style.cssText = 'text-align:center;padding:8px;font-size:12px;color:var(--text-muted);';
  if (tempClass) div.classList.add(tempClass);
  div.textContent = text;
  emptyStateEl.style.display = 'none';
  messagesContainerEl.style.display = '';
  messagesContainerEl.appendChild(div);
  scrollToBottom();
}

/** v4.1: 清除所有消息 DOM 元素（用于 undo/compress 后强制重建） */
export function clearMessagesDOM(): void {
  messagesContainerEl.innerHTML = '';
  _renderedMsgIndices.clear(); // P1-1: 重置 diff 状态
}

export function clearMessages(): void {
  messagesContainerEl.innerHTML = '';
  currentPlaceholder = null;
  _renderedMsgIndices.clear(); // P1-1: 重置 diff 状态
}

/** 清理 currentPlaceholder 引用（当 placeholder 从外部被移除时调用） */
export function resetCurrentPlaceholder(): void {
  if (currentPlaceholder && !currentPlaceholder.isConnected) {
    currentPlaceholder = null;
  }
  // R34: 清理加载提示旋转
  if (_loadingHintInterval) {
    clearInterval(_loadingHintInterval);
    _loadingHintInterval = null;
  }
}

/** 从 DOM 移除当前 placeholder 并清空引用 */
export function removeCurrentPlaceholder(): void {
  if (currentPlaceholder) {
    currentPlaceholder.remove();
    currentPlaceholder = null;
  }
}

export function enableAutoScroll(): void {
  autoScroll = true;
  if (scrollBtnEl) scrollBtnEl.style.display = 'none';
}

// ── R38: 代码块复制按钮 ──

/** R38: 为代码块添加复制按钮 */
function addCodeBlockCopyButtons(scope?: ParentNode): void {
  const root = scope || messagesContainerEl;
  const codeBlocks = root.querySelectorAll('pre:not([data-copy-added])');
  for (const pre of codeBlocks) {
    pre.setAttribute('data-copy-added', 'true');
    const btn = document.createElement('button');
    btn.className = 'code-copy-btn';
    btn.textContent = '📋 复制';
    btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:4px;cursor:pointer;opacity:0;transition:opacity 0.2s;';
    // 确保 pre 是相对定位
    if (getComputedStyle(pre).position === 'static') {
      (pre as HTMLElement).style.position = 'relative';
    }
    pre.addEventListener('mouseenter', () => { btn.style.opacity = '1'; });
    pre.addEventListener('mouseleave', () => { btn.style.opacity = '0'; });
    btn.addEventListener('click', (e) => {
      e.preventDefault();
      const code = pre.querySelector('code');
      const text = code ? code.textContent : pre.textContent;
      if (text) {
        navigator.clipboard.writeText(text).then(() => {
          btn.textContent = '✅ 已复制';
          setTimeout(() => { btn.textContent = '📋 复制'; }, 2000);
        }).catch(() => {
          btn.textContent = '❌ 失败';
          setTimeout(() => { btn.textContent = '📋 复制'; }, 2000);
        });
      }
    });
    pre.appendChild(btn);
  }
}

// ── R40: 消息操作按钮 ──

/** R40: 为消息添加操作按钮（复制、重试） */
function addMessageActions(): void {
  const messages = messagesContainerEl.querySelectorAll('.message:not([data-actions-added])');
  for (const msg of messages) {
    msg.setAttribute('data-actions-added', 'true');
    const actionBar = document.createElement('div');
    actionBar.className = 'msg-actions';
    actionBar.style.cssText = 'position:absolute;top:4px;right:4px;display:flex;gap:4px;opacity:0;transition:opacity 0.2s;';

    // 确保 message 是相对定位
    if (getComputedStyle(msg).position === 'static') {
      (msg as HTMLElement).style.position = 'relative';
    }

    // 复制按钮
    const copyBtn = document.createElement('button');
    copyBtn.className = 'msg-action-btn msg-copy-btn';
    copyBtn.textContent = '📋';
    copyBtn.title = '复制消息';
    copyBtn.style.cssText = 'padding:2px 6px;font-size:12px;background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:4px;cursor:pointer;';
    copyBtn.addEventListener('click', (e) => {
      e.preventDefault();
      const contentEl = msg.querySelector('.msg-content');
      const text = contentEl ? contentEl.textContent : '';
      if (text) {
        navigator.clipboard.writeText(text).then(() => {
          copyBtn.textContent = '✅';
          setTimeout(() => { copyBtn.textContent = '📋'; }, 2000);
        }).catch(() => {});
      }
    });
    actionBar.appendChild(copyBtn);

    msg.addEventListener('mouseenter', () => { actionBar.style.opacity = '1'; });
    msg.addEventListener('mouseleave', () => { actionBar.style.opacity = '0'; });
    msg.appendChild(actionBar);
  }
}

// ── 导出功能 ──

async function nativeSaveFile(defaultName: string, content: string): Promise<void> {
  const bridge = window.metonaDesktop;
  if (bridge) {
    const ext = defaultName.split('.').pop() || 'txt';
    const filePath = await bridge.dialog.saveFile({
      defaultPath: defaultName,
      filters: [{ name: ext.toUpperCase(), extensions: [ext] }]
    });
    if (!filePath) return;
    const result = await bridge.fs.writeFile(filePath, content, 'utf-8');
    if (!result.success) {
      logError('导出失败', result.error);
    }
  } else {
    // 回退
    const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
    const a = document.createElement('a');
    a.href = URL.createObjectURL(blob);
    a.download = defaultName;
    a.click();
  }
}

export async function exportAsMarkdown(session: ChatSession): Promise<void> {
  let md = `# ${session.title}\n\n**模型:** ${session.model}  \n**时间:** ${formatTime(session.createdAt)}\n\n---\n\n`;
  session.messages.forEach(m => {
    const role = m.role === 'user' ? '👤 用户' : '🤖 AI';
    md += `### ${role}\n\n${m.content || ''}\n\n`;
    if (m.files && m.files.length > 0) {
      md += m.files.map(f => `📎 \`${f.name}\``).join(' · ') + '\n\n';
    }
    if (m.think) md += `> **思考:** ${m.think}\n\n`;
  });
  await nativeSaveFile(`${session.title}.md`, md);
  logSession('导出', `${session.title}.md`);
}

export async function exportAsHtml(session: ChatSession): Promise<void> {
  let html = `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>${escapeHtml(session.title)}</title>
<style>body{max-width:800px;margin:0 auto;padding:20px;font-family:system-ui;line-height:1.6;background:#FAF7F2;color:#2D2016;}
.user{background:#F5F0E8;padding:12px;border-radius:12px;margin:8px 0;border:1px solid rgba(0,0,0,0.06);}
.assistant{background:#fff;padding:12px;border-radius:12px;margin:8px 0;border:1px solid rgba(0,0,0,0.06);box-shadow:0 1px 4px rgba(45,32,22,0.04);}
pre{background:#2D2016;color:#F5F0E8;padding:14px;border-radius:12px;overflow-x:auto;}
code{background:#F5F0E8;color:#E8734A;padding:2px 6px;border-radius:4px;}</style></head><body>
<h1>${escapeHtml(session.title)}</h1><p>${formatTime(session.createdAt)} · ${session.model}</p><hr>`;
  session.messages.forEach(m => {
    html += `<div class="${m.role}"><strong>${m.role === 'user' ? '👤 用户' : '🤖 AI'}</strong><br>${(m.content || '').replace(/\n/g, '<br>')}</div>`;
  });
  html += '</body></html>';
  await nativeSaveFile(`${session.title}.html`, html);
  logSession('导出', `${session.title}.html`);
}

export async function exportAsTxt(session: ChatSession): Promise<void> {
  let txt = `${session.title}\n模型: ${session.model}\n时间: ${formatTime(session.createdAt)}\n${'='.repeat(50)}\n\n`;
  session.messages.forEach(m => {
    txt += `[${m.role === 'user' ? '用户' : 'AI'}]\n${m.content || ''}\n\n`;
  });
  await nativeSaveFile(`${session.title}.txt`, txt);
  logSession('导出', `${session.title}.txt`);
}
