import { useQuery } from '@tanstack/react-query';
import { useState, useEffect, useRef, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
import MarkdownRenderer from '../components/MarkdownRenderer';
import AgentBayLivePanel, { LivePreviewState } from '../components/AgentBayLivePanel';
import ModelSwitcher from '../components/ModelSwitcher';
import { agentApi, enterpriseApi, tenantApi, uploadFileWithProgress } from '../services/api';
import { IconPaperclip, IconSend } from '@tabler/icons-react';
import { formatFileSize } from '../utils/formatFileSize';
import { useAuthStore } from '../stores';
import { useDropZone } from '../hooks/useDropZone';
import { useToast } from '../components/Toast/ToastProvider';

/* ── Inline SVG Icons ── */
const Icons = {
    bot: (
        <svg width="18" height="18" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" strokeLinejoin="round">
            <rect x="3" y="5" width="12" height="10" rx="2" />
            <circle cx="7" cy="10" r="1" fill="currentColor" stroke="none" />
            <circle cx="11" cy="10" r="1" fill="currentColor" stroke="none" />
            <path d="M9 2v3M6 2h6" />
        </svg>
    ),
    user: (
        <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
            <circle cx="8" cy="5.5" r="2.5" />
            <path d="M3 14v-1a4 4 0 018 0v1" />
        </svg>
    ),
    chat: (
        <svg width="28" height="28" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round">
            <path d="M2 3a1 1 0 011-1h10a1 1 0 011 1v7a1 1 0 01-1 1H5l-3 3V3z" />
            <path d="M5 5.5h6M5 8h4" />
        </svg>
    ),
    tool: (
        <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
            <path d="M10.5 10.5L14 14M4.5 2a2.5 2.5 0 00-1.8 4.2l5.1 5.1A2.5 2.5 0 1012 7.2L6.8 2.2A2.5 2.5 0 004.5 2z" />
        </svg>
    ),
};

interface ToolCall {
    name: string;
    args: any;
    result?: string;
}

interface Message {
    role: 'user' | 'assistant';
    content: string;
    fileName?: string;
    toolCalls?: ToolCall[];
    thinking?: string;
    imageUrl?: string;
    timestamp?: string;
    _isToolGroup?: boolean;
}

// CSS keyframe for the pulse/breathing LED — injected once into <head>
const PULSE_STYLE_ID = 'cw-tool-pulse-style';
if (typeof document !== 'undefined' && !document.getElementById(PULSE_STYLE_ID)) {
    const s = document.createElement('style');
    s.id = PULSE_STYLE_ID;
    s.textContent = `
        @keyframes cw-pulse-led {
            0%, 100% { opacity: 1; transform: scale(1); box-shadow: 0 0 0 0 rgba(107,114,128,0.45); }
            50%       { opacity: 0.55; transform: scale(1.5); box-shadow: 0 0 0 4px rgba(107,114,128,0); }
        }
        .cw-running-led { animation: cw-pulse-led 1.4s ease-in-out infinite; }
    `;
    document.head.appendChild(s);
}

function ChatToolChain({ toolCalls }: { toolCalls: ToolCall[] }) {
    const { t } = useTranslation();
    const [expanded, setExpanded] = useState(false);
    const count = toolCalls.length;

    // Find the last tool without a result — that is the currently-executing one.
    const activeIdx = (() => {
        for (let i = toolCalls.length - 1; i >= 0; i--) {
            if (!toolCalls[i].result) return i;
        }
        return -1; // -1 = all done
    })();
    const isRunning = activeIdx >= 0;
    const activeTool = isRunning ? toolCalls[activeIdx] : null;

    return (
        <div style={{
            borderRadius: '8px',
            background: isRunning ? 'color-mix(in srgb, var(--bg-secondary) 72%, var(--bg-primary))' : 'var(--bg-primary)',
            border: `1px solid ${isRunning ? 'var(--border-default)' : 'var(--border-subtle)'}`,
            fontSize: '12px',
            overflow: 'hidden',
            marginBottom: '6px',
            transition: 'border-color 0.3s ease',
        }}>
            {/* ── Header / toggle row ── */}
            <button
                onClick={() => setExpanded(v => !v)}
                style={{
                    background: 'none', border: 'none', cursor: 'pointer',
                    width: '100%', display: 'flex', alignItems: 'center', gap: '6px',
                    padding: '7px 10px',
                    color: 'var(--text-secondary)',
                }}
            >
                {/* Left label: title + running-tool indicator */}
                <span style={{ flex: 1, textAlign: 'left', display: 'flex', alignItems: 'center', gap: '6px', minWidth: 0 }}>
                    <span style={{ fontWeight: 500, flexShrink: 0, color: 'var(--text-primary)' }}>{t('agent.chat.toolCallChain')}</span>
                    <span style={{ color: 'var(--text-tertiary)', flexShrink: 0 }}>·</span>
                    {isRunning && activeTool ? (
                        <>
                            {/* Pulse LED: breathing dot while a tool runs */}
                            <span
                                className="cw-running-led"
                                style={{
                                    display: 'inline-block',
                                    width: '6px', height: '6px',
                                    borderRadius: '50%',
                                    background: 'var(--text-tertiary)',
                                    flexShrink: 0,
                                }}
                            />
                            {/* Currently-running tool name */}
                            <span style={{
                                fontFamily: 'var(--font-mono)',
                                fontSize: '11px',
                                color: 'var(--text-secondary)',
                                overflow: 'hidden',
                                textOverflow: 'ellipsis',
                                whiteSpace: 'nowrap',
                            }}>
                                {activeTool.name}
                            </span>
                        </>
                    ) : (
                        /* Static green dot when all tools are done */
                        <span style={{
                            display: 'inline-block',
                            width: '6px', height: '6px',
                            borderRadius: '50%',
                            background: '#22c55e',
                            flexShrink: 0,
                            opacity: 0.85,
                        }} />
                    )}
                </span>

                {/* Count badge */}
                <span style={{
                    background: 'var(--bg-secondary)', color: 'var(--text-secondary)',
                    borderRadius: '10px', padding: '1px 7px',
                    fontSize: '10px', fontWeight: 600, flexShrink: 0,
                }}>
                    {count}
                </span>

                {/* Expand chevron */}
                <span style={{
                    fontSize: '10px', color: 'var(--text-tertiary)',
                    transition: 'transform 0.2s', display: 'inline-block',
                    transform: expanded ? 'rotate(90deg)' : 'rotate(0deg)',
                    flexShrink: 0,
                }}></span>
            </button>

            {/* ── Collapsed: pills with individual run-state dots ── */}
            {!expanded && count > 0 && (
                <div style={{ padding: '0 10px 7px 10px', display: 'flex', flexWrap: 'wrap', gap: '4px' }}>
                    {toolCalls.map((tc, i) => {
                        const running = !tc.result;
                        return (
                            <span key={i} style={{
                                background: running ? 'var(--bg-secondary)' : 'var(--bg-primary)',
                                border: '1px solid var(--border-subtle)',
                                borderRadius: '4px', padding: '1px 6px',
                                fontSize: '10px', color: 'var(--text-secondary)',
                                fontFamily: 'var(--font-mono)',
                                display: 'inline-flex', alignItems: 'center', gap: '4px',
                            }}>
                                {running && (
                                    <span
                                        className="cw-running-led"
                                        style={{
                                            display: 'inline-block',
                                            width: '4px', height: '4px',
                                            borderRadius: '50%',
                                            background: 'var(--text-tertiary)',
                                            flexShrink: 0,
                                        }}
                                    />
                                )}
                                {tc.name}
                            </span>
                        );
                    })}
                </div>
            )}

            {/* ── Expanded: each tool's full detail row ── */}
            {expanded && (
                <div style={{ borderTop: '1px solid var(--border-subtle)' }}>
                    {toolCalls.map((tc, i) => {
                        const running = !tc.result;
                        return (
                            <div key={i} style={{
                                padding: '7px 10px',
                                borderBottom: i < toolCalls.length - 1 ? '1px solid var(--border-subtle)' : 'none',
                            }}>
                                <div style={{ display: 'flex', alignItems: 'center', gap: '5px', marginBottom: '4px' }}>
                                    {/* Status dot: amber + pulse = running; green = done */}
                                    <span
                                        className={running ? 'cw-running-led' : undefined}
                                        style={{
                                            display: 'inline-block',
                                            width: '5px', height: '5px',
                                            borderRadius: '50%',
                                            background: running ? '#f59e0b' : '#22c55e',
                                            flexShrink: 0,
                                        }}
                                    />
                                    <span style={{ fontFamily: 'var(--font-mono)', fontSize: '11px', color: 'var(--text-secondary)', fontWeight: 600 }}>
                                        {tc.name}
                                    </span>
                                    {running && (
                                        <span style={{ fontSize: '10px', color: 'var(--text-tertiary)', marginLeft: 'auto' }}>
                                            {t('common.loading')}
                                        </span>
                                    )}
                                </div>
                                {tc.args && Object.keys(tc.args).length > 0 && (
                                    <div style={{
                                        fontFamily: 'var(--font-mono)', fontSize: '10px',
                                        color: 'var(--text-tertiary)', whiteSpace: 'pre-wrap',
                                        wordBreak: 'break-all', maxHeight: '80px', overflowY: 'auto',
                                        background: 'var(--bg-secondary)', borderRadius: '4px',
                                        padding: '4px 6px', marginBottom: tc.result ? '4px' : 0,
                                    }}>
                                        {JSON.stringify(tc.args, null, 2)}
                                    </div>
                                )}
                                {tc.result && (
                                    <div style={{
                                        fontSize: '10px', color: 'var(--text-secondary)',
                                        whiteSpace: 'pre-wrap', wordBreak: 'break-all',
                                        maxHeight: '80px', overflowY: 'auto',
                                        borderTop: '1px solid var(--border-subtle)', paddingTop: '4px',
                                    }}>
                                        {tc.result.length > 500 ? tc.result.slice(0, 500) + '…' : tc.result}
                                    </div>
                                )}
                            </div>
                        );
                    })}
                </div>
            )}
        </div>
    );
}

export default function Chat() {
    const { t, i18n } = useTranslation();
    const toast = useToast();
    const { id } = useParams<{ id: string }>();
    const token = useAuthStore((s) => s.token);
    const [messages, setMessages] = useState<Message[]>([]);
    const [input, setInput] = useState('');
    const [connected, setConnected] = useState(false);
    const [uploadProgress, setUploadProgress] = useState<{
        name: string;
        percent: number;
        previewUrl?: string;
        sizeBytes: number;
    } | null>(null);
    const [streaming, setStreaming] = useState(false);
    const [isWaiting, setIsWaiting] = useState(false);
    const [attachedFile, setAttachedFile] = useState<{ name: string; text: string; path?: string; imageUrl?: string } | null>(null);
    const [liveState, setLiveState] = useState<LivePreviewState>({});
    const [livePanelVisible, setLivePanelVisible] = useState(false);
    const [wsSessionId, setWsSessionId] = useState<string>('');
    const wsRef = useRef<WebSocket | null>(null);
    const messagesEndRef = useRef<HTMLDivElement>(null);
    const messagesContainerRef = useRef<HTMLDivElement>(null);
    const isNearBottomRef = useRef(true);
    const userPinnedAwayFromBottomRef = useRef(false);
    const fileInputRef = useRef<HTMLInputElement>(null);
    // Ref to the chat textarea for direct DOM height manipulation
    const textareaRef = useRef<HTMLTextAreaElement>(null);
    const pendingToolCalls = useRef<ToolCall[]>([]);
    const streamContent = useRef('');
    const thinkingContent = useRef('');
    // Track history load + whether we've already fired the one-shot onboarding
    // trigger so the agent greets the user at most once per mount.
    const historyLoaded = useRef(false);
    const onboardingKickoffSent = useRef(false);

    const { data: agent } = useQuery({
        queryKey: ['agent', id],
        queryFn: () => agentApi.get(id!),
        enabled: !!id,
    });

    // Tenant default model — used to render the "默认" tag and as a visual
    // fallback when an agent has no explicit primary model.
    const { data: myTenant } = useQuery({
        queryKey: ['tenant', 'me'],
        queryFn: () => tenantApi.me(),
        staleTime: 5 * 60 * 1000,
        refetchOnMount: 'always',
    });

    // Chat-side selected model is a per-session override. It is sent with each
    // message over WebSocket and should not require permission to edit the agent.
    // Re-sync on session changes so new conversations start from the saved agent
    // model rather than a stale prior override.
    const [overrideModelId, setOverrideModelId] = useState<string | null>(null);
    useEffect(() => {
        if (agent?.primary_model_id && agent.primary_model_id !== overrideModelId) {
            setOverrideModelId(agent.primary_model_id);
        }
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [agent?.primary_model_id, wsSessionId]);

    const handleModelChange = useCallback(async (newModelId: string | null) => {
        setOverrideModelId(newModelId);
    }, []);

    const { data: llmModels = [], isLoading: llmModelsLoading } = useQuery({
        queryKey: ['llm-models'],
        queryFn: () => enterpriseApi.llmModels(),
        enabled: !!agent,
        refetchOnMount: 'always',
    });

    const enabledLlmModels = (llmModels as any[]).filter((m: any) => m.enabled);
    const effectiveChatModelId = overrideModelId
        || agent?.primary_model_id
        || myTenant?.default_model_id
        || enabledLlmModels[0]?.id
        || null;
    const effectiveModelReady = !!effectiveChatModelId && enabledLlmModels.some((m: any) => m.id === effectiveChatModelId);
    const supportsVision = !!effectiveChatModelId && llmModels.some(
        (m: any) => m.id === effectiveChatModelId && m.supports_vision
    );

    const parseMessage = (msg: Message): Message => {
        if (msg.role !== 'user') return msg;

        let result = { ...msg };

        // ── Step 1: strip prefix markers to extract fileName ─────────────────
        // Standard web chat format: [file:name.pdf]\ncontent
        const newFmt = result.content.match(/^\[file:([^\]]+)\]\n?/);
        if (newFmt) {
            result = { ...result, fileName: newFmt[1], content: result.content.slice(newFmt[0].length).trim() };
        } else {
            // Feishu/Slack channel format: [\u6587\u4ef6\u5df2\u4e0a\u4f20: workspace/uploads/name]
            const chanFmt = result.content.match(/^\[\u6587\u4ef6\u5df2\u4e0a\u4f20: (?:workspace\/uploads\/)?([^\]\n]+)\]/);
            if (chanFmt) {
                const raw = chanFmt[1]; const fileName = raw.split('/').pop() || raw;
                result = { ...result, fileName, content: result.content.slice(chanFmt[0].length).trim() };
            } else {
                // Old format: [File: name.pdf]\nFile location:...\nQuestion: user_msg
                const oldFmt = result.content.match(/^\[File: ([^\]]+)\]/);
                if (oldFmt) {
                    const fileName = oldFmt[1];
                    const qMatch = result.content.match(/\nQuestion: ([\s\S]+)$/);
                    result = { ...result, fileName, content: qMatch ? qMatch[1].trim() : '' };
                }
            }
        }

        // ── Step 2: strip [image_data:...] markers ───────────────────────────
        // When a history message was saved with an inline base64 image marker
        // (e.g. [image_data:data:image/jpeg;base64,xxx]), we must:
        //   a) extract the data URL and use it as imageUrl for the thumbnail
        //   b) remove the raw marker from the displayed content so base64 is
        //      never rendered as text (also prevents layout/scroll breakage)
        const imgDataPattern = /\[image_data:(data:image\/[^;]+;base64,[A-Za-z0-9+/=]+)\]/;
        const imgMatch = result.content.match(imgDataPattern);
        if (imgMatch) {
            result = {
                ...result,
                // Prefer existing imageUrl (set by the live upload flow); fall back
                // to the extracted data URL so the thumbnail shows in history.
                imageUrl: result.imageUrl || imgMatch[1],
                content: result.content
                    .replace(/\[image_data:data:image\/[^;]+;base64,[A-Za-z0-9+/=]+\]\n?/g, '')
                    .trim(),
            };
        }

        return result;
    };

    // Load chat history on mount
    useEffect(() => {
        if (!id || !token) return;
        fetch(`/api/chat/${id}/history`, {
            headers: { Authorization: `Bearer ${token}` },
        })
            .then(r => r.json())
            .then((history: any[]) => {
                if (history.length > 0) {
                    // Group consecutive tool_call entries into _isToolGroup messages
                    const processed: Message[] = [];
                    for (const h of history) {
                        if (h.role === 'tool_call') {
                            const tc: ToolCall = {
                                name: h.toolName || h.tool_name || '',
                                args: h.toolArgs || h.tool_args || {},
                                result: h.toolResult || h.tool_result || '',
                            };
                            const last = processed[processed.length - 1];
                            if (last && last._isToolGroup) {
                                // Merge into existing tool group
                                last.toolCalls = [...(last.toolCalls || []), tc];
                            } else if (last && last.role === 'assistant' && !(last.content && last.content.trim())) {
                                // Previous is empty assistant — convert to tool group
                                last._isToolGroup = true;
                                last.toolCalls = [...(last.toolCalls || []), tc];
                            } else {
                                // Start new tool group
                                processed.push({
                                    role: 'assistant', content: '', toolCalls: [tc],
                                    timestamp: h.created_at || undefined,
                                    _isToolGroup: true,
                                });
                            }
                        } else {
                            const msg = parseMessage({ role: h.role, content: h.content, fileName: h.fileName, toolCalls: h.toolCalls, thinking: h.thinking, imageUrl: h.imageUrl });
                            msg.timestamp = h.created_at || undefined;
                            processed.push(msg);
                        }
                    }
                    setMessages(processed);
                }
            })
            .catch(() => { /* ignore */ })
            .finally(() => { historyLoaded.current = true; });
    }, [id, token]);

    // Per-(user, agent) onboarding kickoff: if this viewer has never been
    // onboarded to this agent and the conversation is empty, fire a tagged
    // trigger the backend treats as "agent greets first" — no visible user
    // bubble, no DB write for the placeholder turn. One-shot per mount.
    useEffect(() => {
        if (onboardingKickoffSent.current) return;
        if (!connected || !wsRef.current) return;
        if (!agent || agent.onboarded_for_me !== false) return;
        if (!historyLoaded.current) return;
        if (llmModelsLoading || !effectiveModelReady || !effectiveChatModelId) return;
        if (messages.length > 0) return;
        onboardingKickoffSent.current = true;
        setIsWaiting(true);
        setStreaming(true);
        wsRef.current.send(JSON.stringify({
            content: '',
            kind: 'onboarding_trigger',
            model_id: effectiveChatModelId,
        }));
    }, [connected, agent, messages.length, llmModelsLoading, effectiveModelReady, effectiveChatModelId]);

    useEffect(() => {
        if (!id || !token) return;

        let cancelled = false;

        const connect = () => {
            if (cancelled) return;
            const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
            const lang = (i18n.language || 'en').toLowerCase().startsWith('zh') ? 'zh' : 'en';
            const wsUrl = `${protocol}//${window.location.host}/ws/chat/${id}?token=${token}&lang=${lang}`;
            const ws = new WebSocket(wsUrl);

            ws.onopen = () => {
                if (cancelled) {
                    ws.close();
                    return;
                }
                setConnected(true);
                wsRef.current = ws;
            };
            ws.onclose = () => {
                if (!cancelled) {
                    setConnected(false);
                    setTimeout(() => connect(), 2000);
                }
            };
            ws.onerror = () => {
                if (!cancelled) setConnected(false);
            };
            ws.onmessage = (event) => {
                const data = JSON.parse(event.data);
                if (['thinking', 'chunk', 'tool_call', 'done', 'error', 'quota_exceeded'].includes(data.type)) {
                    setIsWaiting(false);
                }
                if (['error', 'quota_exceeded'].includes(data.type)) {
                    setStreaming(false);
                }

                // Capture session_id from the 'connected' message for Take Control
                if (data.type === 'connected' && data.session_id) {
                    setWsSessionId(data.session_id);
                    return;
                }

                // ── AgentBay live preview events ──
                if (data.type === 'agentbay_live') {
                    console.log('[LivePreview] Received:', data.env, 'url:', data.screenshot_url?.substring(0, 60));
                    setLiveState(prev => {
                        const next = { ...prev };
                        if ((data.env === 'desktop' || data.env === 'browser') && data.screenshot_url) {
                            // Use URL-based approach: append cache-busting timestamp
                            const imgUrl = data.screenshot_url + '&_t=' + Date.now();
                            if (data.env === 'desktop') next.desktop = { screenshotUrl: imgUrl };
                            else next.browser = { screenshotUrl: imgUrl };
                        } else if (data.env === 'code' && data.output) {
                            // Append code output
                            const existing = prev.code?.output || '';
                            next.code = { output: existing + (existing ? '\n---\n' : '') + data.output };
                        }
                        return next;
                    });
                    // Auto-expand the live panel on first data
                    setLivePanelVisible(true);
                    return;
                }

                if (data.type === 'thinking') {
                    // Accumulate thinking content
                    thinkingContent.current += data.content;
                    setMessages(prev => {
                        const last = prev[prev.length - 1];
                        if (last && last.role === 'assistant') {
                            const updated = [...prev];
                            updated[updated.length - 1] = { ...last, thinking: thinkingContent.current };
                            return updated;
                        }
                        return [...prev, { role: 'assistant', content: '', thinking: thinkingContent.current, timestamp: new Date().toISOString() }];
                    });
                } else if (data.type === 'chunk') {
                    // Streaming text chunk — accumulate and update live preview
                    streamContent.current += data.content;
                    setMessages(prev => {
                        const last = prev[prev.length - 1];
                        if (last && last.role === 'assistant') {
                            // Update the streaming message in-place
                            const updated = [...prev];
                            updated[updated.length - 1] = { ...last, content: streamContent.current };
                            return updated;
                        }
                        return [...prev, { role: 'assistant', content: streamContent.current, timestamp: new Date().toISOString() }];
                    });
                } else if (data.type === 'tool_call') {
                    console.log('[ToolCall]', data.name, data.status);
                    if (data.status === 'running') {
                        // Tool execution started — show in-progress in tool group
                        const tc: ToolCall = { name: data.name, args: data.args || {} };
                        pendingToolCalls.current.push(tc);
                        const now = new Date().toISOString();
                        setMessages(prev => {
                            let msgs = [...prev];
                            // Remove trailing empty assistant messages (stream placeholders)
                            while (msgs.length > 0) {
                                const last = msgs[msgs.length - 1];
                                if (last.role === 'assistant' && !last._isToolGroup && !(last.content && last.content.trim())) {
                                    msgs.pop();
                                } else break;
                            }
                            // Merge into existing tool group, but stop at user messages (new turn)
                            for (let i = msgs.length - 1; i >= Math.max(0, msgs.length - 5); i--) {
                                if (msgs[i].role === 'user') break;
                                if (msgs[i]._isToolGroup) {
                                    msgs[i] = { ...msgs[i], toolCalls: [...(msgs[i].toolCalls || []), tc], timestamp: now };
                                    return msgs;
                                }
                            }
                            return [...msgs, { role: 'assistant', content: '', toolCalls: [tc], timestamp: now, _isToolGroup: true }];
                        });
                    } else if (data.status === 'done') {
                        // Tool execution finished — update result
                        streamContent.current = '';
                        thinkingContent.current = '';
                        const newCall: ToolCall = { name: data.name, args: data.args, result: data.result || '' };
                        // Update pending: replace running entry or add new
                        const idx = pendingToolCalls.current.findIndex(tc => tc.name === data.name && !tc.result);
                        if (idx >= 0) {
                            pendingToolCalls.current[idx] = newCall;
                        } else {
                            pendingToolCalls.current.push(newCall);
                        }
                        const now = new Date().toISOString();
                        setMessages(prev => {
                            let msgs = [...prev];
                            // Remove trailing empty assistant messages
                            while (msgs.length > 0) {
                                const last = msgs[msgs.length - 1];
                                if (last.role === 'assistant' && !last._isToolGroup && !(last.content && last.content.trim())) {
                                    msgs.pop();
                                } else break;
                            }
                            // Find recent tool group, but stop at user messages (new turn)
                            for (let i = msgs.length - 1; i >= Math.max(0, msgs.length - 5); i--) {
                                if (msgs[i].role === 'user') break;
                                if (msgs[i]._isToolGroup) {
                                    // Update the matching tool call with result, or add new
                                    const existing = (msgs[i].toolCalls || []).map(tc =>
                                        tc.name === data.name && !tc.result ? newCall : tc
                                    );
                                    const hasIt = existing.some(tc => tc.name === data.name && tc.result);
                                    msgs[i] = { ...msgs[i], toolCalls: hasIt ? existing : [...existing, newCall], timestamp: now };
                                    return msgs;
                                }
                            }
                            return [...msgs, { role: 'assistant', content: '', toolCalls: [newCall], timestamp: now, _isToolGroup: true }];
                        });

                        // ── AgentBay live preview (embedded in tool_call) ──
                        if (data.live_preview) {
                            const lp = data.live_preview;
                            setLiveState(prev => {
                                const next = { ...prev };
                                if ((lp.env === 'desktop' || lp.env === 'browser') && lp.screenshot_url) {
                                    const imgUrl = lp.screenshot_url + '&_t=' + Date.now();
                                    if (lp.env === 'desktop') next.desktop = { screenshotUrl: imgUrl };
                                    else next.browser = { screenshotUrl: imgUrl };
                                } else if (lp.env === 'code' && lp.output) {
                                    const existing = prev.code?.output || '';
                                    next.code = { output: existing + (existing ? '\n---\n' : '') + lp.output };
                                }
                                return next;
                            });
                            setLivePanelVisible(true);
                        }
                    }
                } else if (data.type === 'done') {
                    // Final response — replace streaming message with final + tool calls
                    const toolCalls = pendingToolCalls.current.length > 0 ? [...pendingToolCalls.current] : undefined;
                    const thinking = thinkingContent.current || undefined;
                    pendingToolCalls.current = [];
                    streamContent.current = '';
                    thinkingContent.current = '';
                    setStreaming(false);
                    setMessages(prev => {
                        const updated = [...prev];
                        // Replace the last streaming assistant message
                        if (updated.length > 0 && updated[updated.length - 1].role === 'assistant') {
                            updated[updated.length - 1] = { role: 'assistant', content: data.content, toolCalls, thinking };
                        } else {
                            updated.push({ role: 'assistant', content: data.content, toolCalls, thinking });
                        }
                        return updated;
                    });
                } else {
                    // Legacy format: {role, content}
                    setMessages(prev => [...prev, { role: data.role, content: data.content }]);
                }
            };
        };

        connect();

        return () => {
            cancelled = true;
            if (wsRef.current) {
                wsRef.current.close();
                wsRef.current = null;
            }
        };
    }, [id, token]);

    // Auto-focus input when connection is established
    useEffect(() => {
        if (connected) {
            setTimeout(() => textareaRef.current?.focus(), 50);
        }
    }, [connected]);

    const handleMessagesScroll = () => {
        const el = messagesContainerRef.current;
        if (!el) return;
        const distFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
        isNearBottomRef.current = distFromBottom < 120;
        userPinnedAwayFromBottomRef.current = distFromBottom > 220;
    };

    const handleMessagesWheelCapture = (event: React.WheelEvent<HTMLDivElement>) => {
        const el = messagesContainerRef.current;
        if (!el) return;
        if (event.deltaY < 0 && el.scrollTop > 0) {
            userPinnedAwayFromBottomRef.current = true;
            isNearBottomRef.current = false;
        }
    };

    useEffect(() => {
        if (userPinnedAwayFromBottomRef.current || !isNearBottomRef.current) return;
        const el = messagesContainerRef.current;
        if (!el) return;
        requestAnimationFrame(() => {
            if (userPinnedAwayFromBottomRef.current) return;
            el.scrollTop = el.scrollHeight;
        });
    }, [messages]);

    const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
        const file = e.target.files?.[0];
        if (!file) return;

        const previewUrl = file.type.startsWith('image/') ? URL.createObjectURL(file) : undefined;
        setUploadProgress({ name: file.name, percent: 0, previewUrl, sizeBytes: file.size });

        try {
            const { promise } = uploadFileWithProgress(
                '/chat/upload',
                file,
                (pct) => {
                    setUploadProgress((prev) =>
                        prev ? { ...prev, percent: pct >= 101 ? 100 : pct } : null,
                    );
                },
                id ? { agent_id: id } : undefined,
            );
            const data = await promise;
            setAttachedFile({
                name: data.filename,
                text: data.extracted_text,
                path: data.workspace_path,
                imageUrl: data.image_data_url || undefined,
            });
        } catch (err: any) {
            if (err?.message !== 'Upload cancelled') {
                toast.error(t('agent.upload.failed'), { details: String(err?.message || '') });
            }
        } finally {
            if (previewUrl) URL.revokeObjectURL(previewUrl);
            setUploadProgress(null);
            if (fileInputRef.current) fileInputRef.current.value = '';
        }
    };

    const sendMessage = () => {
        if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return;
        if (!input.trim() && !attachedFile) return;

        // Reset streaming state for new response
        pendingToolCalls.current = [];
        streamContent.current = '';
        thinkingContent.current = '';
        userPinnedAwayFromBottomRef.current = false;
        isNearBottomRef.current = true;
        setIsWaiting(true);
        setStreaming(true);

        let userMsg = input.trim();
        let contentForLLM = userMsg;

        if (attachedFile) {
            if (attachedFile.imageUrl && supportsVision) {
                // Vision model — embed image data marker for direct analysis
                const imageMarker = `[image_data:${attachedFile.imageUrl}]`;
                contentForLLM = userMsg
                    ? `${imageMarker}\n${userMsg}`
                    : `${imageMarker}\n请分析这张图片`;
                userMsg = userMsg || `[图片] ${attachedFile.name}`;
            } else if (attachedFile.imageUrl) {
                // Non-vision model — just reference the file path
                const wsPath = attachedFile.path || '';
                contentForLLM = userMsg
                    ? `[图片文件已上传: ${attachedFile.name},保存在 ${wsPath}]\n\n${userMsg}`
                    : `[图片文件已上传: ${attachedFile.name},保存在 ${wsPath}]\n请描述或处理这个图片文件。你可以使用 read_document 工具读取它。`;
                userMsg = userMsg || `[图片] ${attachedFile.name}`;
            } else {
                const wsPath = attachedFile.path || '';
                const codePath = wsPath.replace(/^workspace\//, '');
                const fileLoc = wsPath ? `\nFile location: ${wsPath} (for read_file/read_document tools)\nIn execute_code, use relative path: "${codePath}" (working directory is workspace/)` : '';
                const fileContext = `[文件: ${attachedFile.name}]${fileLoc}\n\n${attachedFile.text}`;
                contentForLLM = userMsg
                    ? `${fileContext}\n\n用户问题: ${userMsg}`
                    : `请阅读并分析以下文件内容:\n\n${fileContext}`;
                userMsg = userMsg || `[${t('agent.chat.attachment')}] ${attachedFile.name}`;
            }
        }

        setMessages((prev) => [...prev, {
            role: 'user',
            content: userMsg,
            fileName: attachedFile?.name,
            imageUrl: attachedFile?.imageUrl,
            timestamp: new Date().toISOString(),
        }]);
        wsRef.current.send(JSON.stringify({ content: contentForLLM, display_content: userMsg, file_name: attachedFile?.name || '', model_id: effectiveChatModelId }));
        setInput('');
        setAttachedFile(null);
    };

    const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
        // Enter sends the message; Shift+Enter inserts a newline
        if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing && !isWaiting && !streaming) {
            e.preventDefault();
            sendMessage();
        }
    };

    const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
        setInput(e.target.value);
    };

    const hasLiveData = !!(liveState.desktop || liveState.browser || liveState.code);

    // ── Drag-and-drop file upload ──
    const handleDroppedFiles = useCallback(async (files: File[]) => {
        const file = files[0]; // Chat supports single file
        if (!file) return;

        const previewUrl = file.type.startsWith('image/') ? URL.createObjectURL(file) : undefined;
        setUploadProgress({ name: file.name, percent: 0, previewUrl, sizeBytes: file.size });

        try {
            const { promise } = uploadFileWithProgress(
                '/chat/upload',
                file,
                (pct) => {
                    setUploadProgress((prev) =>
                        prev ? { ...prev, percent: pct >= 101 ? 100 : pct } : null,
                    );
                },
                id ? { agent_id: id } : undefined,
            );
            const data = await promise;
            setAttachedFile({
                name: data.filename,
                text: data.extracted_text,
                path: data.workspace_path,
                imageUrl: data.image_data_url || undefined,
            });
        } catch (err: any) {
            if (err?.message !== 'Upload cancelled') {
                toast.error(t('agent.upload.failed'), { details: String(err?.message || '') });
            }
        } finally {
            if (previewUrl) URL.revokeObjectURL(previewUrl);
            setUploadProgress(null);
        }
    }, [id, t]);

    const { isDragging: isChatDragging, dropZoneProps: chatDropProps } = useDropZone({
        onDrop: handleDroppedFiles,
        disabled: !connected || !!uploadProgress || isWaiting || streaming,
    });

    return (
        <div style={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
            <div className="page-header">
                <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
                    <div style={{ width: '36px', height: '36px', borderRadius: 'var(--radius-md)', background: 'var(--bg-tertiary)', border: '1px solid var(--border-subtle)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--text-tertiary)' }}>
                        {Icons.bot}
                    </div>
                    <div>
                        <h1 className="page-title" style={{ fontSize: '18px' }}>{agent?.name || '...'}</h1>
                        <div style={{ fontSize: '12px', display: 'flex', alignItems: 'center', gap: '6px' }}>
                            <span className={`status-dot ${connected ? 'running' : 'stopped'}`} />
                            <span style={{ color: 'var(--text-tertiary)' }}>{connected ? t('agent.chat.connected') : t('agent.chat.disconnected')}</span>
                        </div>
                    </div>
                </div>
            </div>

            <div className={`chat-container ${hasLiveData ? 'chat-with-live-panel' : ''}`} {...chatDropProps} style={{ position: 'relative' }}>
                {/* Drop overlay */}
                {isChatDragging && (
                    <div className="drop-zone-overlay">
                        <div className="drop-zone-overlay__icon"><IconPaperclip size={28} stroke={1.8} /></div>
                        <div className="drop-zone-overlay__text">{t('agent.upload.dropToAttach', 'Drop file to attach')}</div>
                    </div>
                )}
                {/* Wrap chat area in a column so it coexists with the live panel in flex-row */}
                <div className="chat-main">
                <div
                    ref={messagesContainerRef}
                    className="chat-messages"
                    onScroll={handleMessagesScroll}
                    onWheelCapture={handleMessagesWheelCapture}
                >
                    {messages.length === 0 && (
                        <div style={{ textAlign: 'center', padding: '60px', color: 'var(--text-tertiary)' }}>
                            <div style={{ marginBottom: '12px', display: 'flex', justifyContent: 'center' }}>{Icons.chat}</div>
                            <div>{t('agent.chat.startConversation', { name: agent?.name || t('nav.newAgent') })}</div>
                            <div style={{ fontSize: '12px', marginTop: '8px', opacity: 0.7 }}>{t('agent.chat.fileSupport')}</div>
                        </div>
                    )}
                    {messages.filter(m => {
                        // Skip empty assistant messages (stream placeholders)
                        if (m.role === 'assistant' && !m._isToolGroup && !(m.content && m.content.trim()) && !m.toolCalls?.length && !m.thinking) return false;
                        return true;
                    }).map((msg, i) => (
                        msg._isToolGroup ? (
                            /* Tool call group — compact display without avatar bubble */
                            <div key={i} style={{ marginLeft: '48px', marginBottom: '8px' }}>
                                {msg.toolCalls && msg.toolCalls.length > 0 && (
                                    <ChatToolChain toolCalls={msg.toolCalls} />
                                )}
                            </div>
                        ) :
                        <div key={i} className={`chat-message ${msg.role}`}>
                            <div className="chat-avatar" style={{ color: 'var(--text-tertiary)' }}>
                                {msg.role === 'user' ? Icons.user : Icons.bot}
                            </div>
                            <div className="chat-bubble">
                                {msg.fileName && (() => {
                                    const fe = msg.fileName!.split('.').pop()?.toLowerCase() ?? '';
                                    const isImage = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp'].includes(fe);
                                    if (isImage && msg.imageUrl) {
                                        return (<div style={{ marginBottom: '4px' }}>
                                            <img src={msg.imageUrl} alt={msg.fileName} style={{ maxWidth: '240px', maxHeight: '180px', borderRadius: '8px', border: '1px solid var(--border-subtle)' }} />
                                        </div>);
                                    }
                                    return (<div style={{ display: 'inline-flex', alignItems: 'center', gap: '5px', background: 'rgba(0,0,0,0.08)', borderRadius: '6px', padding: '4px 8px', marginBottom: msg.content ? '4px' : '0', fontSize: '11px', border: '1px solid var(--border-subtle)', color: 'var(--text-secondary)' }}><IconPaperclip size={13} stroke={1.8} /><span style={{ fontWeight: 500, color: 'var(--text-primary)', maxWidth: '200px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{msg.fileName}</span></div>);
                                })()}
                                {msg.thinking && (
                                    <details className="thinking-panel">
                                        <summary className="thinking-summary">
                                            <span className="thinking-status-dot" />
                                            {streaming && i === messages.length - 1 && !msg.content
                                                ? t('agent.chat.thinkingLabel', '思考中')
                                                : t('agent.chat.thoughtLabel', '已思考')}
                                        </summary>
                                        <div className="thinking-content">
                                            {msg.thinking}
                                        </div>
                                    </details>
                                )}
                                {msg.toolCalls && msg.toolCalls.length > 0 && (
                                    <ChatToolChain toolCalls={msg.toolCalls} />
                                )}
                                {msg.role === 'assistant' ? (
                                    streaming && !msg.content && !msg.thinking && i === messages.length - 1 ? (
                                        <div className="thinking-indicator">
                                            <div className="thinking-dots">
                                                <span /><span /><span />
                                            </div>
                                            <span style={{ color: 'var(--text-tertiary)', fontSize: '13px' }}>{t('agent.chat.thinking', 'Thinking...')}</span>
                                        </div>
                                    ) : (
                                        <MarkdownRenderer content={msg.content} />
                                    )
                                ) : (
                                    <MarkdownRenderer content={msg.content} />
                                )}
                                {msg.timestamp && (
                                    <div style={{ fontSize: '10px', color: 'var(--text-tertiary)', marginTop: '4px', opacity: 0.7 }}>
                                        {new Date(msg.timestamp).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}
                                    </div>
                                )}
                            </div>
                        </div>
                    ))}
                    {(isWaiting || (streaming && (messages.length === 0 || messages[messages.length - 1].role === 'user'))) && (
                        <div className="chat-message assistant">
                            <div className="chat-avatar" style={{ color: 'var(--text-tertiary)' }}>
                                {Icons.bot}
                            </div>
                            <div className="chat-bubble">
                                <div className="thinking-indicator">
                                    <div className="thinking-dots">
                                        <span /><span /><span />
                                    </div>
                                    <span style={{ color: 'var(--text-tertiary)', fontSize: '13px' }}>{t('agent.chat.thinking', 'Thinking...')}</span>
                                </div>
                            </div>
                        </div>
                    )}
                    <div ref={messagesEndRef} />
                </div>

                <div className="chat-input-area">
                    <div className="chat-composer">
                        {(uploadProgress || (attachedFile && !uploadProgress)) && (
                            <div className="chat-composer-attachments">
                                {uploadProgress && (
                                    <div className="chat-file-pill">
                                        <div
                                            className="chat-file-pill__fill"
                                            style={{ width: `${uploadProgress.percent}%` }}
                                        />
                                        <div className="chat-file-pill__row">
                                            {uploadProgress.previewUrl ? (
                                                <img className="chat-file-pill__thumb" src={uploadProgress.previewUrl} alt="" />
                                            ) : (
                                                <span className="chat-file-pill__icon">
                                                    <IconPaperclip size={14} stroke={1.75} />
                                                </span>
                                            )}
                                            <span className="chat-file-pill__name">{uploadProgress.name}</span>
                                            <span className="chat-file-pill__size">{formatFileSize(uploadProgress.sizeBytes)}</span>
                                            <span className="chat-file-pill__pct">{uploadProgress.percent}%</span>
                                        </div>
                                    </div>
                                )}
                                {attachedFile && !uploadProgress && (
                                    <div className="chat-file-pill">
                                        <div className="chat-file-pill__row">
                                            {attachedFile.imageUrl ? (
                                                <img className="chat-file-pill__thumb" src={attachedFile.imageUrl} alt="" />
                                            ) : (
                                                <span className="chat-file-pill__icon">
                                                    <IconPaperclip size={14} stroke={1.75} />
                                                </span>
                                            )}
                                            <span className="chat-file-pill__name">{attachedFile.name}</span>
                                            <button
                                                type="button"
                                                className="chat-file-pill__remove"
                                                onClick={() => setAttachedFile(null)}
                                                title={t('common.close', 'Close')}
                                            >
                                                ×
                                            </button>
                                        </div>
                                    </div>
                                )}
                            </div>
                        )}
                        <div className="chat-composer-input-block">
                            <textarea
                                ref={textareaRef}
                                className="chat-input"
                                value={input}
                                onChange={handleInputChange}
                                onKeyDown={handleKeyDown}
                                placeholder={t('chat.placeholder')}
                                disabled={!connected}
                                rows={1}
                            />
                        </div>
                        <div className="chat-composer-toolbar">
                            <input type="file" ref={fileInputRef} onChange={handleFileSelect} style={{ display: 'none' }} />
                            <button
                                type="button"
                                className="chat-composer-btn"
                                onClick={() => fileInputRef.current?.click()}
                                disabled={!connected || !!uploadProgress || isWaiting || streaming}
                                title={t('agent.workspace.uploadFile')}
                            >
                                <IconPaperclip size={16} stroke={1.75} />
                            </button>
                            <ModelSwitcher
                                value={overrideModelId}
                                onChange={handleModelChange}
                                tenantDefaultId={myTenant?.default_model_id || null}
                                disabled={!connected}
                            />
                            <div style={{ flex: 1 }} />
                            {(streaming || isWaiting) ? (
                                <button
                                    type="button"
                                    className="btn btn-stop-generation"
                                    onClick={() => {
                                        if (wsRef.current?.readyState === WebSocket.OPEN) {
                                            wsRef.current.send(JSON.stringify({ type: 'abort' }));
                                            setStreaming(false);
                                            setIsWaiting(false);
                                        }
                                    }}
                                    title={t('chat.stop', 'Stop')}
                                >
                                    <span className="stop-icon" />
                                </button>
                            ) : (
                                <button
                                    type="button"
                                    className="btn btn-primary chat-composer-send"
                                    onClick={sendMessage}
                                    disabled={!connected || (!input.trim() && !attachedFile)}
                                    title={t('chat.send')}
                                >
                                    <IconSend size={16} stroke={1.75} />
                                </button>
                            )}
                        </div>
                    </div>
                </div>
                </div>

                {/* AgentBay Live Preview Panel */}
                {hasLiveData && (
                    <AgentBayLivePanel
                        liveState={liveState}
                        visible={livePanelVisible}
                        onToggle={() => setLivePanelVisible(v => !v)}
                        agentId={id}
                        sessionId={wsSessionId}
                        onLiveUpdate={(env, screenshotDataUri) => {
                            // Update live preview with the latest screenshot from Take Control
                            setLiveState(prev => ({
                                ...prev,
                                [env]: { screenshotUrl: screenshotDataUri },
                            }));
                        }}
                    />
                )}
            </div>
        </div>
    );
}