import { useState, useEffect, useRef, useCallback, useMemo, type MutableRefObject, type RefObject, type CSSProperties, createElement } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { List, type ListImperativeAPI } from 'react-window';
import { useChatStore, type Message } from '../store/chatStore';
import { useConversationStore } from '../store/chat/conversationStore';
import { useArtifactStore } from '../store/chat/artifactStore';
import { useConnectionStore } from '../store/connectionStore';
import { useAgentStore } from '../store/agentStore';
import { useConfigStore } from '../store/configStore';
import { useSaaSStore } from '../store/saasStore';
import { type UnlistenFn } from '@tauri-apps/api/event';
import { safeListenEvent } from '../lib/safe-tauri';
import { Paperclip, SquarePen, ArrowUp, MessageSquare, Download, X, FileText, Image as ImageIcon, Search } from 'lucide-react';
import { Button, EmptyState, MessageListSkeleton, LoadingDots } from './ui';
import { ResizableChatLayout } from './ai/ResizableChatLayout';
import { ArtifactPanel } from './ai/ArtifactPanel';
import { ToolCallChain, type ToolCallStep } from './ai/ToolCallChain';
import { TaskProgress, type Subtask } from './ai/TaskProgress';
import { listItemVariants, defaultTransition, fadeInVariants } from '../lib/animations';
import { FirstConversationPrompt } from './FirstConversationPrompt';
import { ClassroomPlayer } from './classroom_player';
import { useClassroomStore } from '../store/classroomStore';
import { MessageSearch } from './MessageSearch';
import { OfflineIndicator } from './OfflineIndicator';
import {
  useVirtualizedMessages,
  type VirtualizedMessageItem
} from '../lib/message-virtualization';
import { Conversation } from './ai/Conversation';
import { ReasoningBlock } from './ai/ReasoningBlock';
import { StreamingText } from './ai/StreamingText';
import { ChatMode } from './ai/ChatMode';
import { ModelSelector } from './ai/ModelSelector';
import { SuggestionChips } from './ai/SuggestionChips';
import { PipelineResultPreview } from './pipeline/PipelineResultPreview';
import { PresentationContainer } from './presentation/PresentationContainer';
// TokenMeter temporarily unused — using inline text counter instead

// Default heights for virtualized messages
const DEFAULT_MESSAGE_HEIGHTS: Record<string, number> = {
  user: 80,
  assistant: 150,
  tool: 120,
  hand: 120,
  workflow: 100,
  system: 60,
};

// Threshold for enabling virtualization (messages count)
const VIRTUALIZATION_THRESHOLD = 100;

export function ChatArea({ compact }: { compact?: boolean }) {
  const {
    messages, isStreaming, isLoading,
    sendMessage: sendToGateway, initStreamListener,
    newConversation, chatMode, setChatMode, suggestions,
    totalInputTokens, totalOutputTokens,
  } = useChatStore();
  const currentAgent = useConversationStore((s) => s.currentAgent);
  const currentModel = useConversationStore((s) => s.currentModel);
  const setCurrentModel = useConversationStore((s) => s.setCurrentModel);
  const {
    artifacts, selectedArtifactId, artifactPanelOpen,
    selectArtifact, setArtifactPanelOpen,
  } = useArtifactStore();
  const connectionState = useConnectionStore((s) => s.connectionState);
  const { activeClassroom, classroomOpen, closeClassroom, generating, progressPercent, progressActivity, error: classroomError, clearError: clearClassroomError } = useClassroomStore();
  const clones = useAgentStore((s) => s.clones);
  const configModels = useConfigStore((s) => s.models);
  const saasModels = useSaaSStore((s) => s.availableModels);
  const isLoggedIn = useSaaSStore((s) => s.isLoggedIn);

  // Merge models: SaaS available models take priority when logged in
  const models = useMemo(() => {
    if (isLoggedIn && saasModels.length > 0) {
      return saasModels.map(m => ({
        id: m.alias || m.id,
        name: m.alias || m.id,
        provider: m.provider_id,
      }));
    }
    if (configModels.length > 0) {
      return configModels;
    }
    // Fallback: provide common models when no backend is connected
    return [
      { id: 'glm-4-flash', name: 'GLM-4 Flash', provider: 'zhipu' },
      { id: 'gpt-4o', name: 'GPT-4o', provider: 'openai' },
      { id: 'gpt-4o-mini', name: 'GPT-4o Mini', provider: 'openai' },
      { id: 'deepseek-chat', name: 'DeepSeek V3', provider: 'deepseek' },
      { id: 'qwen-max', name: 'Qwen Max', provider: 'qwen' },
      { id: 'claude-3-5-sonnet', name: 'Claude 3.5 Sonnet', provider: 'anthropic' },
    ];
  }, [isLoggedIn, saasModels, configModels]);

  const [input, setInput] = useState('');
  const [pendingFiles, setPendingFiles] = useState<File[]>([]);
  const [searchOpen, setSearchOpen] = useState(false);
  const scrollRef = useRef<HTMLDivElement>(null);
  const textareaRef = useRef<HTMLTextAreaElement>(null);
  const messageRefs = useRef<Map<string, HTMLDivElement>>(new Map());
  const fileInputRef = useRef<HTMLInputElement>(null);

  // Convert messages to virtualization format
  const virtualizedMessages: VirtualizedMessageItem[] = useMemo(
    () => messages.map((msg) => ({
      id: msg.id,
      height: DEFAULT_MESSAGE_HEIGHTS[msg.role] ?? 100,
      role: msg.role,
    })),
    [messages]
  );

  // Use virtualization hook
  const {
    listRef,
    getHeight,
    setHeight,
    scrollToBottom,
  } = useVirtualizedMessages(virtualizedMessages, DEFAULT_MESSAGE_HEIGHTS);

  // Whether to use virtualization
  const useVirtualization = messages.length >= VIRTUALIZATION_THRESHOLD;

  // Get current clone for first conversation prompt
  const currentClone = useMemo(() => {
    if (!currentAgent) return null;
    return clones.find((c) => c.id === currentAgent.id) || null;
  }, [currentAgent, clones]);

  // Check if should show first conversation prompt
  const showFirstPrompt = messages.length === 0 && currentClone && !currentClone.onboardingCompleted;

  // Handle suggestion click from first conversation prompt
  const handleSelectSuggestion = (text: string) => {
    setInput(text);
    textareaRef.current?.focus();
  };

  // Auto-resize textarea
  const adjustTextarea = useCallback(() => {
    const el = textareaRef.current;
    if (el) {
      el.style.height = 'auto';
      el.style.height = Math.min(el.scrollHeight, 160) + 'px';
    }
  }, []);

  // File handling
  const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
  const MAX_FILES = 5;

  const addFiles = useCallback((files: FileList | File[]) => {
    const incoming = Array.from(files).filter((f) => f.size <= MAX_FILE_SIZE);
    setPendingFiles((prev) => {
      const combined = [...prev, ...incoming];
      return combined.slice(0, MAX_FILES);
    });
  }, []);

  // Paste handler for images/files
  useEffect(() => {
    const handler = (e: ClipboardEvent) => {
      if (e.clipboardData?.files.length) {
        e.preventDefault();
        addFiles(e.clipboardData.files);
      }
    };
    document.addEventListener('paste', handler);
    return () => document.removeEventListener('paste', handler);
  }, [addFiles]);

  const handleDrop = useCallback((e: React.DragEvent) => {
    e.preventDefault();
    if (e.dataTransfer.files.length) {
      addFiles(e.dataTransfer.files);
    }
  }, [addFiles]);

  const handleDragOver = useCallback((e: React.DragEvent) => {
    e.preventDefault();
  }, []);

  // Init agent stream listener on mount
  useEffect(() => {
    const unsub = initStreamListener();
    return unsub;
  }, []);

  // Listen for hand-execution-complete Tauri events
  useEffect(() => {
    let unlisten: UnlistenFn | undefined;
    safeListenEvent<{ approvalId: string; handId: string; success: boolean; error?: string | null }>(
      'hand-execution-complete',
      (event) => {
        const { handId, success, error } = event.payload;
        useChatStore.getState().addMessage({
          id: crypto.randomUUID(),
          role: 'hand',
          content: success
            ? `Hand ${handId} 执行完成`
            : `Hand ${handId} 执行失败: ${error || '未知错误'}`,
          timestamp: new Date(),
          handName: handId,
          handStatus: success ? 'completed' : 'failed',
          handResult: event.payload,
        });
      },
    ).then((fn) => { unlisten = fn; });
    return () => { unlisten?.(); };
  }, []);

  // Auto-scroll to bottom on new messages
  useEffect(() => {
    if (scrollRef.current && !useVirtualization) {
      scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
    } else if (useVirtualization && messages.length > 0) {
      scrollToBottom();
    }
  }, [messages, useVirtualization, scrollToBottom]);

  const handleSend = () => {
    if ((!input.trim() && pendingFiles.length === 0) || isStreaming) return;
    // Attach file names as metadata in the message
    const fileContext = pendingFiles.length > 0
      ? `\n\n[附件: ${pendingFiles.map((f) => f.name).join(', ')}]`
      : '';
    sendToGateway(input + fileContext);
    setInput('');
    setPendingFiles([]);
  };

  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault();
      handleSend();
    }
  };

  const connected = connectionState === 'connected';

  // Export current conversation as Markdown
  const exportCurrentConversation = () => {
    const title = currentAgent?.name || 'ZCLAW 对话';
    const lines = [`# ${title}`, '', `导出时间: ${new Date().toLocaleString('zh-CN')}`, ''];
    for (const msg of messages) {
      const label = msg.role === 'user' ? '用户' : msg.role === 'assistant' ? '助手' : msg.role;
      lines.push(`## ${label}`, '', msg.content, '');
    }
    const blob = new Blob([lines.join('\n')], { type: 'text/markdown;charset=utf-8' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `${title.replace(/[/\\?%*:|"<>]/g, '_')}.md`;
    a.click();
    URL.revokeObjectURL(url);
  };

  // Build artifact panel content
  const artifactRightPanel = (
    <ArtifactPanel
      artifacts={artifacts}
      selectedId={selectedArtifactId}
      onSelect={selectArtifact}
    />
  );

  return (
    <div className="relative flex-1 min-h-0">
    {/* Generation progress overlay */}
    <AnimatePresence>
      {generating && (
        <motion.div
          key="generation-overlay"
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
          className="absolute inset-0 z-40 bg-white/80 dark:bg-gray-900/80 backdrop-blur-sm flex items-center justify-center"
        >
          <div className="text-center space-y-4">
            <div className="w-12 h-12 border-4 border-indigo-200 border-t-indigo-500 rounded-full animate-spin mx-auto" />
            <div>
              <p className="text-lg font-medium text-gray-900 dark:text-white">
                正在生成课堂...
              </p>
              <p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
                {progressActivity || '准备中...'}
              </p>
            </div>
            {progressPercent > 0 && (
              <div className="w-64 mx-auto">
                <div className="h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
                  <div
                    className="h-full bg-indigo-500 rounded-full transition-all duration-500"
                    style={{ width: `${progressPercent}%` }}
                  />
                </div>
                <p className="text-xs text-gray-400 mt-1">{progressPercent}%</p>
              </div>
            )}
            <button
              onClick={() => useClassroomStore.getState().cancelGeneration()}
              className="px-4 py-2 text-sm text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg"
            >
              取消
            </button>
          </div>
        </motion.div>
      )}
    </AnimatePresence>

    {/* ClassroomPlayer overlay */}
    <AnimatePresence>
      {classroomOpen && activeClassroom && (
        <motion.div
          key="classroom-overlay"
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
          className="absolute inset-0 z-50 bg-white dark:bg-gray-900"
        >
          <ClassroomPlayer
            onClose={closeClassroom}
          />
        </motion.div>
      )}
    </AnimatePresence>

    <ResizableChatLayout
      chatPanel={
        <div className="flex flex-col h-full">
      {/* Classroom generation error banner */}
      {classroomError && (
        <div className="mx-4 mt-2 px-4 py-2 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg flex items-center justify-between text-sm">
          <span className="text-red-600 dark:text-red-400">课堂生成失败: {classroomError}</span>
          <button onClick={clearClassroomError} className="text-red-400 hover:text-red-600 ml-3 text-xs">关闭</button>
        </div>
      )}
      {/* Header — DeerFlow-style: minimal */}
      <div className="h-14 border-b border-transparent flex items-center justify-between px-6 flex-shrink-0 bg-white dark:bg-gray-900">
        <div className="flex items-center gap-2 text-sm text-gray-500">
          <span>{currentAgent?.name || '新对话'}</span>
        </div>
        <div className="flex items-center gap-4">
          {/* Token usage counter — DeerFlow-style plain text */}
          {!compact && (totalInputTokens + totalOutputTokens) > 0 && (() => {
            const total = totalInputTokens + totalOutputTokens;
            const display = total >= 1000 ? `${(total / 1000).toFixed(1)}K` : String(total);
            return (
              <span className="text-sm text-gray-500 flex items-center gap-1.5">
                {display}
              </span>
            );
          })()}
          <OfflineIndicator compact />
          {!compact && messages.length > 0 && (
            <Button
              variant="ghost"
              size="sm"
              onClick={() => setSearchOpen((prev) => !prev)}
              className={`flex items-center gap-1 rounded-lg transition-colors ${searchOpen ? 'text-orange-600 dark:text-orange-400 bg-orange-50 dark:bg-orange-900/20' : 'text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800'}`}
              title="搜索消息"
            >
              <Search className="w-3.5 h-3.5" />
            </Button>
          )}
          {messages.length > 0 && (
            <Button
              variant="ghost"
              size="sm"
              onClick={exportCurrentConversation}
              className="flex items-center gap-2 text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-lg transition-colors"
              title="导出对话"
            >
              <Download className="w-3.5 h-3.5" />
              <span className="text-sm">导出</span>
            </Button>
          )}
          {messages.length > 0 && (
            <Button
              variant="ghost"
              size="sm"
              onClick={newConversation}
              className="flex items-center gap-2 text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-lg transition-colors"
              title="新对话"
            >
              <SquarePen className="w-3.5 h-3.5" />
              新对话
            </Button>
          )}
        </div>
      </div>

      {/* MessageSearch panel */}
      <AnimatePresence>
        {searchOpen && messages.length > 0 && (
          <motion.div
            key="message-search"
            initial={{ opacity: 0, height: 0 }}
            animate={{ opacity: 1, height: 'auto' }}
            exit={{ opacity: 0, height: 0 }}
            transition={{ duration: 0.2 }}
            className="border-b border-gray-100 dark:border-gray-800 bg-gray-50 dark:bg-gray-800/50 overflow-hidden"
          >
            <div className="px-6 py-3 max-w-4xl mx-auto">
              <MessageSearch onNavigateToMessage={(id) => {
                const el = messageRefs.current.get(id);
                if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
              }} />
            </div>
          </motion.div>
        )}
      </AnimatePresence>

      {/* Messages */}
      <Conversation className="flex-1 bg-white dark:bg-gray-900">
        <AnimatePresence mode="popLayout">
          {/* Loading skeleton */}
          {isLoading && messages.length === 0 && (
            <motion.div
              key="loading-skeleton"
              variants={fadeInVariants}
              initial="initial"
              animate="animate"
              exit="exit"
            >
              <MessageListSkeleton count={3} />
            </motion.div>
          )}

          {/* Empty state */}
          {!isLoading && messages.length === 0 && (
            <motion.div
              key="empty-state"
              variants={fadeInVariants}
              initial="initial"
              animate="animate"
              exit="exit"
            >
              {showFirstPrompt && currentClone ? (
                <FirstConversationPrompt
                  clone={currentClone}
                  onSelectSuggestion={handleSelectSuggestion}
                />
              ) : (
                <EmptyState
                  icon={<MessageSquare className="w-8 h-8" />}
                  title="欢迎使用 ZCLAW"
                  description={connected ? '发送消息开始对话。' : '请先在设置中连接 Gateway。'}
                />
              )}
            </motion.div>
          )}

          {/* Virtualized list for large message counts, smooth scroll for small counts */}
          {useVirtualization && messages.length > 0 ? (
            <VirtualizedMessageList
              messages={messages}
              listRef={listRef}
              getHeight={getHeight}
              onHeightChange={setHeight}
              messageRefs={messageRefs}
              setInput={setInput}
            />
          ) : (
            messages.map((message) => (
              <motion.div
                key={message.id}
                ref={(el) => { if (el) messageRefs.current.set(message.id, el); }}
                variants={listItemVariants}
                initial="hidden"
                animate="visible"
                layout
                transition={defaultTransition}
              >
                <MessageBubble message={message} setInput={setInput} />
              </motion.div>
            ))
          )}
        </AnimatePresence>
      </Conversation>

      {/* Input */}
      <div className="flex-shrink-0 p-4 bg-white dark:bg-gray-900">
        <div className="max-w-4xl mx-auto">
          {/* Suggestion chips */}
          {!isStreaming && suggestions.length > 0 && !messages.some(m => m.error) && (
            <SuggestionChips
              suggestions={suggestions}
              onSelect={(text) => { setInput(text); textareaRef.current?.focus(); }}
              className="mb-3"
            />
          )}
          {/* Hidden file input */}
          <input
            ref={fileInputRef}
            type="file"
            multiple
            className="hidden"
            onChange={(e) => { if (e.target.files) addFiles(e.target.files); e.target.value = ''; }}
          />
          {/* Pending file previews */}
          {pendingFiles.length > 0 && (
            <div className="flex flex-wrap gap-2 mb-2">
              {pendingFiles.map((file, idx) => (
                <div
                  key={`${file.name}-${idx}`}
                  className="flex items-center gap-2 px-3 py-1.5 bg-gray-100 dark:bg-gray-700 rounded-lg text-xs text-gray-700 dark:text-gray-300 max-w-[200px]"
                >
                  {file.type.startsWith('image/') ? (
                    <ImageIcon className="w-3.5 h-3.5 flex-shrink-0 text-orange-500" />
                  ) : (
                    <FileText className="w-3.5 h-3.5 flex-shrink-0 text-gray-500" />
                  )}
                  <span className="truncate">{file.name}</span>
                  <span className="text-gray-400 flex-shrink-0">({(file.size / 1024).toFixed(0)}K)</span>
                  <button
                    onClick={() => setPendingFiles((prev) => prev.filter((_, i) => i !== idx))}
                    className="p-0.5 text-gray-400 hover:text-red-500 flex-shrink-0"
                  >
                    <X className="w-3 h-3" />
                  </button>
                </div>
              ))}
            </div>
          )}
          {/* Input card — DeerFlow-style: white card, textarea top, actions bottom */}
          <div
            className="bg-white dark:bg-gray-800 rounded-2xl shadow-sm transition-all"
            onDrop={handleDrop}
            onDragOver={handleDragOver}
          >
            {/* Textarea area */}
            <div className="px-4 pt-4 pb-1">
              <textarea
                ref={textareaRef}
                value={input}
                onChange={(e) => { setInput(e.target.value); adjustTextarea(); }}
                onKeyDown={handleKeyDown}
                placeholder={
                  isStreaming
                    ? 'Agent 正在回复...'
                    : '今天我能为你做些什么?'
                }
                disabled={isStreaming}
                rows={2}
                className="w-full bg-transparent border-none outline-none ring-0 focus:outline-none focus:ring-0 text-gray-700 dark:text-gray-200 placeholder-gray-400 dark:placeholder-gray-500 disabled:opacity-50 resize-none leading-relaxed"
                style={{ minHeight: '48px', maxHeight: '160px', border: 'none', outline: 'none', boxShadow: 'none' }}
              />
            </div>

            {/* Bottom action bar */}
            <div className="flex items-center justify-between px-3 pb-3">
              <div className="flex items-center gap-1">
                <Button
                  variant="ghost"
                  size="sm"
                  className="p-2 text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
                  aria-label="添加附件"
                  onClick={() => fileInputRef.current?.click()}
                >
                  <Paperclip className="w-5 h-5" />
                </Button>
                {!compact && <ChatMode
                  value={chatMode}
                  onChange={setChatMode}
                  disabled={isStreaming}
                />
                }
              </div>
              <div className="flex items-center gap-2">
                <ModelSelector
                  models={models.map(m => ({ id: m.id, name: m.name, provider: m.provider }))}
                  currentModel={currentModel}
                  onSelect={setCurrentModel}
                  disabled={isStreaming}
                />
                <Button
                  variant="primary"
                  size="sm"
                  onClick={handleSend}
                  disabled={isStreaming || (!input.trim() && pendingFiles.length === 0)}
                  className="w-8 h-8 rounded-full p-0 flex items-center justify-center bg-orange-500 hover:bg-orange-600 text-white disabled:opacity-50"
                  aria-label="发送消息"
                >
                  <ArrowUp className="w-4 h-4 text-white" />
                </Button>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
      }
      rightPanel={artifactRightPanel}
      rightPanelTitle="产物"
      rightPanelOpen={artifactPanelOpen}
      onRightPanelToggle={setArtifactPanelOpen}
    />
    </div>
  );
}

function MessageBubble({ message, setInput }: { message: Message; setInput: (text: string) => void }) {
  if (message.role === 'tool') {
    return null;
  }

  const isUser = message.role === 'user';
  const isThinking = message.streaming && !message.content;

  // Extract typed arrays for JSX rendering
  const toolCallSteps: ToolCallStep[] | undefined = message.toolSteps;
  const subtaskList: Subtask[] | undefined = message.subtasks;

  // framer-motion 12 + React 19 type compat: motion.span/div return types resolve to `unknown`.
  // Use createElement to bypass JSX child-context type inference.
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const renderToolChain = (): any => {
    if (isUser || !toolCallSteps || toolCallSteps.length === 0) return null;
    return createElement(ToolCallChain, { steps: toolCallSteps, isStreaming: !!message.streaming });
  };
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const renderSubtasks = (): any => {
    if (isUser || !subtaskList || subtaskList.length === 0) return null;
    return createElement(TaskProgress, { tasks: subtaskList, className: 'mb-3' });
  };

  // Download message as Markdown file
  const handleDownloadMessage = () => {
    if (!message.content) return;
    const timestamp = new Date().toISOString().slice(0, 10);
    const filename = `message-${timestamp}.md`;
    const blob = new Blob([message.content], { type: 'text/markdown;charset=utf-8' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = filename;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
  };

  return (
    <div className={`flex gap-4 ${isUser ? 'justify-end' : ''}`}>
      <div
        className={`w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 ${isUser ? 'bg-gray-200 dark:bg-gray-600 text-gray-600 dark:text-gray-200 order-last' : 'agent-avatar text-white'}`}
      >
        {isUser ? '用' : 'Z'}
      </div>
      <div className={isUser ? 'max-w-2xl' : 'flex-1 max-w-3xl'}>
        {isThinking ? (
          // Thinking indicator
          <div className="flex items-center gap-2 px-4 py-3 text-gray-500 dark:text-gray-400">
            <LoadingDots />
            <span className="text-sm">Thinking...</span>
          </div>
        ) : (
          <div className={`p-4 shadow-sm ${isUser ? 'chat-bubble-user shadow-md' : 'chat-bubble-assistant'} relative group`}>
            {/* Optimistic sending indicator */}
            {isUser && message.optimistic && (
              <span className="text-xs text-blue-200 dark:text-blue-300 mb-1 block animate-pulse">
                Sending...
              </span>
            )}
            {/* Reasoning block for thinking content (DeerFlow-inspired) */}
            {!isUser && message.thinkingContent && (
              <ReasoningBlock
                content={message.thinkingContent}
                isStreaming={message.streaming}
              />
            )}
            {/* Tool call steps chain (DeerFlow-inspired) */}
            {renderToolChain()}
            {/* Subtask tracking (DeerFlow-inspired) */}
            {renderSubtasks()}
            {/* Message content with streaming support */}
            <div className={`leading-relaxed ${isUser ? 'text-white whitespace-pre-wrap' : 'text-gray-700 dark:text-gray-200'}`}>
              {message.content
                ? (isUser
                    ? message.content
                    : <StreamingText
                        content={message.content}
                        isStreaming={!!message.streaming}
                        className="text-gray-700 dark:text-gray-200"
                      />
                  )
                : '...'}
            </div>
            {/* Pipeline / Hand result presentation */}
            {!isUser && (message.role === 'workflow' || message.role === 'hand') && message.workflowResult && typeof message.workflowResult === 'object' && message.workflowResult !== null && (
              <div className="mt-3">
                <PipelineResultPreview
                  outputs={message.workflowResult as Record<string, unknown>}
                  pipelineId={message.workflowId}
                />
              </div>
            )}
            {!isUser && message.role === 'hand' && message.handResult && typeof message.handResult === 'object' && message.handResult !== null && !message.workflowResult && (
              <div className="mt-3">
                <PresentationContainer data={message.handResult} />
              </div>
            )}
            {message.error && (
              <div className="flex items-center gap-2 mt-2">
                <p className="text-xs text-red-500">{message.error}</p>
                <button
                  onClick={() => {
                    const text = typeof message.content === 'string' ? message.content : '';
                    if (text) {
                      setInput(text);
                    }
                  }}
                  className="text-xs px-2 py-0.5 rounded bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400 hover:bg-red-200 dark:hover:bg-red-900/50 transition-colors"
                >
                  重试
                </button>
              </div>
            )}
            {/* Download button for AI messages - show on hover */}
            {!isUser && message.content && !message.streaming && (
              <button
                onClick={handleDownloadMessage}
                className="absolute top-2 right-2 p-1.5 bg-gray-200/80 dark:bg-gray-700/80 hover:bg-gray-300 dark:hover:bg-gray-600 rounded text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 transition-colors opacity-0 group-hover:opacity-100"
                title="下载为 Markdown"
              >
                <Download className="w-3.5 h-3.5" />
              </button>
            )}
          </div>
        )}
      </div>
    </div>
  );
}

// === Virtualized Message Components ===

interface VirtualizedMessageRowProps {
  message: Message;
  onHeightChange: (height: number) => void;
  messageRefs: MutableRefObject<Map<string, HTMLDivElement>>;
  setInput: (text: string) => void;
}

/**
 * Single row in the virtualized list.
 * Measures actual height after render and reports back.
 */
function VirtualizedMessageRow({
  message,
  onHeightChange,
  messageRefs,
  setInput,
  style,
  ariaAttributes,
}: VirtualizedMessageRowProps & {
  style: CSSProperties;
  ariaAttributes: {
    'aria-posinset': number;
    'aria-setsize': number;
    role: 'listitem';
  };
}) {
  const rowRef = useRef<HTMLDivElement>(null);

  // Measure height after mount
  useEffect(() => {
    if (rowRef.current) {
      const height = rowRef.current.getBoundingClientRect().height;
      if (height > 0) {
        onHeightChange(height);
      }
    }
  }, [message.content, message.streaming, onHeightChange]);

  return (
    <div
      ref={(el) => {
        if (el) {
          (rowRef as MutableRefObject<HTMLDivElement | null>).current = el;
          messageRefs.current.set(message.id, el);
        }
      }}
      style={style}
      className="py-3"
      {...ariaAttributes}
    >
      <MessageBubble message={message} setInput={setInput} />
    </div>
  );
}

interface VirtualizedMessageListProps {
  messages: Message[];
  listRef: RefObject<ListImperativeAPI | null>;
  getHeight: (id: string, role: string) => number;
  onHeightChange: (id: string, height: number) => void;
  messageRefs: MutableRefObject<Map<string, HTMLDivElement>>;
  setInput: (text: string) => void;
}

/**
 * Virtualized message list for efficient rendering of large message counts.
 * Uses react-window's List with dynamic height measurement.
 */
function VirtualizedMessageList({
  messages,
  listRef,
  getHeight,
  onHeightChange,
  messageRefs,
  setInput,
}: VirtualizedMessageListProps) {
  // Row component for react-window v2
  const RowComponent = (props: {
    ariaAttributes: {
      'aria-posinset': number;
      'aria-setsize': number;
      role: 'listitem';
    };
    index: number;
    style: CSSProperties;
  }) => (
    <VirtualizedMessageRow
      message={messages[props.index]}
      onHeightChange={(h) => onHeightChange(messages[props.index].id, h)}
      messageRefs={messageRefs}
      setInput={setInput}
      style={props.style}
      ariaAttributes={props.ariaAttributes}
    />
  );

  return (
    <List
      listRef={listRef}
      rowComponent={RowComponent}
      rowProps={{}}
      rowHeight={(index: number) => getHeight(messages[index].id, messages[index].role)}
      rowCount={messages.length}
      defaultHeight={500}
      overscanCount={5}
      className="focus:outline-none"
    />
  );
}
