<template>
  <div class="messages-area messages-area--stick" ref="scrollRef">
    <div class="messages-flow" ref="contentRef">
      <div
        v-for="(msg, index) in filteredMessages"
        :key="index"
        class="message-block"
        :class="{
          'message-block--sub-agent-notice': isSubAgentNoticeOnlyMessage(msg),
          'message-block--before-sub-agent-notice': isFollowedBySubAgentNotice(index)
        }"
      >
        <div v-if="msg.role === 'user'" class="user-message">
          <div class="message-header icon-label">
            <span class="icon icon-sm" :style="iconStyleSafe(userHeaderIconKey(msg))" aria-hidden="true"></span>
            <span>{{ userHeaderLabel(msg) }}</span>
          </div>
          <div class="message-text user-bubble-text">
            <div v-if="msg.content" class="bubble-text">{{ msg.content }}</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>
        </div>
        <div v-else-if="msg.role === 'assistant'" class="assistant-message">
          <!-- 在 assistant 消息前显示 AI Assistant 头部 -->
          <!-- 只有当前一条消息是 user 且当前消息有内容时才显示 -->
          <div
            v-if="
              shouldShowAssistantHeader(index) &&
              (hasRenderableAssistantActions(msg.actions || []) || msg.awaitingFirstContent)
            "
            class="message-header icon-label"
          >
            <span class="icon icon-sm" :style="iconStyleSafe('bot')" aria-hidden="true"></span>
            <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"
              :render-markdown="renderMarkdown"
              :register-thinking-ref="registerThinkingRef"
              :handle-thinking-scroll="props.handleThinkingScroll"
            />
          </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"
                :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"
              />
              <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
                }"
              >
                <div
                  v-if="group.action?.type === 'thinking'"
                  class="collapsible-block thinking-block"
                  :class="{
                    expanded: expandedBlocks?.has(
                      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 }">
                    <div
                      v-if="group.action.streaming"
                      v-html="renderMarkdown(group.action.content, true)"
                    ></div>
                    <div v-else v-html="renderMarkdown(group.action.content, false)"></div>
                  </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}`"
                  @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
                }"
              >
                <div
                  v-if="action.type === 'thinking'"
                  class="collapsible-block thinking-block"
                  :class="{
                    expanded: expandedBlocks?.has(
                      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 }">
                    <div
                      v-if="action.streaming"
                      v-html="renderMarkdown(action.content, true)"
                    ></div>
                    <div v-else v-html="renderMarkdown(action.content, false)"></div>
                  </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}`"
                  @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}`) }"
          >
            <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>
</template>

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

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;
}>();
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 blockDisplayMode = computed(() => {
  return personalization.experiments.blockDisplayMode || 'stacked';
});
const stackedBlocksEnabled = computed(() => {
  // 堆叠模式和极简模式都使用堆叠布局
  return blockDisplayMode.value === 'stacked' || blockDisplayMode.value === 'minimal';
});
const aiAssistantName = computed(() => {
  if (!personalization.form.use_custom_names) {
    return 'AI Assistant';
  }
  return personalization.form.self_identify || 'AI Assistant';
});
const userName = computed(() => {
  if (!personalization.form.use_custom_names) {
    return '用户';
  }
  return personalization.form.user_name || '用户';
});
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
  );
};
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 (isSystemAutoUserMessage(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 latestMessageIndex = computed(() => filteredMessages.value.length - 1);
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'
]);
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 DEFAULT_GENERATING_TEXT = '生成中…';
const {
  scrollRef,
  contentRef,
  isAtBottom,
  isNearBottom,
  escapedFromLock,
  scrollToBottom,
  stopScroll
} = useStickToBottom({
  resize: {
    damping: 0.68,
    stiffness: 0.11,
    mass: 1.05
  },
  initial: 'instant'
});
const rootEl = scrollRef;
const thinkingRefs = new Map<string, HTMLElement | 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 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();
    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 top = target.scrollTop;
    const delta = top - lastObservedTop;
    const height = target.scrollHeight || 0;
    const heightDelta = height - lastObservedHeight;
    lastObservedTop = top;
    lastObservedHeight = height;
    const now = Date.now();
    const trusted = (event as any).isTrusted === true;
    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();
      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 || ''
    });
  }
}

watch(
  [isAtBottom, isNearBottom, escapedFromLock],
  ([atBottom, nearBottom, escaped]) => {
    emit('stick-state-change', {
      isAtBottom: !!atBottom,
      isNearBottom: !!nearBottom,
      escapedFromLock: !!escaped
    });
  },
  { immediate: true }
);
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) {
    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 next = filteredMessages.value[index + 1];
  const yes = !!next && isSubAgentNoticeOnlyMessage(next);
  if (yes) {
    userMDebug('ChatArea.isFollowedBySubAgentNotice:hit', {
      index,
      currentRole: filteredMessages.value[index]?.role,
      nextRole: next?.role
    });
  }
  return yes;
}

function assistantWorkLabel(index: number): string {
  if (index <= 0) {
    return '';
  }
  const prev = filteredMessages.value[index - 1];
  if (!prev || prev.role !== 'user') {
    return '';
  }
  const timer = prev?.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 {
  if (index <= 0) {
    return false;
  }
  const prev = filteredMessages.value[index - 1];
  if (!prev || prev.role !== 'user') {
    return false;
  }
  const source = String(prev?.metadata?.message_source || 'user')
    .trim()
    .toLowerCase();
  return ['user', 'presend', 'sub_agent', 'background_command'].includes(source);
}

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 === '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 === 'notify' || source === 'sub_agent' || source === 'background_command') {
    return 'bell';
  }
  return 'user';
}

onMounted(() => {
  attachBounceListener();
  console.warn('[SCROLL_BOUNCE_TRACE]', 'mounted', {
    hasScrollRef: !!scrollRef.value
  });
  timerHandle = window.setInterval(() => {
    nowMs.value = Date.now();
  }, 1000);
});

watch(scrollRef, () => {
  attachBounceListener();
});

onBeforeUnmount(() => {
  detachBounceListener();
  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 = () => {
    if (buffer.length >= 2) {
      result.push({
        kind: 'stack',
        actions: buffer.slice(),
        key: `stack-${messageIndex}-${result.length}`
      });
    } else if (buffer.length === 1) {
      const single = buffer[0];
      result.push({
        kind: 'single',
        action: single,
        actionIndex: actions.indexOf(single),
        key: single.id || `single-${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>
