<template>
  <div
    class="stacked-shell"
    ref="shell"
    :style="{
      height: `${shellHeight}px`,
      paddingTop: moreVisible ? `${moreHeight}px` : '0px'
    }"
  >
    <div
      class="stacked-more-block"
      ref="moreBlock"
      :class="{ visible: moreVisible }"
      :style="{ height: `${moreVisible ? moreHeight : 0}px` }"
      @click="toggleMore"
    >
      <img class="more-icon" src="/static/icons/align-left.svg" alt="展开" />
      <div class="more-copy">
        <span class="more-title">{{ moreTitle }}</span>
        <span class="more-desc">{{ moreDesc }}</span>
      </div>
    </div>

    <div class="stacked-viewport" :style="{ height: `${viewportHeight}px` }">
      <TransitionGroup
        name="stacked"
        tag="div"
        class="stacked-inner"
        ref="inner"
        :style="{ transform: `translateY(${innerOffset}px)` }"
        appear
      >
      <div v-for="(action, idx) in stackableActions" :key="blockKey(action, idx)" class="stacked-item">
        <div
          v-if="action.type === 'thinking'"
          class="collapsible-block thinking-block stacked-block"
          :class="{ expanded: isExpanded(action, idx), processing: action.streaming }"
        >
          <div class="collapsible-header" @click="toggleBlock(blockKey(action, idx))">
            <div class="arrow"></div>
            <div class="status-icon">
              <span class="thinking-icon" :class="{ 'thinking-animation': action.streaming }">
                <span class="icon icon-sm" :style="iconStyle('brain')" aria-hidden="true"></span>
              </span>
            </div>
            <span class="status-text">{{ action.streaming ? '正在思考...' : '思考过程' }}</span>
          </div>
          <div class="collapsible-content" :style="contentStyle(blockKey(action, idx))">
            <div
              class="content-inner thinking-content"
              :ref="el => registerThinking(blockKey(action, idx), el)"
              @scroll="handleThinkingScrollInternal(blockKey(action, idx), $event)"
              style="max-height: 240px; overflow-y: auto;"
            >
              {{ action.content }}
            </div>
          </div>
        </div>

        <div
          v-else-if="action.type === 'tool'"
          class="collapsible-block tool-block stacked-block"
          :class="{
            expanded: isExpanded(action, idx),
            processing: isToolProcessing(action),
            completed: isToolCompleted(action)
          }"
        >
          <div class="collapsible-header" @click="toggleBlock(blockKey(action, idx))">
            <div class="arrow"></div>
            <div class="status-icon">
              <span
                class="tool-icon icon icon-md"
                :class="getToolAnimationClass(action.tool)"
                :style="iconStyle(getToolIcon(action.tool))"
                aria-hidden="true"
              ></span>
            </div>
            <span class="status-text">{{ getToolStatusText(action.tool) }}</span>
            <span class="tool-desc">{{ getToolDescription(action.tool) }}</span>
          </div>
          <div class="collapsible-content" :style="contentStyle(blockKey(action, idx))">
            <div class="content-inner">
              <div v-if="action.tool?.name === 'web_search' && action.tool?.result">
                <div class="search-meta">
                  <div><strong>搜索内容：</strong>{{ action.tool.result.query || action.tool.arguments?.query }}</div>
                  <div><strong>主题：</strong>{{ formatSearchTopic(action.tool.result.filters || {}) }}</div>
                  <div><strong>时间范围：</strong>{{ formatSearchTime(action.tool.result.filters || {}) }}</div>
                  <div><strong>结果数量：</strong>{{ action.tool.result.total_results }}</div>
                </div>
                <div v-if="action.tool.result.results && action.tool.result.results.length" class="search-result-list">
                  <div v-for="item in action.tool.result.results" :key="item.url || item.index" class="search-result-item">
                    <div class="search-result-title">{{ item.title || '无标题' }}</div>
                    <div class="search-result-url">
                      <a v-if="item.url" :href="item.url" target="_blank">{{ item.url }}</a><span v-else>无可用链接</span>
                    </div>
                  </div>
                </div>
                <div v-else class="search-empty">未返回详细的搜索结果。</div>
              </div>
              <div v-else-if="action.tool?.name === 'run_python' && action.tool?.result">
                <div class="code-block">
                  <div class="code-label">代码：</div>
                  <pre><code class="language-python">{{ action.tool.result.code || action.tool.arguments?.code }}</code></pre>
                </div>
                <div v-if="action.tool.result.output" class="output-block">
                  <div class="output-label">输出：</div>
                  <pre>{{ action.tool.result.output }}</pre>
                </div>
              </div>
              <div v-else>
                <pre>{{ JSON.stringify(action.tool?.result || action.tool?.arguments, null, 2) }}</pre>
              </div>
            </div>
          </div>
          <div v-if="isToolProcessing(action)" class="progress-indicator"></div>
        </div>
      </div>
      </TransitionGroup>
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';

defineOptions({ name: 'StackedBlocks' });

const props = defineProps<{
  actions: any[];
  expandedBlocks: Set<string>;
  iconStyle: (key: string, size?: string) => Record<string, string>;
  toggleBlock: (blockId: string) => void;
  registerThinkingRef?: (key: string, el: Element | null) => void;
  handleThinkingScroll?: (blockId: string, event: Event) => void;
  getToolAnimationClass: (tool: any) => Record<string, unknown>;
  getToolIcon: (tool: any) => string;
  getToolStatusText: (tool: any) => string;
  getToolDescription: (tool: any) => string;
  formatSearchTopic: (filters: Record<string, any>) => string;
  formatSearchTime: (filters: Record<string, any>) => string;
}>();

const VISIBLE_LIMIT = 6;

const shell = ref<HTMLElement | null>(null);
const inner = ref<any>(null);
const moreBlock = ref<any>(null);

const showAll = ref(false);
const shellHeight = ref(0);
const moreHeight = ref(0);
const innerOffset = ref(0);
const viewportHeight = ref(0);
const contentHeights = ref<Record<string, number>>({});
const ANIMATION_SYNC_MS = 360;
const MORE_ANIMATION_MS = 300;
let syncRaf: number | null = null;
let syncUntil = 0;
const isMoreAnimating = ref(false);

const stackableActions = computed(() => (props.actions || []).filter((item) => item && (item.type === 'thinking' || item.type === 'tool')));

const hiddenCount = computed(() => Math.max(0, stackableActions.value.length - VISIBLE_LIMIT));
const moreVisible = computed(() => hiddenCount.value > 0 || showAll.value);
const totalSteps = computed(() => stackableActions.value.length);
const moreTitle = computed(() => (showAll.value ? '已展开全部' : '更多'));
const moreDesc = computed(() =>
  showAll.value ? `共 ${totalSteps.value} 个步骤` : `${hiddenCount.value} 个步骤折叠`
);

const blockKey = (action: any, idx: number) => action?.blockId || action?.id || `stacked-${idx}`;

const isExpanded = (action: any, idx: number) => props.expandedBlocks?.has(blockKey(action, idx));
const isExpandedById = (blockId: string) => props.expandedBlocks?.has(blockId);
const contentStyle = (blockId: string) => {
  const h = Math.max(0, contentHeights.value[blockId] || 0);
  return { maxHeight: isExpandedById(blockId) ? `${h}px` : '0px' };
};

const isToolProcessing = (action: any) => {
  const status = action?.tool?.status;
  return status === 'preparing' || status === 'running';
};

const isToolCompleted = (action: any) => action?.tool?.status === 'completed';

const toggleMore = () => {
  animateMoreToggle(!showAll.value);
};

const toggleBlock = (blockId: string) => {
  if (typeof props.toggleBlock === 'function') {
    props.toggleBlock(blockId);
    nextTick(() => runSync());
  }
};

const registerThinking = (key: string, el: Element | null) => {
  if (typeof props.registerThinkingRef === 'function') {
    props.registerThinkingRef(key, el);
  }
};

const handleThinkingScrollInternal = (blockId: string, event: Event) => {
  if (typeof props.handleThinkingScroll === 'function') {
    props.handleThinkingScroll(blockId, event);
  }
};

const resolveEl = (target: any): HTMLElement | null => {
  if (!target) return null;
  if (target instanceof HTMLElement) return target;
  if (target.$el && target.$el instanceof HTMLElement) return target.$el;
  return null;
};

const moreBaseHeight = () => {
  const moreEl = resolveEl(moreBlock.value);
  if (!moreEl) return 56;
  const label = moreEl.querySelector('.more-title') as HTMLElement | null;
  return Math.max(48, Math.ceil(label?.getBoundingClientRect().height || 56));
};

const setShellMetrics = () => {
  const innerEl = resolveEl(inner.value);
  const shellEl = resolveEl(shell.value);
  if (!shellEl || !innerEl) return;

  const children = Array.from(innerEl.children) as HTMLElement[];
  const heights: number[] = [];
  const nextContentHeights: Record<string, number> = {};

  children.forEach((el, idx) => {
    const action = stackableActions.value[idx];
    const key = blockKey(action, idx);
    const content = el.querySelector('.collapsible-content') as HTMLElement | null;
    const contentHeight = content ? Math.ceil(content.scrollHeight) : 0;
    nextContentHeights[key] = contentHeight;

    // 使用当前真实高度而非「展开/折叠」状态推导值，避免在折叠动画过程中出现高度骤减导致的闪烁
    const liveHeight = Math.ceil(el.getBoundingClientRect().height || el.offsetHeight || 0);
    heights.push(liveHeight);
  });

  contentHeights.value = nextContentHeights;

  const sum = (arr: number[]) => (arr.length ? arr.reduce((a, b) => a + b, 0) : 0);
  const totalHeight = sum(heights);
  const hiddenHeight = sum(heights.slice(0, Math.max(0, heights.length - VISIBLE_LIMIT)));
  const windowHeight = sum(heights.slice(-VISIBLE_LIMIT));

  moreHeight.value = moreVisible.value ? moreBaseHeight() : 0;

  const targetShell = moreHeight.value + (showAll.value || !moreVisible.value ? totalHeight : windowHeight);
  const targetOffset = showAll.value || !moreVisible.value ? 0 : -hiddenHeight;

  shellHeight.value = targetShell;
  innerOffset.value = targetOffset;
  viewportHeight.value = Math.max(0, targetShell - moreHeight.value);
};

const lerp = (a: number, b: number, t: number) => a + (b - a) * t;
const easeOutCubic = (t: number) => 1 - Math.pow(1 - t, 3);

const animateMoreToggle = (nextShowAll: boolean) => {
  if (isMoreAnimating.value) return;

  // 停掉其他同步，避免冲突
  if (syncRaf) {
    cancelAnimationFrame(syncRaf);
    syncRaf = null;
  }

  const innerEl = resolveEl(inner.value);
  const shellEl = resolveEl(shell.value);
  if (!innerEl || !shellEl) {
    showAll.value = nextShowAll;
    nextTick(() => runSync());
    return;
  }

  // 先计算当前状态
  setShellMetrics();
  const start = {
    shell: shellHeight.value,
    offset: innerOffset.value,
    viewport: viewportHeight.value,
    more: moreHeight.value
  };

  // 切换到目标状态并计算目标值
  showAll.value = nextShowAll;
  setShellMetrics();
  const target = {
    shell: shellHeight.value,
    offset: innerOffset.value,
    viewport: viewportHeight.value,
    more: moreHeight.value
  };

  // 回到起点数值，准备动画
  shellHeight.value = start.shell;
  innerOffset.value = start.offset;
  viewportHeight.value = start.viewport;
  moreHeight.value = start.more;

  isMoreAnimating.value = true;
  const startedAt = performance.now();

  const step = () => {
    const now = performance.now();
    const t = Math.min(1, (now - startedAt) / MORE_ANIMATION_MS);
    const eased = easeOutCubic(t);

    shellHeight.value = lerp(start.shell, target.shell, eased);
    innerOffset.value = lerp(start.offset, target.offset, eased);
    viewportHeight.value = lerp(start.viewport, target.viewport, eased);
    moreHeight.value = lerp(start.more, target.more, eased);

    if (t < 1) {
      requestAnimationFrame(step);
    } else {
      isMoreAnimating.value = false;
      shellHeight.value = target.shell;
      innerOffset.value = target.offset;
      viewportHeight.value = target.viewport;
      moreHeight.value = target.more;
      setShellMetrics();
    }
  };

  requestAnimationFrame(step);
};

const runSync = () => {
  if (isMoreAnimating.value) return;
  if (syncRaf) {
    cancelAnimationFrame(syncRaf);
    syncRaf = null;
  }
  syncUntil = performance.now() + ANIMATION_SYNC_MS;
  const step = () => {
    setShellMetrics();
    if (performance.now() < syncUntil) {
      syncRaf = requestAnimationFrame(step);
    } else {
      syncRaf = null;
    }
  };
  syncRaf = requestAnimationFrame(step);
};

let resizeObserver: ResizeObserver | null = null;
let heightRaf: number | null = null;

onMounted(() => {
  const innerEl = resolveEl(inner.value);
  setShellMetrics();
  resizeObserver = new ResizeObserver(() => {
    if (heightRaf) {
      cancelAnimationFrame(heightRaf);
    }
    heightRaf = requestAnimationFrame(() => {
      setShellMetrics();
    });
  });
  if (innerEl) {
    resizeObserver.observe(innerEl);
  }
});

onBeforeUnmount(() => {
  if (resizeObserver) {
    resizeObserver.disconnect();
    resizeObserver = null;
  }
  if (heightRaf) {
    cancelAnimationFrame(heightRaf);
    heightRaf = null;
  }
  if (syncRaf) {
    cancelAnimationFrame(syncRaf);
    syncRaf = null;
  }
});

watch([stackableActions, moreVisible], () => {
  nextTick(() => runSync());
});

watch(
  () => (props.expandedBlocks ? props.expandedBlocks.size : 0),
  () => {
    nextTick(() => runSync());
  }
);
</script>
