<template>
  <div
    class="messages-area messages-area--stick"
    :class="{ 'messages-area--render-pending': renderPending }"
    ref="scrollRef"
  >
    <div class="messages-flow" ref="contentRef">
      <div
        v-for="(msg, index) in filteredMessages || []"
        :key="index"
        class="message-block"
        :class="{
          'message-block--compact-user': getMessageVisibility(msg) === 'compact',
          'message-block--sub-agent-notice': isSubAgentNoticeOnlyMessage(msg),
          'message-block--before-sub-agent-notice': isFollowedBySubAgentNotice(index),
          'message-block--multi-agent': isMultiAgentMessage(msg),
          'message-block--last': index === (filteredMessages || []).length - 1
        }"
      >
        <div
          v-if="msg.role === 'user'"
          class="user-message"
          :class="{
            'user-message--multi-agent': isMultiAgentMessage(msg),
            'user-message--compact': getMessageVisibility(msg) === 'compact',
            'user-message--brief': isBriefCompactMessage(msg)
          }"
        >
          <div v-if="isBriefCompactMessage(msg)" class="compact-brief-line" role="separator">
            <span class="compact-brief-text">{{ compactBriefLabel(msg) }}</span>
          </div>
          <template v-else>
            <div
              class="message-header icon-label"
              :class="{
                'message-header--multi-agent': isMultiAgentMessage(msg),
                'message-header--hidden':
                  isMultiAgentMessage(msg) && isContinuousMultiAgentMessage(msg, index)
              }"
            >
              <template v-if="isMultiAgentMessage(msg)">
                <span>{{ multiAgentHeaderLabel(msg) }}</span>
              </template>
              <template v-else>
                <span
                  class="icon icon-sm"
                  :style="iconStyleSafe(userHeaderIconKey(msg))"
                  aria-hidden="true"
                ></span>
                <span>{{ userHeaderLabel(msg) }}</span>
              </template>
            </div>
            <div
              class="message-text user-bubble-text"
              :class="{
                'is-collapsed': isUserBubbleCollapsed(msg, index),
                'is-expandable': isUserBubbleExpandable(msg, index),
                'is-history-loading': props.historyLoading || userBubbleTransitionSuppressed
              }"
            >
              <div
                v-if="msg.content"
                class="bubble-text"
                :class="{ 'is-expanded': isUserBubbleExpanded(msg, index) }"
                :ref="(el) => registerUserBubbleRef(msg, index, el)"
              >
                <template v-if="isMultiAgentMessage(msg)">
                  <MarkdownRenderer
                    :content="multiAgentBubbleContent(msg)"
                    :is-streaming="false"
                  />
                </template>
                <template v-else>
                  <span v-html="renderUserMessageContent(msg.content)"></span>
                </template>
              </div>
              <div v-if="msg.images && msg.images.length" class="image-inline-row">
                <div
                  class="image-thumbnail-wrapper"
                  v-for="(img, imgIndex) in msg.images"
                  :key="mediaPreviewKey(msg, img, imgIndex)"
                >
                  <img
                    :src="getPreviewUrl(msg, img, 'image')"
                    :alt="formatImageName(img)"
                    class="image-thumbnail"
                  />
                </div>
              </div>
              <div v-if="msg.videos && msg.videos.length" class="image-inline-row video-inline-row">
                <div
                  class="image-thumbnail-wrapper"
                  v-for="(video, videoIndex) in msg.videos"
                  :key="mediaPreviewKey(msg, video, videoIndex)"
                >
                  <img
                    :src="getPreviewUrl(msg, video, 'video')"
                    :alt="formatImageName(video)"
                    class="image-thumbnail"
                  />
                </div>
              </div>
              <div
                v-if="isUserBubbleExpandable(msg, index)"
                class="user-bubble-expand-row"
              >
                <button
                  class="user-bubble-action-btn user-bubble-expand-btn"
                  :class="{ expanded: isUserBubbleExpanded(msg, index) }"
                  :title="isUserBubbleExpanded(msg, index) ? '收起' : '展开'"
                  :aria-label="isUserBubbleExpanded(msg, index) ? '收起' : '展开'"
                  @click.stop="toggleUserBubble(msg, index)"
                ></button>
              </div>
            </div>
            <div v-if="!isMultiAgentMessage(msg)" class="user-bubble-actions">
              <button
                class="user-bubble-action-btn copy"
                :class="{ copied: isUserBubbleCopied(msg, index) }"
                :title="isUserBubbleCopied(msg, index) ? '已复制' : '复制'"
                :aria-label="isUserBubbleCopied(msg, index) ? '已复制' : '复制'"
                @click="copyUserMessage(msg, index)"
              ></button>
              <button
                class="user-bubble-action-btn branch"
                title="分支"
                aria-label="分支"
                @click="branchUserMessage(msg)"
              ></button>
              <span class="user-bubble-time">{{ formatUserMessageTime(msg) }}</span>
            </div>
          </template>
        </div>
        <div v-else-if="msg.role === 'assistant'" class="assistant-message">
          <!-- 在 assistant 消息前显示 Astrion 头部 -->
          <!-- 只有当前一条消息是 user 且当前消息有内容时才显示 -->
          <div
            v-if="
              shouldShowAssistantHeader(index) &&
              (hasRenderableAssistantActions(msg.actions || []) || msg.awaitingFirstContent)
            "
            class="message-header icon-label"
          >
            <span>{{ aiAssistantName }}</span>
            <span v-if="assistantWorkLabel(index)" class="assistant-work-status">{{
              assistantWorkLabel(index)
            }}</span>
          </div>
          <div
            v-if="msg.awaitingFirstContent"
            class="action-item streaming-content immediate-show assistant-generating-block"
          >
            <div class="text-output">
              <div
                class="text-content assistant-generating-placeholder"
                role="status"
                aria-live="polite"
              >
                <span
                  v-for="(letter, letterIndex) in getGeneratingLetters(msg)"
                  :key="letterIndex"
                  class="assistant-generating-letter"
                  :style="{ animationDelay: `${letterIndex * 0.08}s` }"
                >
                  {{ letter }}
                </span>
              </div>
            </div>
          </div>
          <template v-if="blockDisplayMode === 'minimal'">
            <MinimalBlocks
              v-if="hasRenderableAssistantActions(msg.actions || [])"
              :actions="msg.actions || []"
              :conversation-running="streamingMessage"
              :is-latest-message="index === latestMessageIndex"
              :icon-style="iconStyleSafe"
              :get-tool-icon="getToolIcon"
              :get-tool-status-text="getToolStatusText"
              :get-tool-description="getToolDescription"
              :format-search-topic="formatSearchTopic"
              :format-search-time="formatSearchTime"
              :format-search-domains="formatSearchDomains"
              :register-thinking-ref="registerThinkingRef"
              :handle-thinking-scroll="props.handleThinkingScroll"
              @group-toggle="handleMinimalGroupToggle"
            />
          </template>
          <template v-else-if="stackedBlocksEnabled">
            <template v-for="group in splitActionGroups(msg.actions || [], index)" :key="group.key">
              <StackedBlocks
                v-if="group.kind === 'stack'"
                class="stacked-blocks-wrapper"
                :actions="group.actions"
                :expanded-blocks="expandedBlocks"
                :conversation-running="streamingMessage"
                :is-latest-message="index === latestMessageIndex"
                :icon-style="iconStyleSafe"
                :toggle-block="toggleBlock"
                :register-thinking-ref="registerThinkingRef"
                :handle-thinking-scroll="handleThinkingScroll"
                :get-tool-animation-class="getToolAnimationClass"
                :get-tool-icon="getToolIcon"
                :get-tool-status-text="getToolStatusText"
                :get-tool-description="getToolDescription"
                :format-search-topic="formatSearchTopic"
                :format-search-time="formatSearchTime"
                :format-search-domains="formatSearchDomains"
                @more-toggle="handleStackedMoreToggle"
              />
              <div
                v-else-if="isActionVisible(group.action)"
                class="action-item"
                :key="group.action?.id || `${index}-${group.actionIndex}`"
                :class="{
                  'streaming-content': group.action?.streaming,
                  'completed-tool': group.action?.type === 'tool' && !group.action?.streaming,
                  'immediate-show':
                    group.action?.streaming ||
                    group.action?.type === 'text' ||
                    group.action?.type === 'thinking',
                  'thinking-finished': group.action?.type === 'thinking' && !group.action?.streaming,
                  'no-entry-animation': !streamingMessage || index !== latestMessageIndex
                }"
              >
                <div
                  v-if="group.action?.type === 'thinking'"
                  class="collapsible-block thinking-block"
                  :class="{
                    expanded: expandedBlocks?.has(
                      group.action.blockId || `${index}-thinking-${group.actionIndex}`
                    )
                  }"
                  :data-block-id="group.action.blockId || `${index}-thinking-${group.actionIndex}`"
                >
                  <div
                    class="collapsible-header"
                    @click="
                      toggleBlock(group.action.blockId || `${index}-thinking-${group.actionIndex}`)
                    "
                  >
                    <div class="arrow"></div>
                    <div class="status-icon">
                      <span
                        class="thinking-icon"
                        :class="{ 'thinking-animation': group.action.streaming }"
                      >
                        <span
                          class="icon icon-sm"
                          :style="iconStyleSafe('brain')"
                          aria-hidden="true"
                        ></span>
                      </span>
                    </div>
                    <span class="status-text">{{
                      group.action.streaming ? '正在思考...' : '思考过程'
                    }}</span>
                  </div>
                  <div
                    class="collapsible-content"
                    :ref="
                      (el) =>
                        registerCollapseContent(
                          group.action.blockId || `${index}-thinking-${group.actionIndex}`,
                          el
                        )
                    "
                  >
                    <div
                      class="content-inner thinking-content"
                      :ref="
                        (el) =>
                          registerThinkingRef(
                            group.action.blockId || `${index}-thinking-${group.actionIndex}`,
                            el
                          )
                      "
                      @scroll="
                        handleThinkingScroll(
                          group.action.blockId || `${index}-thinking-${group.actionIndex}`,
                          $event
                        )
                      "
                      style="max-height: 240px; overflow-y: auto"
                    >
                      {{ group.action.content }}
                    </div>
                  </div>
                </div>

                <div v-else-if="group.action?.type === 'text'" class="text-output">
                  <div class="text-content" :class="{ 'streaming-text': group.action.streaming }">
                    <MarkdownRenderer
                      :content="group.action.content || ''"
                      :is-streaming="group.action.streaming"
                    />
                  </div>
                </div>

                <div
                  v-else-if="
                    group.action?.type === 'system' && getSubAgentSystemNoticeLabel(group.action)
                  "
                  class="sub-agent-system-summary-line"
                >
                  {{ getSubAgentSystemNoticeLabel(group.action) }}
                </div>

                <div
                  v-else-if="group.action?.type === 'append_payload'"
                  class="append-placeholder"
                  :class="{ 'append-error': group.action.append?.success === false }"
                >
                  <div class="append-placeholder-content">
                    <template v-if="group.action.append?.success !== false">
                      <div class="icon-label append-status">
                        <span
                          class="icon icon-sm"
                          :style="iconStyleSafe('pencil')"
                          aria-hidden="true"
                        ></span>
                        <span
                          >已写入
                          {{ group.action.append?.path || '目标文件' }}
                          的追加内容（内容已保存至文件）</span
                        >
                      </div>
                    </template>
                    <template v-else>
                      <div class="icon-label append-status append-error-text">
                        <span
                          class="icon icon-sm"
                          :style="iconStyleSafe('x')"
                          aria-hidden="true"
                        ></span>
                        <span
                          >向
                          {{ group.action.append?.path || '目标文件' }}
                          写入失败，内容已截获供后续修复。</span
                        >
                      </div>
                    </template>
                    <div class="append-meta" v-if="group.action.append">
                      <span
                        v-if="
                          group.action.append.lines !== null &&
                          group.action.append.lines !== undefined
                        "
                      >
                        · 行数 {{ group.action.append.lines }}
                      </span>
                      <span
                        v-if="
                          group.action.append.bytes !== null &&
                          group.action.append.bytes !== undefined
                        "
                      >
                        · 字节 {{ group.action.append.bytes }}
                      </span>
                    </div>
                    <div class="append-warning icon-label" v-if="group.action.append?.forced">
                      <span
                        class="icon icon-sm"
                        :style="iconStyleSafe('triangleAlert')"
                        aria-hidden="true"
                      ></span>
                      <span>未检测到结束标记，请根据提示继续补充。</span>
                    </div>
                  </div>
                </div>

                <div
                  v-else-if="group.action?.type === 'append'"
                  class="append-placeholder"
                  :class="{ 'append-error': group.action.append?.success === false }"
                >
                  <div class="append-placeholder-content">
                    <div class="icon-label append-status">
                      <span
                        class="icon icon-sm"
                        :style="iconStyleSafe('pencil')"
                        aria-hidden="true"
                      ></span>
                      <span>{{ group.action.append?.summary || '文件追加完成' }}</span>
                    </div>
                    <div class="append-meta" v-if="group.action.append">
                      <span>{{ group.action.append.path || '目标文件' }}</span>
                      <span v-if="group.action.append.lines"
                        >· 行数 {{ group.action.append.lines }}</span
                      >
                      <span v-if="group.action.append.bytes"
                        >· 字节 {{ group.action.append.bytes }}</span
                      >
                    </div>
                    <div class="append-warning icon-label" v-if="group.action.append?.forced">
                      <span
                        class="icon icon-sm"
                        :style="iconStyleSafe('triangleAlert')"
                        aria-hidden="true"
                      ></span>
                      <span>未检测到结束标记，请按提示继续补充。</span>
                    </div>
                  </div>
                </div>

                <ToolAction
                  v-else-if="group.action?.type === 'tool'"
                  :action="group.action"
                  :expanded="
                    expandedBlocks?.has(
                      group.action.blockId || `${index}-tool-${group.actionIndex}`
                    )
                  "
                  :icon-style="iconStyleSafe"
                  :get-tool-animation-class="getToolAnimationClass"
                  :get-tool-icon="getToolIcon"
                  :get-tool-status-text="getToolStatusText"
                  :get-tool-description="getToolDescription"
                  :format-search-topic="formatSearchTopic"
                  :format-search-time="formatSearchTime"
                  :format-search-domains="formatSearchDomains"
                  :streaming-message="streamingMessage"
                  :register-collapse-content="registerCollapseContent"
                  :collapse-key="group.action.blockId || `${index}-tool-${group.actionIndex}`"
                  :block-id="group.action.blockId || `${index}-tool-${group.actionIndex}`"
                  @toggle="
                    toggleBlock(group.action.blockId || `${index}-tool-${group.actionIndex}`)
                  "
                />
              </div>
            </template>
          </template>

          <template v-else>
            <template
              v-for="(action, actionIndex) in msg.actions || []"
              :key="action.id || `${index}-${actionIndex}`"
            >
              <div
                v-if="isActionVisible(action)"
                class="action-item"
                :class="{
                  'streaming-content': action.streaming,
                  'completed-tool': action.type === 'tool' && !action.streaming,
                  'immediate-show':
                    action.streaming || action.type === 'text' || action.type === 'thinking',
                  'thinking-finished': action.type === 'thinking' && !action.streaming,
                  'no-entry-animation': !streamingMessage || index !== latestMessageIndex
                }"
              >
                <div
                  v-if="action.type === 'thinking'"
                  class="collapsible-block thinking-block"
                  :class="{
                    expanded: expandedBlocks?.has(
                      action.blockId || `${index}-thinking-${actionIndex}`
                    )
                  }"
                  :data-block-id="action.blockId || `${index}-thinking-${actionIndex}`"
                >
                  <div
                    class="collapsible-header"
                    @click="toggleBlock(action.blockId || `${index}-thinking-${actionIndex}`)"
                  >
                    <div class="arrow"></div>
                    <div class="status-icon">
                      <span
                        class="thinking-icon"
                        :class="{ 'thinking-animation': action.streaming }"
                      >
                        <span
                          class="icon icon-sm"
                          :style="iconStyleSafe('brain')"
                          aria-hidden="true"
                        ></span>
                      </span>
                    </div>
                    <span class="status-text">{{
                      action.streaming ? '正在思考...' : '思考过程'
                    }}</span>
                  </div>
                  <div
                    class="collapsible-content"
                    :ref="
                      (el) =>
                        registerCollapseContent(
                          action.blockId || `${index}-thinking-${actionIndex}`,
                          el
                        )
                    "
                  >
                    <div
                      class="content-inner thinking-content"
                      :ref="
                        (el) =>
                          registerThinkingRef(
                            action.blockId || `${index}-thinking-${actionIndex}`,
                            el
                          )
                      "
                      @scroll="
                        handleThinkingScroll(
                          action.blockId || `${index}-thinking-${actionIndex}`,
                          $event
                        )
                      "
                      style="max-height: 240px; overflow-y: auto"
                    >
                      {{ action.content }}
                    </div>
                  </div>
                </div>

                <div v-else-if="action.type === 'text'" class="text-output">
                  <div class="text-content" :class="{ 'streaming-text': action.streaming }">
                    <MarkdownRenderer
                      :content="action.content || ''"
                      :is-streaming="action.streaming"
                    />
                  </div>
                </div>

                <div
                  v-else-if="action.type === 'system' && getSubAgentSystemNoticeLabel(action)"
                  class="sub-agent-system-summary-line"
                >
                  {{ getSubAgentSystemNoticeLabel(action) }}
                </div>

                <div
                  v-else-if="action.type === 'append_payload'"
                  class="append-placeholder"
                  :class="{ 'append-error': action.append?.success === false }"
                >
                  <div class="append-placeholder-content">
                    <template v-if="action.append?.success !== false">
                      <div class="icon-label append-status">
                        <span
                          class="icon icon-sm"
                          :style="iconStyleSafe('pencil')"
                          aria-hidden="true"
                        ></span>
                        <span
                          >已写入
                          {{ action.append?.path || '目标文件' }}
                          的追加内容（内容已保存至文件）</span
                        >
                      </div>
                    </template>
                    <template v-else>
                      <div class="icon-label append-status append-error-text">
                        <span
                          class="icon icon-sm"
                          :style="iconStyleSafe('x')"
                          aria-hidden="true"
                        ></span>
                        <span
                          >向
                          {{ action.append?.path || '目标文件' }}
                          写入失败，内容已截获供后续修复。</span
                        >
                      </div>
                    </template>
                    <div class="append-meta" v-if="action.append">
                      <span
                        v-if="action.append.lines !== null && action.append.lines !== undefined"
                      >
                        · 行数 {{ action.append.lines }}
                      </span>
                      <span
                        v-if="action.append.bytes !== null && action.append.bytes !== undefined"
                      >
                        · 字节 {{ action.append.bytes }}
                      </span>
                    </div>
                    <div class="append-warning icon-label" v-if="action.append?.forced">
                      <span
                        class="icon icon-sm"
                        :style="iconStyleSafe('triangleAlert')"
                        aria-hidden="true"
                      ></span>
                      <span>未检测到结束标记，请根据提示继续补充。</span>
                    </div>
                  </div>
                </div>

                <div
                  v-else-if="action.type === 'append'"
                  class="append-placeholder"
                  :class="{ 'append-error': action.append?.success === false }"
                >
                  <div class="append-placeholder-content">
                    <div class="icon-label append-status">
                      <span
                        class="icon icon-sm"
                        :style="iconStyleSafe('pencil')"
                        aria-hidden="true"
                      ></span>
                      <span>{{ action.append?.summary || '文件追加完成' }}</span>
                    </div>
                    <div class="append-meta" v-if="action.append">
                      <span>{{ action.append.path || '目标文件' }}</span>
                      <span v-if="action.append.lines">· 行数 {{ action.append.lines }}</span>
                      <span v-if="action.append.bytes">· 字节 {{ action.append.bytes }}</span>
                    </div>
                    <div class="append-warning icon-label" v-if="action.append?.forced">
                      <span
                        class="icon icon-sm"
                        :style="iconStyleSafe('triangleAlert')"
                        aria-hidden="true"
                      ></span>
                      <span>未检测到结束标记，请按提示继续补充。</span>
                    </div>
                  </div>
                </div>

                <ToolAction
                  v-else-if="action.type === 'tool'"
                  :action="action"
                  :expanded="expandedBlocks?.has(action.blockId || `${index}-tool-${actionIndex}`)"
                  :icon-style="iconStyleSafe"
                  :get-tool-animation-class="getToolAnimationClass"
                  :get-tool-icon="getToolIcon"
                  :get-tool-status-text="getToolStatusText"
                  :get-tool-description="getToolDescription"
                  :format-search-topic="formatSearchTopic"
                  :format-search-time="formatSearchTime"
                  :format-search-domains="formatSearchDomains"
                  :streaming-message="streamingMessage"
                  :register-collapse-content="registerCollapseContent"
                  :collapse-key="action.blockId || `${index}-tool-${actionIndex}`"
                  :block-id="action.blockId || `${index}-tool-${actionIndex}`"
                  @toggle="toggleBlock(action.blockId || `${index}-tool-${actionIndex}`)"
                />
              </div>
            </template>
          </template>
        </div>
        <div
          v-else
          class="system-message"
          @vue:mounted="
            () =>
              console.log('[ChatArea] 渲染系统消息:', {
                index,
                role: msg.role,
                content: msg.content
              })
          "
        >
          <div
            class="collapsible-block system-block"
            :class="{ expanded: expandedBlocks?.has(`system-${index}`) }"
            :data-block-id="`system-${index}`"
          >
            <div class="collapsible-header" @click="toggleBlock(`system-${index}`)">
              <div class="arrow"></div>
              <div class="status-icon">
                <span
                  class="tool-icon icon icon-md"
                  :style="iconStyleSafe('info')"
                  aria-hidden="true"
                ></span>
              </div>
              <span class="status-text">系统消息 (role: {{ msg.role }})</span>
            </div>
            <div
              class="collapsible-content"
              :ref="(el) => registerCollapseContent(`system-${index}`, el)"
            >
              <div class="content-inner">
                {{ msg.content }}
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
    <div class="messages-bottom-spacer" aria-hidden="true"></div>
  </div>
</template>

<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, provide, ref, watch } from 'vue';
import { useStickToBottom } from 'vue-stick-to-bottom';
import { useBlockExpansionAnchor } from '@/composables/useBlockExpansionAnchor';
import ToolAction from '@/components/chat/actions/ToolAction.vue';
import StackedBlocks from './StackedBlocks.vue';
import MinimalBlocks from './MinimalBlocks.vue';
import MarkdownRenderer from './MarkdownRenderer.vue';
import { usePersonalizationStore } from '@/stores/personalization';
import { getMessageVisibility, messageStartsWork } from '@/utils/messageVisibility';

const props = defineProps<{
  messages: Array<any>;
  iconStyle: (key: string, size?: string) => Record<string, string>;
  expandedBlocks: Set<string>;
  streamingMessage: boolean;
  toggleBlock: (blockId: string) => void;
  handleThinkingScroll: (blockId: string, event: Event) => void;
  renderMarkdown: (content: string, isStreaming: boolean) => string;
  getToolIcon: (tool: any) => string;
  getToolStatusText: (tool: any) => string;
  getToolAnimationClass: (tool: any) => string | Record<string, unknown>;
  getToolDescription: (tool: any) => string;
  formatSearchTopic: (filters: Record<string, any>) => string;
  formatSearchTime: (filters: Record<string, any>) => string;
  formatSearchDomains: (filters: Record<string, any>) => string;
  historyLoading?: boolean;
}>();
const emit = defineEmits<{
  (
    event: 'stick-state-change',
    payload: { isAtBottom: boolean; isNearBottom: boolean; escapedFromLock: boolean }
  ): void;
  (event: 'user-scroll-intent', payload: { ts: number; delta: number; top: number }): void;
}>();

const personalization = usePersonalizationStore();
const personalizationReady = computed(() => {
  return !!personalization.loaded || !!personalization.error;
});
const renderPending = computed(() => {
  return !!props.historyLoading || !personalizationReady.value;
});
const blockDisplayMode = computed(() => {
  return personalization.experiments.blockDisplayMode || 'stacked';
});
const compactMessageDisplay = computed(() => {
  return (
    personalization.form.compact_message_display ||
    personalization.experiments.compactMessageDisplay ||
    'full'
  );
});
const stackedBlocksEnabled = computed(() => {
  // 堆叠模式和极简模式都使用堆叠布局
  return blockDisplayMode.value === 'stacked' || blockDisplayMode.value === 'minimal';
});
const aiAssistantName = computed(() => {
  if (!personalization.form.use_custom_names) {
    return 'Astrion';
  }
  return personalization.form.self_identify || 'Astrion';
});
const userName = computed(() => {
  if (!personalization.form.use_custom_names) {
    return '用户';
  }
  return personalization.form.user_name || '用户';
});

// 用户输入气泡折叠相关状态与逻辑
interface UserBubbleFoldState {
  needsFold: boolean;
  expanded: boolean;
  foldHeight: number;
  fullHeight: number;
}

const USER_BUBBLE_FOLD_LINES = 10;
const DEFAULT_GENERATING_TEXT = '生成中…';
const userBubbleRefs = new Map<string, HTMLElement>();
const userBubbleFoldStates = ref<Record<string, UserBubbleFoldState>>({});
const copiedBubbleKeys = ref<Set<string>>(new Set());
const userBubbleTransitionSuppressed = ref(false);
let userBubbleResizeObserver: ResizeObserver | null = null;
let copiedBubbleTimeouts = new Map<string, number>();
let userBubbleTransitionSeq = 0;

const getUserBubbleKey = (msg: any, index: number) => msg?.id || `user-bubble-${index}`;

const registerUserBubbleRef = (msg: any, index: number, el: Element | null) => {
  const key = getUserBubbleKey(msg, index);
  if (el instanceof HTMLElement) {
    userBubbleRefs.set(key, el);
  } else {
    userBubbleRefs.delete(key);
  }
};

const estimateUserBubbleNeedsFold = (msg: any): boolean => {
  const content = typeof msg?.content === 'string' ? msg.content : '';
  if (!content) return false;
  // 按常见桌面端气泡宽度和字号估算：每行约 48 个字符。
  // 预留余量，避免短气泡被误判为需要折叠而出现“先折叠后展开”的跳变。
  const APPROX_CHARS_PER_LINE = 48;
  const foldChars = APPROX_CHARS_PER_LINE * (USER_BUBBLE_FOLD_LINES + 2);
  const newlineCount = (content.match(/\n/g) || []).length;
  return content.length > foldChars || newlineCount >= USER_BUBBLE_FOLD_LINES + 2;
};

const preMeasureUserBubbles = () => {
  // 在 DOM 测量前，先根据文本内容同步估算是否需要折叠，
  // 避免长气泡先完整渲染再被收起。
  // 只给尚未有状态记录的气泡做首次估算，避免切换对话时旧消息反复重算导致动画。
  (Array.isArray(filteredMessages.value) ? filteredMessages.value : []).forEach((msg, index) => {
    if (!msg || msg.role !== 'user') return;
    const key = getUserBubbleKey(msg, index);
    if (userBubbleFoldStates.value[key]) return;
    const needsFold = estimateUserBubbleNeedsFold(msg);
    userBubbleFoldStates.value[key] = {
      needsFold,
      expanded: false,
      foldHeight: 0,
      fullHeight: 0
    };
  });
};

const measureUserBubbles = () => {
  userBubbleRefs.forEach((el, key) => {
    if (!el.isConnected) {
      userBubbleRefs.delete(key);
      return;
    }
    const computed = window.getComputedStyle(el);
    const rawLineHeight = parseFloat(computed.lineHeight);
    const lineHeight = Number.isFinite(rawLineHeight) ? rawLineHeight : 24;
    const foldHeight = Math.round(lineHeight * USER_BUBBLE_FOLD_LINES);
    const fullHeight = Math.ceil(el.scrollHeight);
    const needsFold = fullHeight > foldHeight + 1;
    const existing = userBubbleFoldStates.value[key];
    userBubbleFoldStates.value[key] = {
      needsFold,
      expanded: existing?.expanded ?? false,
      foldHeight,
      fullHeight
    };
    el.style.setProperty('--bubble-fold-height', `${foldHeight}px`);
    el.style.setProperty('--bubble-full-height', `${fullHeight}px`);
  });
};

const isUserBubbleExpandable = (msg: any, index: number) => {
  const key = getUserBubbleKey(msg, index);
  return !!userBubbleFoldStates.value[key]?.needsFold;
};

const isUserBubbleExpanded = (msg: any, index: number) => {
  const key = getUserBubbleKey(msg, index);
  return !!userBubbleFoldStates.value[key]?.expanded;
};

const isUserBubbleCollapsed = (msg: any, index: number) => {
  const key = getUserBubbleKey(msg, index);
  const state = userBubbleFoldStates.value[key];
  return !!state?.needsFold && !state?.expanded;
};

const toggleUserBubble = (msg: any, index: number) => {
  const key = getUserBubbleKey(msg, index);
  const state = userBubbleFoldStates.value[key];
  if (!state || !state.needsFold) return;
  userBubbleFoldStates.value[key] = { ...state, expanded: !state.expanded };
};

const formatUserMessageTime = (msg: any): string => {
  const createdAt = msg?.created_at;
  if (!createdAt) return '';
  const date = new Date(createdAt);
  const now = new Date();
  const diffMs = Math.max(0, now.getTime() - date.getTime());
  const diffSec = Math.floor(diffMs / 1000);
  const diffMin = Math.floor(diffSec / 60);
  const diffHour = Math.floor(diffMin / 60);
  const diffDay = Math.floor(diffHour / 24);

  if (diffDay >= 1) {
    const month = date.getMonth() + 1;
    const day = date.getDate();
    const hour = String(date.getHours()).padStart(2, '0');
    const minute = String(date.getMinutes()).padStart(2, '0');
    return `${month}月${day}日 ${hour}:${minute}`;
  }
  if (diffHour > 0) return `${diffHour}小时前`;
  if (diffMin > 0) return `${diffMin}分钟前`;
  if (diffSec > 0) return `${diffSec}秒前`;
  return '刚刚';
};

const isUserBubbleCopied = (msg: any, index: number) => {
  const key = getUserBubbleKey(msg, index);
  return copiedBubbleKeys.value.has(key);
};

const copyUserMessage = async (msg: any, index: number) => {
  const key = getUserBubbleKey(msg, index);
  const text = typeof msg?.content === 'string' ? msg.content : '';
  if (!text || copiedBubbleKeys.value.has(key)) return;
  try {
    await navigator.clipboard.writeText(text);
    copiedBubbleKeys.value.add(key);
    const existing = copiedBubbleTimeouts.get(key);
    if (existing) window.clearTimeout(existing);
    const timeout = window.setTimeout(() => {
      copiedBubbleKeys.value.delete(key);
      copiedBubbleTimeouts.delete(key);
    }, 5000);
    copiedBubbleTimeouts.set(key, timeout);
  } catch (err) {
    console.warn('复制用户消息失败:', err);
  }
};

const branchUserMessage = (_msg: any) => {
  // TODO: 分支功能占位
  void _msg;
  console.log('[ChatArea] 分支功能待实现');
};

const observeUserBubbles = () => {
  if (!userBubbleResizeObserver) return;
  userBubbleResizeObserver.disconnect();
  userBubbleRefs.forEach((el) => {
    if (el.isConnected) userBubbleResizeObserver!.observe(el);
  });
};

const releaseUserBubbleTransitionAfterLayout = () => {
  const seq = ++userBubbleTransitionSeq;
  requestAnimationFrame(() => {
    requestAnimationFrame(() => {
      if (seq === userBubbleTransitionSeq) {
        userBubbleTransitionSuppressed.value = false;
      }
    });
  });
};

const isSystemAutoUserMessage = (message: any) => {
  if (!message || message.role !== 'user') {
    return false;
  }
  const meta = message.metadata || {};
  return !!(
    meta.is_auto_generated ||
    meta.auto_message_type ||
    meta.sub_agent_notice ||
    meta.background_command_notice
  );
};

// ---------- 多智能体消息渲染 ----------

interface MultiAgentMessageInfo {
  displayName: string;
  subtype: string;
  content: string;
}

const MULTI_AGENT_MESSAGE_RE =
  /^来自\s+(.+?)\s+的(.+?)\nid:\s*(\S+)\n\n<(.+?)>\n<(\w+)([^>]*)>\n([\s\S]*?)\n<\/\5>\n<\/\4>$/;

function parseMultiAgentMessage(content: string): MultiAgentMessageInfo | null {
  const text = String(content || '');
  const m = text.match(MULTI_AGENT_MESSAGE_RE);
  if (!m) {
    return null;
  }
  const attrs = m[6] || '';
  const subtypeMatch = attrs.match(/subtype="([^"]+)"/);
  return {
    displayName: m[1].trim(),
    subtype: subtypeMatch ? subtypeMatch[1] : '',
    content: m[7]
  };
}

function isMultiAgentMessage(message: any): boolean {
  if (!message || message.role !== 'user') {
    return false;
  }
  const meta = message.metadata || {};
  const autoType = String(meta.auto_message_type || message.auto_message_type || '');
  return autoType.startsWith('multi_agent_');
}

function multiAgentHeaderLabel(message: any): string {
  const displayName =
    message?.metadata?.multi_agent_display_name || message?.multi_agent_display_name;
  if (displayName) {
    return displayName;
  }
  const parsed = parseMultiAgentMessage(message?.content || '');
  return parsed?.displayName || '子智能体';
}

function isContinuousMultiAgentMessage(message: any, index: number): boolean {
  if (!isMultiAgentMessage(message)) {
    return false;
  }
  const messages = getFilteredMessagesSafe();
  const prev = messages[index - 1];
  if (!prev || !isMultiAgentMessage(prev)) {
    return false;
  }
  return multiAgentHeaderLabel(message) === multiAgentHeaderLabel(prev);
}

function multiAgentBubbleContent(message: any): string {
  const parsed = parseMultiAgentMessage(message?.content || '');
  return (parsed?.content || message?.content || '').trim();
}

const escapeUserHtml = (value: string): string =>
  String(value || '')
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');

const USER_SKILL_LINK_RE = /\[\$([^\]\n]+)\]\(([^)\n]*\/\.astrion\/skills\/[^)\n]+\/SKILL\.md)\)/g;
const USER_FILE_LINK_RE = /\[([^\]\n]+)\]\(file:\/\/([^)\n]+)\)/g;
const SUB_AGENT_DONE_LABEL_RE = /^子智能体\d+\s*任务完成$/;
const SUB_AGENT_DONE_PREFIX_RE = /^(?:✅\s*)?子智能体\s*#?\s*(\d+)\s*任务摘要[:：]/;
const BG_RUN_COMMAND_DONE_LABEL_RE = /^(?:\[)?后台\s*run_command\s*完成(?:\])?$/;
const RENDERABLE_ACTION_TYPES = new Set([
  'thinking',
  'text',
  'tool',
  'append',
  'append_payload',
  'system'
]);

function renderUserMessageContent(content: string): string {
  const source = String(content || '');
  const combinedRe = new RegExp(
    `(?:${USER_SKILL_LINK_RE.source})|(?:${USER_FILE_LINK_RE.source})`,
    'g'
  );
  let html = '';
  let cursor = 0;
  let match: RegExpExecArray | null;
  while ((match = combinedRe.exec(source))) {
    html += escapeUserHtml(source.slice(cursor, match.index));
    if (match[1] !== undefined) {
      html += ` <span class="user-skill-link">${escapeUserHtml(match[1] || '')}</span> `;
    } else if (match[3] !== undefined) {
      html += ` <span class="user-file-link">${escapeUserHtml(match[3] || '')}</span> `;
    }
    cursor = match.index + match[0].length;
  }
  html += escapeUserHtml(source.slice(cursor));
  return html;
}
const isHiddenUserMessage = (message: any) => {
  if (!message || message.role !== 'user') {
    return false;
  }
  return getMessageVisibility(message) === 'hidden';
};
const isEmptyAssistantMessage = (message: any) => {
  if (!message || message.role !== 'assistant') {
    return false;
  }
  const actions = Array.isArray(message.actions) ? message.actions : [];
  const hasRenderable = actions.some((action) => isActionVisible(action));
  if (hasRenderable) {
    return false;
  }
  if (message.awaitingFirstContent) {
    return false;
  }
  // 无可渲染 action 且不在等待首包、也不在流式中的 assistant 视为“空壳”并过滤。
  const streaming = !!(message.streaming || message.currentStreamingType || message.streamingText || message.streamingThinking);
  return !streaming;
};

const filteredMessages = computed(() => {
  const source = props.messages || [];
  const droppedRoleSystem = source.filter((m) => m && m.role === 'system');
  if (droppedRoleSystem.length > 0) {
    console.log('[DEBUG_SYSTEM][render] filteredMessages 过滤了 role=system 消息', {
      count: droppedRoleSystem.length,
      samples: droppedRoleSystem.slice(0, 3)
    });
  }
  const result = source.filter((m) => {
    if (m && m.metadata && m.metadata.system_injected_image) {
      return false;
    }
    if (m?.role === 'system') {
      return false;
    }
    if (isHiddenUserMessage(m)) {
      return false;
    }
    if (isEmptyAssistantMessage(m)) {
      userMDebug('ChatArea.filteredMessages:drop-empty-assistant', {
        actions: Array.isArray(m?.actions) ? m.actions.map((a: any) => a?.type) : [],
        awaitingFirstContent: !!m?.awaitingFirstContent
      });
      return false;
    }
    return true;
  });
  if (source.length !== result.length) {
    userMDebug('ChatArea.filteredMessages:dropped', {
      sourceLength: source.length,
      resultLength: result.length,
      dropped: source.length - result.length
    });
  }
  return result;
});
const getFilteredMessagesSafe = () =>
  Array.isArray(filteredMessages.value) ? filteredMessages.value : [];
const latestMessageIndex = computed(() => getFilteredMessagesSafe().length - 1);
const nowMs = ref(Date.now());
let timerHandle: number | null = null;
const debugLoggedSystemActionKeys = new Set<string>();
const debugLoggedUnknownActionKeys = new Set<string>();
const CHAT_DEBUG_LOGS = false;
const userMDebug = (...args: any[]) => {
  if (!CHAT_DEBUG_LOGS) {
    return;
  }
  console.log('[USERMDEBUG]', ...args);
};
const {
  scrollRef,
  contentRef,
  isAtBottom,
  isNearBottom,
  escapedFromLock,
  scrollToBottom,
  stopScroll
} = useStickToBottom({
  resize: {
    damping: 0.5,
    stiffness: 0.28,
    mass: 0.75
  },
  initial: 'instant'
});

// 公式渲染后如果已经在底部附近，主动追底，避免 KaTeX 渲染导致的高度跳变让 stick-to-bottom 锁不住
provide('mathRenderedCallback', () => {
  if (isNearBottom.value && stickScrollToBottom) {
    requestAnimationFrame(() => {
      stickScrollToBottom({ animation: 'instant', preserveScrollPosition: false });
    });
  }
});

const rootEl = scrollRef;
const { anchorBlockElement, stopAll: stopBlockExpansionAnchors } = useBlockExpansionAnchor(
  scrollRef,
  { stopScroll }
);
const thinkingRefs = new Map<string, HTMLElement | null>();
let scrollbarResizeObserver: ResizeObserver | null = null;
let bounceTraceCount = 0;
const BOUNCE_TRACE_MAX = 240;
const bounceTraceLastTsByKey = new Map<string, number>();
let lastObservedTop = 0;
let lastObservedHeight = 0;
let lastUserDownScrollTs = 0;
let lastUserWheelUpTs = 0;
let lastProgrammaticHintTs = 0;
let lastProgrammaticHintSource = '';
let suppressUserIntentUntil = 0;
let isHandlingLargeGrowth = false;
let scrollListener: ((event: Event) => void) | null = null;
let wheelListener: ((event: WheelEvent) => void) | null = null;
let traceAttachLogged = false;

function isScrollBounceTraceEnabled() {
  if (typeof window === 'undefined') return true;
  try {
    const explicit = (window as any).__SCROLL_BOUNCE_TRACE__;
    if (explicit === false || explicit === '0') return false;
    if (explicit === true || explicit === '1') return true;
    const localFlag = window.localStorage?.getItem('scrollBounceTrace');
    if (localFlag === '0' || localFlag === 'false') return false;
    if (localFlag === '1' || localFlag === 'true') return true;
    return true;
  } catch {
    return true;
  }
}

function getScrollMetrics(el: HTMLElement) {
  const top = el.scrollTop;
  const height = el.scrollHeight;
  const client = el.clientHeight;
  return {
    top,
    height,
    client,
    remain: height - top - client,
    hasShowHtml: !!el.querySelector(
      'show_html, show-html, .chat-inline-html, .chat-inline-card--html'
    )
  };
}

function bounceTraceLog(
  event: string,
  payload: Record<string, any> = {},
  key = event,
  throttleMs = 120
) {
  if (!isScrollBounceTraceEnabled()) return;
  if (bounceTraceCount >= BOUNCE_TRACE_MAX) return;
  const now = Date.now();
  const last = bounceTraceLastTsByKey.get(key) || 0;
  if (throttleMs > 0 && now - last < throttleMs) return;
  bounceTraceLastTsByKey.set(key, now);
  bounceTraceCount += 1;
  if (bounceTraceCount === BOUNCE_TRACE_MAX) {
    console.warn('[SCROLL_BOUNCE_TRACE]', 'log-limit-reached', { max: BOUNCE_TRACE_MAX });
    return;
  }
  console.log('[SCROLL_BOUNCE_TRACE]', event, payload);
}

function markProgrammaticHint(source: string) {
  lastProgrammaticHintTs = Date.now();
  lastProgrammaticHintSource = source;
}

function detachBounceListener() {
  const el = scrollRef.value;
  if (el && scrollListener) {
    el.removeEventListener('scroll', scrollListener as EventListener);
  }
  if (el && wheelListener) {
    el.removeEventListener('wheel', wheelListener as EventListener, true);
  }
  scrollListener = null;
  wheelListener = null;
}

function attachBounceListener() {
  const el = scrollRef.value;
  if (!el) return;
  if (scrollListener) {
    el.removeEventListener('scroll', scrollListener as EventListener);
  }
  if (wheelListener) {
    el.removeEventListener('wheel', wheelListener as EventListener, true);
  }
  lastObservedTop = el.scrollTop || 0;
  lastObservedHeight = el.scrollHeight || 0;
  wheelListener = (event: WheelEvent) => {
    const target = scrollRef.value;
    if (!target) return;
    const trusted = (event as any).isTrusted === true;
    const deltaY = Number(event.deltaY || 0);
    if (!(trusted && deltaY < -1 && Date.now() > suppressUserIntentUntil)) {
      return;
    }
    const now = Date.now();
    lastUserWheelUpTs = now;
    stopScroll();
    stopBlockExpansionAnchors();
    emit('user-scroll-intent', {
      ts: now,
      delta: deltaY,
      top: target.scrollTop || 0
    });
    bounceTraceLog(
      'user-scroll-intent:wheel-up',
      { deltaY, top: target.scrollTop || 0, ...getScrollMetrics(target) },
      'user-scroll-intent:wheel-up',
      80
    );
  };
  el.addEventListener('wheel', wheelListener, { passive: true, capture: true });
  scrollListener = (event: Event) => {
    const target = event.target as HTMLElement | null;
    if (!target) return;
    const trusted = (event as any).isTrusted === true;
    const top = target.scrollTop;
    const delta = top - lastObservedTop;
    const height = target.scrollHeight || 0;
    const heightDelta = height - lastObservedHeight;
    lastObservedTop = top;
    lastObservedHeight = height;

    if (heightDelta !== 0) {
      console.log('[ChatAreaScrollDebug] height-change', {
        heightDelta,
        scrollHeight: height,
        top,
        delta,
        escapedFromLock: escapedFromLock.value,
        isAtBottom: isAtBottom.value,
        isNearBottom: isNearBottom.value,
      });
    }

    // 大幅高度增长检测：当 scrollHeight 突增（内容大幅展开/渲染），
    // 且用户没有主动脱离锁定（escapedFromLock=false）时，
    // 用 instant 瞬间追底，绕过 spring 动画的延迟。
    // 这解决了思考块展开、Markdown 表格渲染等场景下滚动跟不上内容增长的问题。
    const largeGrowthThreshold = Math.max(200, target.clientHeight * 0.35 || 200);
    if (
      !isHandlingLargeGrowth &&
      heightDelta > largeGrowthThreshold &&
      !escapedFromLock.value
    ) {
      console.log('[ChatAreaScrollDebug] TRIGGER:large-growth-scroll', {
        heightDelta,
        threshold: largeGrowthThreshold,
        top,
        delta,
        escapedFromLock: escapedFromLock.value,
        isAtBottom: isAtBottom.value,
        isNearBottom: isNearBottom.value,
      });
      isHandlingLargeGrowth = true;
      markProgrammaticHint('ChatArea.largeGrowth');
      suppressUserIntentUntil = Date.now() + 900;
      scrollToBottom({ animation: 'instant', preserveScrollPosition: false });
      bounceTraceLog(
        'large-growth:instant-scroll',
        { heightDelta, threshold: largeGrowthThreshold, ...getScrollMetrics(target) },
        'large-growth:instant-scroll',
        120
      );
      setTimeout(() => {
        isHandlingLargeGrowth = false;
      }, 400);
    }

    const now = Date.now();
    const closeToProgrammaticHint = now - lastProgrammaticHintTs <= 140;
    const likelyLayoutDriven = Math.abs(heightDelta) > 2;
    const manualUpByWheel = delta < -3 && now - lastUserWheelUpTs <= 240;
    const strongManualUp = delta < -18 && now > suppressUserIntentUntil && !closeToProgrammaticHint;
    const shouldTreatAsUserIntent =
      trusted &&
      Math.abs(delta) > 5 &&
      now > suppressUserIntentUntil &&
      (!closeToProgrammaticHint || strongManualUp) &&
      (delta < 0 ? strongManualUp || manualUpByWheel || !likelyLayoutDriven : !likelyLayoutDriven);
    if (shouldTreatAsUserIntent) {
      // 用户手动滚动时，立即中断 stick 引擎中可能仍在运行的动画滚动
      stopScroll();
      stopBlockExpansionAnchors();
      lastUserDownScrollTs = now;
      emit('user-scroll-intent', {
        ts: lastUserDownScrollTs,
        delta,
        top
      });
      bounceTraceLog(
        'user-scroll-intent',
        {
          delta,
          top,
          ts: lastUserDownScrollTs,
          closeToProgrammaticHint,
          likelyLayoutDriven,
          manualUpByWheel,
          strongManualUp,
          ...getScrollMetrics(target)
        },
        'user-scroll-intent',
        120
      );
    }
    if (delta < -8) {
      const reboundWindow = now - lastUserDownScrollTs <= 1400;
      if (reboundWindow) {
        const byProgrammatic = now - lastProgrammaticHintTs <= 1200;
        bounceTraceLog(
          'rebound:detected',
          {
            delta,
            trusted,
            likelySource: byProgrammatic
              ? lastProgrammaticHintSource || 'programmatic-unknown'
              : 'unknown(internal-stick/browser-anchor)',
            msSinceUserDown: now - lastUserDownScrollTs,
            msSinceProgrammatic: now - lastProgrammaticHintTs,
            ...getScrollMetrics(target),
            stickState: {
              isAtBottom: !!isAtBottom.value,
              isNearBottom: !!isNearBottom.value,
              escapedFromLock: !!escapedFromLock.value
            }
          },
          'rebound:detected',
          0
        );
      }
    } else {
      bounceTraceLog(
        'scroll:event',
        {
          delta,
          trusted,
          ...getScrollMetrics(target),
          stickState: {
            isAtBottom: !!isAtBottom.value,
            isNearBottom: !!isNearBottom.value,
            escapedFromLock: !!escapedFromLock.value
          }
        },
        'scroll:event',
        180
      );
    }
  };
  el.addEventListener('scroll', scrollListener, { passive: true });
  if (!traceAttachLogged) {
    traceAttachLogged = true;
    console.warn('[SCROLL_BOUNCE_TRACE]', 'listener-attached', {
      hasElement: !!el,
      className: el.className || ''
    });
  }
}

function updateChatScrollbarWidth() {
  const el = scrollRef.value;
  if (!el) return;
  const scrollbarWidth = Math.max(0, el.offsetWidth - el.clientWidth);
  el.style.setProperty('--chat-scrollbar-width', `${scrollbarWidth}px`);
}

watch(
  [isAtBottom, isNearBottom, escapedFromLock],
  ([atBottom, nearBottom, escaped]) => {
    emit('stick-state-change', {
      isAtBottom: !!atBottom,
      isNearBottom: !!nearBottom,
      escapedFromLock: !!escaped
    });
  },
  { immediate: true }
);

function escapeBlockSelector(value: string) {
  if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') {
    return CSS.escape(value);
  }
  return value.replace(/["\\]/g, '\\$&');
}

const prevExpandedBlocks = ref(new Set<string>());

watch(
  () => props.expandedBlocks,
  (newSet, oldSet) => {
    const prev = oldSet ?? prevExpandedBlocks.value;
    const all = new Set([...Array.from(newSet), ...Array.from(prev)]);
    const changed = new Set<string>();
    for (const id of all) {
      if (newSet.has(id) !== prev.has(id)) {
        changed.add(id);
      }
    }
    prevExpandedBlocks.value = new Set(newSet);

    if (changed.size === 0) return;

    // 保守策略：运行期间保持旧逻辑，由 stick-to-bottom 固定向下展开并向上滚动
    if (props.streamingMessage) return;

    const container = scrollRef.value;
    if (!container) return;

    for (const id of changed) {
      const el = container.querySelector(
        `[data-block-id="${escapeBlockSelector(id)}"]`
      ) as HTMLElement | null;
      if (el) {
        anchorBlockElement(el, id, {
          direction: 'auto',
          duration: 260,
          phase: newSet.has(id) ? 'expand' : 'collapse'
        });
      }
    }
  },
  { flush: 'post' }
);

function handleStackedMoreToggle(payload: {
  element: HTMLElement;
  expanded: boolean;
  stackKey: string;
}) {
  // 保守策略：运行期间保持旧逻辑
  if (props.streamingMessage) return;

  anchorBlockElement(payload.element, `more-${payload.stackKey}`, {
    direction: 'auto',
    duration: 260,
    phase: expanded ? 'expand' : 'collapse'
  });
}

function handleMinimalGroupToggle(payload: { groupId: string; expanded: boolean }) {
  // 保守策略：运行期间保持旧逻辑
  if (props.streamingMessage) return;

  const { groupId, expanded } = payload;
  const container = scrollRef.value;
  if (!container) return;
  const el = container.querySelector(
    `[data-group-id="${escapeBlockSelector(groupId)}"]`
  ) as HTMLElement | null;
  if (el) {
    anchorBlockElement(el, groupId, {
      direction: 'auto',
      duration: 300,
      phase: expanded ? 'expand' : 'collapse'
    });
  }
}

const registerCollapseContent = (key: string, el: Element | null) => {
  if (!(el instanceof HTMLElement)) {
    return;
  }
  const COLLAPSE_MAX_HEIGHT = 600;
  requestAnimationFrame(() => {
    const h = el.scrollHeight || el.offsetHeight || 0;
    if (h > 0) {
      el.style.setProperty('--collapse-max', `${Math.min(h, COLLAPSE_MAX_HEIGHT)}px`);
    }
  });
};

function registerThinkingRef(key: string, el: Element | null) {
  if (el instanceof HTMLElement) {
    thinkingRefs.set(key, el);
  } else {
    thinkingRefs.delete(key);
  }
}

function getThinkingRef(key: string) {
  return thinkingRefs.get(key) || null;
}

async function stickScrollToBottom(
  options: {
    behavior?: ScrollBehavior;
    force?: boolean;
    preserveScrollPosition?: boolean;
  } = {}
) {
  const el = scrollRef.value;
  if (el) {
    console.log('[ChatAreaScrollDebug] CALL:stickScrollToBottom', {
      behavior: options.behavior || 'auto',
      force: !!options.force,
      preserveScrollPosition: !!options.preserveScrollPosition,
      isAtBottom: isAtBottom.value,
      isNearBottom: isNearBottom.value,
      escapedFromLock: escapedFromLock.value,
      ...getScrollMetrics(el),
    });
    bounceTraceLog(
      'programmatic:scrollToBottom:before',
      {
        source: 'ChatArea.stickScrollToBottom',
        behavior: options.behavior || 'auto',
        force: !!options.force,
        preserveScrollPosition: !!options.preserveScrollPosition,
        ...getScrollMetrics(el)
      },
      'programmatic:scrollToBottom:before',
      80
    );
  }
  markProgrammaticHint('ChatArea.stickScrollToBottom');
  // 点击“滚动到底部”按钮时会触发 smooth 动画滚动；期间会产生 trusted scroll 事件，
  // 若不短暂抑制会被误判为“用户手动滚动”从而中断动画，表现为只滚动一点点。
  if (options.force || options.behavior === 'smooth') {
    suppressUserIntentUntil = Date.now() + 900;
  }
  return await scrollToBottom({
    animation: options.behavior === 'smooth' ? 'smooth' : 'auto',
    ignoreEscapes: !!options.force,
    preserveScrollPosition: !!options.preserveScrollPosition,
    duration: options.force ? 260 : 0
  });
}

async function stickConditionalScrollToBottom(options: { force?: boolean } = {}) {
  const el = scrollRef.value;
  if (el) {
    bounceTraceLog(
      'programmatic:conditional:before',
      {
        source: 'ChatArea.stickConditionalScrollToBottom',
        force: !!options.force,
        ...getScrollMetrics(el)
      },
      'programmatic:conditional:before',
      100
    );
  }
  markProgrammaticHint('ChatArea.stickConditionalScrollToBottom');
  if (options.force) {
    return await stickScrollToBottom({ force: true, preserveScrollPosition: false });
  }
  // conditional 场景由上层先判断“是否需要追底”，这里不要再使用 preserveScrollPosition，
  // 否则一旦高度突增导致 escapedFromLock=true，后续会被卡在非底部。
  return await stickScrollToBottom({ preserveScrollPosition: false });
}

function getStickState() {
  return {
    isAtBottom: !!isAtBottom.value,
    isNearBottom: !!isNearBottom.value,
    escapedFromLock: !!escapedFromLock.value
  };
}

function iconStyleSafe(key: string, size?: string) {
  if (typeof props.iconStyle === 'function') {
    return props.iconStyle(key, size);
  }
  return {};
}

function normalizeMediaPath(input: any): string {
  if (!input) return '';
  if (typeof input === 'string') {
    return input;
  }
  if (typeof input?.path === 'string') {
    return input.path;
  }
  if (typeof input?.source_path === 'string') {
    return input.source_path;
  }
  return '';
}

function formatImageName(input: any): string {
  const path = normalizeMediaPath(input);
  if (!path) {
    if (typeof input?.name === 'string' && input.name) {
      return input.name;
    }
    if (typeof input?.title === 'string' && input.title) {
      return input.title;
    }
    if (typeof input?.media_id === 'string' && input.media_id) {
      return input.media_id;
    }
    return '';
  }
  const parts = path.split(/[/\\]/);
  return parts[parts.length - 1] || path;
}

function resolveMessageMediaRef(message: any, input: any, kind: 'image' | 'video'): any | null {
  const refs = message?.media_refs || message?.metadata?.media_refs || [];
  if (!Array.isArray(refs) || !refs.length) {
    return null;
  }
  const mediaId = typeof input?.media_id === 'string' ? input.media_id : '';
  if (mediaId) {
    const matched = refs.find((item: any) => item?.media_id === mediaId);
    if (matched) return matched;
  }
  const sourcePath = normalizeMediaPath(input);
  if (sourcePath) {
    const matched = refs.find(
      (item: any) =>
        item &&
        item.kind === kind &&
        typeof item.source_path === 'string' &&
        item.source_path === sourcePath
    );
    if (matched) return matched;
  }
  const firstSameKind = refs.find((item: any) => item && item.kind === kind);
  return firstSameKind || null;
}

function getPreviewUrl(message: any, input: any, kind: 'image' | 'video'): string {
  const mediaRef = resolveMessageMediaRef(message, input, kind);
  const mediaId = typeof mediaRef?.media_id === 'string' ? mediaRef.media_id : '';
  if (mediaId) {
    return `/api/conversations/media/${encodeURIComponent(mediaId)}`;
  }
  const path = normalizeMediaPath(input);
  if (!path) return '';
  return `/api/gui/files/download?path=${encodeURIComponent(path)}`;
}

function mediaPreviewKey(message: any, input: any, index: number): string {
  const mediaRef =
    resolveMessageMediaRef(message, input, 'image') ||
    resolveMessageMediaRef(message, input, 'video');
  if (typeof mediaRef?.media_id === 'string' && mediaRef.media_id) {
    return `${mediaRef.media_id}-${index}`;
  }
  const path = normalizeMediaPath(input);
  if (path) return `${path}-${index}`;
  return `media-${index}`;
}

function formatDurationMs(durationMs: number): string {
  const totalSeconds = Math.max(0, Math.floor((durationMs || 0) / 1000));
  const hours = Math.floor(totalSeconds / 3600);
  const minutes = Math.floor((totalSeconds % 3600) / 60);
  const seconds = totalSeconds % 60;
  if (hours > 0) {
    return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
  }
  return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
}

function getSubAgentSystemNoticeLabel(action: any): string | null {
  if (!action || action.type !== 'system') {
    return null;
  }
  const content = typeof action.content === 'string' ? action.content.trim() : '';
  const key = action?.id || `no-id-${content.slice(0, 32)}`;
  if (!content) {
    if (!debugLoggedSystemActionKeys.has(`${key}-empty`)) {
      console.log('[DEBUG_SYSTEM][render] system action 内容为空', { action });
      debugLoggedSystemActionKeys.add(`${key}-empty`);
    }
    return null;
  }
  if (
    action.variant === 'sub_agent_done' &&
    (SUB_AGENT_DONE_LABEL_RE.test(content) ||
      BG_RUN_COMMAND_DONE_LABEL_RE.test(content))
  ) {
    if (!debugLoggedSystemActionKeys.has(`${key}-variant-pass`)) {
      console.log('[DEBUG_SYSTEM][render] 命中 variant 渲染', { action, label: content });
      debugLoggedSystemActionKeys.add(`${key}-variant-pass`);
    }
    return content;
  }
  const m = content.match(SUB_AGENT_DONE_PREFIX_RE);
  if (m && /已完成/.test(content)) {
    if (!debugLoggedSystemActionKeys.has(`${key}-regex-pass`)) {
      console.log('[DEBUG_SYSTEM][render] 命中 regex 渲染', {
        action,
        normalized: `子智能体${m[1]} 任务完成`
      });
      debugLoggedSystemActionKeys.add(`${key}-regex-pass`);
    }
    return `子智能体${m[1]} 任务完成`;
  }
  if (!debugLoggedSystemActionKeys.has(`${key}-blocked`)) {
    console.log('[DEBUG_SYSTEM][render] system action 被渲染层拦截', {
      action,
      regex: SUB_AGENT_DONE_PREFIX_RE.toString(),
      completed: /已完成/.test(content)
    });
    debugLoggedSystemActionKeys.add(`${key}-blocked`);
  }
  return null;
}

function isActionVisible(action: any): boolean {
  if (!action) {
    return false;
  }
  const actionType = action.type;
  if (!RENDERABLE_ACTION_TYPES.has(actionType)) {
    const key = action?.id || `unknown-${actionType || 'none'}`;
    if (!debugLoggedUnknownActionKeys.has(key)) {
      console.log('[DEBUG_SYSTEM][render] 未知 action.type，已跳过渲染', {
        actionType,
        action
      });
      debugLoggedUnknownActionKeys.add(key);
    }
    return false;
  }
  if (action.type === 'tool') {
    const tool = action?.tool || {};
    const hasName = typeof tool.name === 'string' && tool.name.trim().length > 0;
    const hasMessage = typeof tool.message === 'string' && tool.message.trim().length > 0;
    const hasResult = tool.result !== null && tool.result !== undefined;
    const hasArgs = tool.arguments && typeof tool.arguments === 'object' && Object.keys(tool.arguments).length > 0;
    const visible = hasName || hasMessage || hasResult || hasArgs;
    return visible;
  }
  if (action.type !== 'system') {
    return true;
  }
  return !!getSubAgentSystemNoticeLabel(action);
}

function hasRenderableAssistantActions(actions: any[] = []): boolean {
  return (actions || []).some((action) => isActionVisible(action));
}

function isSubAgentNoticeOnlyMessage(msg: any): boolean {
  if (!msg || msg.role !== 'assistant') {
    return false;
  }
  const visibleActions = (msg.actions || []).filter((action: any) => isActionVisible(action));
  if (visibleActions.length !== 1) {
    return false;
  }
  const onlyAction = visibleActions[0];
  const yes = onlyAction?.type === 'system' && !!getSubAgentSystemNoticeLabel(onlyAction);
  if (yes) {
    userMDebug('ChatArea.isSubAgentNoticeOnlyMessage:hit', {
      actionId: onlyAction?.id,
      content: onlyAction?.content
    });
  }
  return yes;
}

function isFollowedBySubAgentNotice(index: number): boolean {
  const messages = getFilteredMessagesSafe();
  const next = messages[index + 1];
  const yes = !!next && isSubAgentNoticeOnlyMessage(next);
  if (yes) {
    userMDebug('ChatArea.isFollowedBySubAgentNotice:hit', {
      index,
      currentRole: messages[index]?.role,
      nextRole: next?.role
    });
  }
  return yes;
}

function assistantWorkLabel(index: number): string {
  if (index <= 0) {
    return '';
  }
  const workUser = findAssistantHeaderAnchorUser(index);
  const timer = workUser?.metadata?.work_timer;
  if (!timer || !timer.started_at) {
    return '';
  }
  const startMs = Date.parse(timer.started_at);
  if (!Number.isFinite(startMs)) {
    return '';
  }
  const status = timer.status || 'working';
  if (status === 'working') {
    return `工作中 ${formatDurationMs(nowMs.value - startMs)}`;
  }
  const durationMs =
    typeof timer.duration_ms === 'number'
      ? timer.duration_ms
      : Math.max(
          0,
          (Number.isFinite(Date.parse(timer.finished_at || ''))
            ? Date.parse(timer.finished_at)
            : nowMs.value) - startMs
        );
  return `工作完成 ${formatDurationMs(durationMs)}`;
}

function shouldShowAssistantHeader(index: number): boolean {
  return !!findAssistantHeaderAnchorUser(index);
}

function findAssistantHeaderAnchorUser(index: number): any | null {
  if (index <= 0) {
    return null;
  }
  const messages = getFilteredMessagesSafe();
  const prev = messages[index - 1];
  if (!prev || prev.role !== 'user') {
    return null;
  }
  // assistant 头部属于“紧挨着它上方的一组连续 user-side 消息”。
  // 只有 starts_work=true 的消息才会开启新 work segment；运行期 guidance
  // 只是当前 segment 内的补充，不会重置头部与计时器。
  for (let i = index - 1; i >= 0; i -= 1) {
    const item = messages[i];
    if (!item || item.role !== 'user') {
      break;
    }
    if (messageStartsWork(item)) {
      return item;
    }
  }
  return null;
}

function userHeaderSource(msg: any): string {
  return String(msg?.metadata?.message_source || 'user')
    .trim()
    .toLowerCase();
}

function userHeaderLabel(msg: any): string {
  const source = userHeaderSource(msg);
  if (source === 'guidance') {
    return '引导';
  }
  if (source === 'goal') {
    return '目标';
  }
  if (source === 'goal_review') {
    return '审核';
  }
  if (source === 'compression' || source === 'compression_handoff') {
    return '压缩';
  }
  if (source === 'notify' || source === 'sub_agent' || source === 'background_command') {
    return '通知';
  }
  return userName.value;
}

function userHeaderIconKey(msg: any): string {
  const source = userHeaderSource(msg);
  if (source === 'guidance') {
    return 'navigation';
  }
  if (source === 'goal') {
    return 'flag';
  }
  if (source === 'goal_review') {
    return 'circleAlert';
  }
  if (source === 'compression' || source === 'compression_handoff') {
    return 'layers';
  }
  if (source === 'notify' || source === 'sub_agent' || source === 'background_command') {
    return 'bell';
  }
  return 'user';
}

// ---- 简略信息渲染（compact 消息可选显示形式）----
// 命中条件：当前消息为 compact 可见性，且用户在个人空间选择了“简略信息”。
function isBriefCompactMessage(msg: any): boolean {
  if (!msg || msg.role !== 'user') {
    return false;
  }
  if (compactMessageDisplay.value !== 'brief') {
    return false;
  }
  return getMessageVisibility(msg) === 'compact';
}

// 简略信息只展示一行概要标签（不显示原始内容），按来源给出固定文案。
function compactBriefLabel(msg: any): string {
  const source = userHeaderSource(msg);
  switch (source) {
    case 'goal_review':
      return '目标审核完成';
    case 'sub_agent':
      return '后台子智能体完成';
    case 'background_command':
      return '后台指令完成';
    case 'compression':
    case 'compression_handoff':
      return '对话压缩完成';
    case 'goal':
      return '目标进行中';
    case 'guidance':
      return '运行中引导';
    case 'notify':
      return '系统通知';
    default:
      return '系统消息';
  }
}

watch(
  () =>
    getFilteredMessagesSafe().map((m) => m?.id ?? m?.content?.slice(0, 80)),
  () => {
    userBubbleTransitionSuppressed.value = true;
    // 先同步估算折叠状态，再异步精确测量，避免长气泡先展开后收起
    preMeasureUserBubbles();
    nextTick(() => {
      requestAnimationFrame(() => {
        measureUserBubbles();
        observeUserBubbles();
        releaseUserBubbleTransitionAfterLayout();
      });
    });
  },
  { immediate: true }
);

onMounted(() => {
  attachBounceListener();
  updateChatScrollbarWidth();
  if (typeof ResizeObserver !== 'undefined' && scrollRef.value) {
    scrollbarResizeObserver = new ResizeObserver(updateChatScrollbarWidth);
    scrollbarResizeObserver.observe(scrollRef.value);
  }
  if (typeof ResizeObserver !== 'undefined') {
    userBubbleResizeObserver = new ResizeObserver(() => {
      requestAnimationFrame(measureUserBubbles);
    });
    nextTick(() => observeUserBubbles());
  }
  console.warn('[SCROLL_BOUNCE_TRACE]', 'mounted', {
    hasScrollRef: !!scrollRef.value
  });
  timerHandle = window.setInterval(() => {
    nowMs.value = Date.now();
  }, 1000);
});

watch(scrollRef, () => {
  attachBounceListener();
  updateChatScrollbarWidth();
  if (scrollbarResizeObserver) {
    scrollbarResizeObserver.disconnect();
    scrollbarResizeObserver = null;
  }
  if (typeof ResizeObserver !== 'undefined' && scrollRef.value) {
    scrollbarResizeObserver = new ResizeObserver(updateChatScrollbarWidth);
    scrollbarResizeObserver.observe(scrollRef.value);
  }
});

onBeforeUnmount(() => {
  detachBounceListener();
  stopBlockExpansionAnchors();
  if (scrollbarResizeObserver) {
    scrollbarResizeObserver.disconnect();
    scrollbarResizeObserver = null;
  }
  if (userBubbleResizeObserver) {
    userBubbleResizeObserver.disconnect();
    userBubbleResizeObserver = null;
  }
  userBubbleRefs.clear();
  copiedBubbleTimeouts.forEach((t) => window.clearTimeout(t));
  copiedBubbleTimeouts.clear();
  if (timerHandle !== null) {
    clearInterval(timerHandle);
    timerHandle = null;
  }
});

const isStackable = (action: any) =>
  action && (action.type === 'thinking' || action.type === 'tool');
const isEmptyTextAction = (action: any) => {
  if (!action || action.type !== 'text') {
    return false;
  }
  const content = typeof action.content === 'string' ? action.content : '';
  return !content.trim();
};
const splitActionGroups = (actions: any[] = [], messageIndex = 0) => {
  const result: Array<
    | { kind: 'stack'; actions: any[]; key: string }
    | { kind: 'single'; action: any; actionIndex: number; key: string }
  > = [];
  let buffer: any[] = [];

  // 调试：记录输入
  if (CHAT_DEBUG_LOGS && actions.length > 0) {
    console.log(`[splitActionGroups] 消息 ${messageIndex}:`, {
      totalActions: actions.length,
      actionTypes: actions.map((a) => a.type),
      stackedBlocksEnabled: props.stackedBlocksEnabled
    });
  }

  const flushBuffer = () => {
    // 单个可堆叠块(thinking/tool)也走 stack 路径，避免 1↔2 块时在 single/stack
    // 两套渲染路径之间整体切换导致 StackedBlocks 重新挂载(表现为外框瞬间塌成最扁再展开)。
    // 单块时 StackedBlocks 内部 moreVisible=false，不显示「更多」头，外观由 CSS 对齐 single。
    if (buffer.length >= 1) {
      result.push({
        kind: 'stack',
        actions: buffer.slice(),
        key: `stack-${messageIndex}-${result.length}`
      });
    }
    buffer = [];
  };

  actions.forEach((action, idx) => {
    if (isEmptyTextAction(action)) {
      return;
    }
    if (isStackable(action)) {
      buffer.push(action);
    } else {
      flushBuffer();
      result.push({
        kind: 'single',
        action,
        actionIndex: idx,
        key: action.id || `single-${messageIndex}-${idx}`
      });
    }
  });
  flushBuffer();

  // 调试：记录输出
  if (CHAT_DEBUG_LOGS && actions.length > 0) {
    console.log(`[splitActionGroups] 消息 ${messageIndex} 结果:`, {
      totalGroups: result.length,
      groups: result.map((g) => ({
        kind: g.kind,
        actionsCount: g.kind === 'stack' ? g.actions.length : 1,
        actionType: g.kind === 'stack' ? g.actions[0]?.type : g.action?.type
      }))
    });
  }

  return result;
};

function getGeneratingLetters(message: any) {
  const label =
    typeof message?.generatingLabel === 'string' && message.generatingLabel.trim()
      ? message.generatingLabel.trim()
      : DEFAULT_GENERATING_TEXT;
  return Array.from(label);
}

defineExpose({
  rootEl,
  getThinkingRef,
  isUsingStickToBottom: () => true,
  getStickState,
  stopStickScroll: stopScroll,
  scrollToBottom: stickScrollToBottom,
  conditionalStickToBottom: stickConditionalScrollToBottom
});
</script>
