<template>
  <div class="flex h-full">
    <!-- 左侧主区域：待解构帖子 -->
    <div v-if="showPostTree" class="flex flex-col flex-1 min-w-0">
      <!-- 头部 -->
      <div class="flex items-center justify-between px-4 py-2 bg-base-300 text-xs text-base-content/60">
        <span>待解构帖子</span>
        <div class="flex items-center gap-2">
          <span v-if="store.highlightedNodeIds.size > 0" class="text-primary">
            已高亮 {{ store.highlightedNodeIds.size }} 个节点
          </span>
          <template v-if="showExpand">
            <button
              v-if="store.expandedPanel !== 'post-tree'"
              @click="store.expandPanel('post-tree')"
              class="btn btn-ghost btn-xs"
              title="放大"
            >⤢</button>
            <button
              v-if="store.expandedPanel !== 'default'"
              @click="store.resetLayout()"
              class="btn btn-ghost btn-xs"
              title="恢复"
            >⊡</button>
          </template>
        </div>
      </div>

      <!-- 帖子筛选列表 -->
      <div class="px-2 py-1.5 bg-base-200 border-b border-base-300 flex gap-1 overflow-x-auto">
        <button
          v-for="post in store.postList"
          :key="post.index"
          @click="selectPost(post.index)"
          class="btn btn-xs shrink-0 transition-colors"
          :class="selectedPostIdx === post.index ? 'btn-primary' : 'btn-ghost'"
          :title="post.postTitle"
        >
          {{ formatPostTitle(post) }}
        </button>
        <span v-if="store.postList.length === 0" class="text-xs text-base-content/40 px-2">暂无帖子</span>
      </div>

      <!-- SVG 容器 -->
      <div ref="containerRef" class="flex-1 overflow-hidden bg-base-100">
        <svg ref="svgRef" class="w-full h-full" @click="handleSvgClick"></svg>
      </div>
    </div>

    <!-- 右侧：匹配列表 + 详情 -->
    <div
      v-if="showMatchList || showDetail"
      class="shrink-0 bg-base-200 border-l border-base-300 flex flex-col text-xs transition-all duration-200"
      :class="showPostTree ? 'w-72' : 'flex-1'"
    >
      <!-- 匹配列表（1/3高度） -->
      <div
        v-if="showMatchList"
        class="flex flex-col border-b border-base-300 transition-all duration-200"
        :class="matchListCollapsed ? 'h-8' : (showDetail ? 'h-1/3' : 'flex-1')"
      >
        <div
          class="px-3 py-2 bg-base-300 text-base-content/60 flex items-center justify-between shrink-0 cursor-pointer"
          @click="emit('update:matchListCollapsed', !matchListCollapsed)"
        >
          <div class="flex items-center gap-2">
            <span class="transition-transform" :class="{ '-rotate-90': matchListCollapsed }">▼</span>
            <span>匹配列表</span>
          </div>
          <span class="text-base-content/40">{{ sortedMatchEdges.length }}</span>
        </div>
        <div v-show="!matchListCollapsed" class="flex-1 overflow-y-auto">
          <div
            v-for="(edge, idx) in sortedMatchEdges"
            :key="idx"
            class="px-3 py-1.5 hover:bg-base-300 cursor-pointer border-b border-base-300/50 transition-colors"
            :class="{ 'bg-primary/10': store.selectedEdgeId === getEdgeId(edge) }"
            @click="onMatchClick(edge)"
          >
            <div class="flex items-center gap-1.5">
              <!-- 源节点样式（帖子域-空心） -->
              <span
                class="w-2 h-2 shrink-0 rounded-full border-2"
                :style="{ borderColor: getSourceNodeColor(edge), backgroundColor: 'transparent' }"
              ></span>
              <span class="truncate text-base-content/80" :title="edge.sourceName">{{ edge.sourceName }}</span>
              <!-- 分数（带边颜色） -->
              <span
                class="px-1 text-[10px] font-medium shrink-0 border-t border-b"
                :style="{ color: getScoreColor(edge.score), borderColor: getScoreColor(edge.score) }"
              >{{ edge.score != null ? edge.score.toFixed(2) : '-' }}</span>
              <!-- 目标节点样式（人设域-实心） -->
              <span
                class="w-2 h-2 shrink-0 rounded-full"
                :style="{ backgroundColor: getTargetNodeColor(edge) }"
              ></span>
              <span class="truncate text-base-content/60" :title="edge.targetName">{{ edge.targetName }}</span>
            </div>
          </div>
          <div v-if="sortedMatchEdges.length === 0" class="px-3 py-4 text-base-content/40 text-center">
            暂无匹配
          </div>
        </div>
      </div>

      <!-- 详情 -->
      <div v-if="showDetail" class="flex-1 flex flex-col min-h-0">
        <div class="px-3 py-2 bg-base-300 text-base-content/60 shrink-0 flex items-center justify-between">
          <span>详情</span>
          <label class="swap swap-flip text-[10px]">
            <input type="checkbox" v-model="showRawData" />
            <span class="swap-on">JSON</span>
            <span class="swap-off">渲染</span>
          </label>
        </div>
        <div class="flex-1 overflow-y-auto p-3 space-y-3">
          <!-- 原始JSON模式 -->
          <template v-if="showRawData && (displayNode || store.selectedEdge)">
            <div class="relative">
              <button
                @click="copyJson"
                class="absolute top-1 right-1 btn btn-ghost btn-xs opacity-60 hover:opacity-100"
                :title="copySuccess ? '已复制' : '复制'"
              >
                <span v-if="copySuccess">✓</span>
                <span v-else>📋</span>
              </button>
              <pre class="text-[10px] bg-base-100 p-2 pr-8 rounded overflow-x-auto whitespace-pre-wrap break-all select-all">{{ JSON.stringify(displayNode || store.selectedEdge, null, 2) }}</pre>
            </div>
          </template>
          <!-- 渲染模式 -->
          <template v-else-if="!showRawData">
            <!-- 节点详情（hover 优先于 selected） -->
            <template v-if="displayNode">
              <div class="flex items-center gap-2">
                <!-- hover 标识 -->
                <span v-if="store.hoverNode" class="text-[10px] text-warning/60">[hover]</span>
                <!-- 节点样式：空心（帖子域）或实心（人设域） -->
                <span
                  class="w-2.5 h-2.5 shrink-0"
                  :class="displayNodeStyle.shape === 'rect' ? 'rounded-sm' : 'rounded-full'"
                  :style="displayNodeStyle.hollow
                    ? { backgroundColor: 'transparent', border: '2px solid ' + displayNodeStyle.color }
                    : { backgroundColor: displayNodeStyle.color }"
                ></span>
                <span class="text-primary font-medium truncate">{{ displayNode.name }}</span>
              </div>
              <div class="space-y-1.5 text-[11px]">
                <template v-for="(value, key) in displayNode" :key="key">
                  <template v-if="!hiddenNodeFields.includes(key) && key !== 'name' && value !== null && value !== undefined && value !== ''">
                    <div v-if="typeof value !== 'object'" class="flex justify-between gap-2">
                      <span class="text-base-content/50 shrink-0">{{ formatKey(key) }}</span>
                      <span class="text-right break-all">{{ formatValue(value) }}</span>
                    </div>
                    <div v-else-if="Object.keys(value).length > 0" class="space-y-1">
                      <span class="text-base-content/50">{{ formatKey(key) }}</span>
                      <div class="pl-2 border-l border-base-content/20 space-y-1">
                        <template v-for="(v, k) in value" :key="k">
                          <div v-if="v !== null && v !== undefined && v !== ''" class="flex justify-between gap-2 text-[10px]">
                            <span class="text-base-content/50 shrink-0">{{ formatKey(k) }}</span>
                            <span class="text-right break-all">{{ formatValue(v) }}</span>
                          </div>
                        </template>
                      </div>
                    </div>
                  </template>
                </template>
              </div>
              <!-- 入边列表 -->
              <div v-if="nodeInEdges.length > 0" class="mt-3 pt-2 border-t border-base-content/10">
                <div
                  class="text-[10px] text-base-content/50 mb-1 cursor-pointer hover:text-primary"
                  @click="openEdgeListModal('in', nodeInEdges)"
                >入边 ({{ nodeInEdges.length }}) ›</div>
                <div class="space-y-1 max-h-24 overflow-y-auto">
                  <div
                    v-for="edge in nodeInEdges"
                    :key="`in-${edge.source}-${edge.type}`"
                    class="flex items-center gap-1 text-[10px] px-1 py-0.5 rounded hover:bg-base-300 cursor-pointer"
                    @click="openEdgeModal(edge)"
                  >
                    <span class="w-2 h-0.5" :style="{ backgroundColor: edgeTypeColors[edge.type] }"></span>
                    <span class="truncate flex-1">{{ getNodeName(edge.source) }}</span>
                    <span class="text-base-content/40">{{ edge.score?.toFixed(2) || '-' }}</span>
                  </div>
                </div>
              </div>
              <!-- 出边列表 -->
              <div v-if="nodeOutEdges.length > 0" class="mt-2 pt-2 border-t border-base-content/10">
                <div
                  class="text-[10px] text-base-content/50 mb-1 cursor-pointer hover:text-primary"
                  @click="openEdgeListModal('out', nodeOutEdges)"
                >出边 ({{ nodeOutEdges.length }}) ›</div>
                <div class="space-y-1 max-h-24 overflow-y-auto">
                  <div
                    v-for="edge in nodeOutEdges"
                    :key="`out-${edge.target}-${edge.type}`"
                    class="flex items-center gap-1 text-[10px] px-1 py-0.5 rounded hover:bg-base-300 cursor-pointer"
                    @click="openEdgeModal(edge)"
                  >
                    <span class="w-2 h-0.5" :style="{ backgroundColor: edgeTypeColors[edge.type] }"></span>
                    <span class="truncate flex-1">{{ getNodeName(edge.target) }}</span>
                    <span class="text-base-content/40">{{ edge.score?.toFixed(2) || '-' }}</span>
                  </div>
                </div>
              </div>
            </template>
            <!-- 边详情（hover 优先于 selected） -->
            <template v-else-if="displayEdge">
              <div class="flex items-center gap-2">
                <span class="w-4 h-0.5 shrink-0" :style="{ backgroundColor: displayEdgeColor }"></span>
                <span class="text-secondary font-medium">{{ displayEdge.type }} 边</span>
              </div>
              <div class="space-y-1.5 text-[11px]">
                <template v-for="(value, key) in displayEdge" :key="key">
                  <template v-if="key !== 'index' && value !== null && value !== undefined && value !== ''">
                    <div v-if="typeof value !== 'object'" class="flex justify-between gap-2">
                      <span class="text-base-content/50 shrink-0">{{ formatKey(key) }}</span>
                      <span class="text-right break-all">{{ formatEdgeValue(key, value) }}</span>
                    </div>
                    <div v-else-if="Object.keys(value).length > 0" class="space-y-1">
                      <span class="text-base-content/50">{{ formatKey(key) }}</span>
                      <div class="pl-2 border-l border-base-content/20 space-y-1">
                        <template v-for="(v, k) in value" :key="k">
                          <div v-if="v !== null && v !== undefined && v !== ''" class="flex justify-between gap-2 text-[10px]">
                            <span class="text-base-content/50 shrink-0">{{ formatKey(k) }}</span>
                            <span class="text-right break-all">{{ formatValue(v) }}</span>
                          </div>
                        </template>
                      </div>
                    </div>
                  </template>
                </template>
              </div>
            </template>
            <!-- 无选中 -->
            <div v-else class="text-base-content/40 text-center py-4">
              点击节点或边查看详情
            </div>
          </template>
        </div>
      </div>
    </div>

    <!-- 边详情模态框 -->
    <dialog v-if="modalEdge" class="modal modal-open">
      <div class="modal-box max-w-md">
        <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" @click="closeEdgeModal">✕</button>
        <h3 class="font-bold text-lg flex items-center gap-2">
          <span class="w-4 h-0.5" :style="{ backgroundColor: edgeTypeColors[modalEdge.type] }"></span>
          {{ modalEdge.type }} 边
        </h3>
        <div class="py-4 space-y-2 text-sm">
          <template v-for="(value, key) in modalEdge" :key="key">
            <template v-if="key !== 'index' && value !== null && value !== undefined && value !== ''">
              <div v-if="typeof value !== 'object'" class="flex justify-between gap-2">
                <span class="text-base-content/50 shrink-0">{{ formatKey(key) }}</span>
                <span class="text-right break-all">{{ formatEdgeValue(key, value) }}</span>
              </div>
              <div v-else-if="Object.keys(value).length > 0" class="space-y-1">
                <span class="text-base-content/50">{{ formatKey(key) }}</span>
                <div class="pl-3 border-l border-base-content/20 space-y-1 text-xs">
                  <template v-for="(v, k) in value" :key="k">
                    <div v-if="k !== 'index' && v !== null && v !== undefined && v !== ''" class="flex justify-between gap-2">
                      <span class="text-base-content/50 shrink-0">{{ formatKey(k) }}</span>
                      <span class="text-right break-all">{{ formatValue(v) }}</span>
                    </div>
                  </template>
                </div>
              </div>
            </template>
          </template>
        </div>
      </div>
      <form method="dialog" class="modal-backdrop">
        <button @click="closeEdgeModal">close</button>
      </form>
    </dialog>

    <!-- 边列表模态框 -->
    <dialog v-if="edgeListModal.show" class="modal modal-open">
      <div class="modal-box max-w-2xl max-h-[80vh]">
        <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" @click="closeEdgeListModal">✕</button>
        <h3 class="font-bold text-lg">{{ edgeListModal.type === 'in' ? '入边' : '出边' }}列表 ({{ edgeListModal.edges.length }})</h3>
        <div class="py-4 overflow-y-auto max-h-[60vh]">
          <div class="space-y-3">
            <div
              v-for="(edge, idx) in edgeListModal.edges"
              :key="idx"
              class="p-3 bg-base-200 rounded-lg"
            >
              <div class="flex items-center gap-2 mb-2 pb-2 border-b border-base-300">
                <span class="w-4 h-0.5" :style="{ backgroundColor: edgeTypeColors[edge.type] }"></span>
                <span class="font-medium">{{ edge.type }}</span>
                <span class="text-base-content/50 text-sm">
                  {{ edgeListModal.type === 'in' ? getNodeName(edge.source) : getNodeName(edge.target) }}
                </span>
                <span v-if="edge.score != null" class="ml-auto text-primary">{{ edge.score.toFixed(2) }}</span>
              </div>
              <div class="space-y-1 text-sm">
                <template v-for="(value, key) in edge" :key="key">
                  <template v-if="!hiddenNodeFields.includes(key) && key !== 'type' && value !== null && value !== undefined && value !== ''">
                    <div v-if="typeof value !== 'object'" class="flex justify-between gap-2">
                      <span class="text-base-content/50 shrink-0">{{ formatKey(key) }}</span>
                      <span class="text-right break-all">{{ formatEdgeValue(key, value) }}</span>
                    </div>
                    <div v-else-if="Object.keys(value).length > 0" class="space-y-1">
                      <span class="text-base-content/50">{{ formatKey(key) }}</span>
                      <div class="pl-3 border-l border-base-content/20 space-y-1 text-xs">
                        <template v-for="(v, k) in value" :key="k">
                          <div v-if="v !== null && v !== undefined && v !== ''" class="flex justify-between gap-2">
                            <span class="text-base-content/50 shrink-0">{{ formatKey(k) }}</span>
                            <span class="text-right break-all">{{ formatValue(v) }}</span>
                          </div>
                        </template>
                      </div>
                    </div>
                  </template>
                </template>
              </div>
            </div>
          </div>
        </div>
      </div>
      <form method="dialog" class="modal-backdrop">
        <button @click="closeEdgeListModal">close</button>
      </form>
    </dialog>
  </div>
</template>

<script setup>
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
import * as d3 from 'd3'
import { useGraphStore } from '../stores/graph'
import { getNodeStyle, applyNodeShape, dimColors } from '../config/nodeStyle'
import { getEdgeStyle, edgeTypeColors } from '../config/edgeStyle'
import { applyHighlight, applyHoverHighlight } from '../utils/highlight'

const props = defineProps({
  showExpand: {
    type: Boolean,
    default: false
  },
  showPostTree: {
    type: Boolean,
    default: true
  },
  showMatchList: {
    type: Boolean,
    default: true
  },
  showDetail: {
    type: Boolean,
    default: true
  },
  matchListCollapsed: {
    type: Boolean,
    default: false
  }
})

const emit = defineEmits(['update:matchListCollapsed'])

// 不需要显示的节点字段
const hiddenNodeFields = ['index', 'x', 'y', 'vx', 'vy', 'fx', 'fy']

const store = useGraphStore()

const containerRef = ref(null)
const svgRef = ref(null)

// 详情显示模式：原始JSON / 渲染
const showRawData = ref(false)
const copySuccess = ref(false)

// 边详情模态框
const modalEdge = ref(null)

function openEdgeModal(edge) {
  modalEdge.value = edge
}

function closeEdgeModal() {
  modalEdge.value = null
}

// 边列表模态框
const edgeListModal = ref({
  show: false,
  type: 'in',  // 'in' or 'out'
  edges: []
})

function openEdgeListModal(type, edges) {
  edgeListModal.value = {
    show: true,
    type,
    edges
  }
}

function closeEdgeListModal() {
  edgeListModal.value = {
    show: false,
    type: 'in',
    edges: []
  }
}

// 复制JSON到剪贴板（详情）
function copyJson() {
  const data = store.selectedNode || store.selectedEdge
  if (!data) return
  navigator.clipboard.writeText(JSON.stringify(data, null, 2)).then(() => {
    copySuccess.value = true
    setTimeout(() => { copySuccess.value = false }, 1500)
  })
}

// 当前选中的帖子索引
const selectedPostIdx = ref(store.selectedPostIndex)

// 匹配边列表（按分数从高到低排序，去重，无分数的放最后）
const sortedMatchEdges = computed(() => {
  const postGraph = store.currentPostGraph
  if (!postGraph?.edges) return []

  const matchEdges = []
  const seen = new Set()  // 用于去重

  for (const [edgeId, edge] of Object.entries(postGraph.edges)) {
    if (edge.type === '匹配') {
      // 生成去重key（两个节点排序后拼接，确保A-B和B-A生成相同的key）
      const pairKey = [edge.source, edge.target].sort().join('|')
      if (seen.has(pairKey)) continue
      seen.add(pairKey)

      // 获取源节点和目标节点名称
      const sourceNode = postGraph.nodes?.[edge.source]
      const targetNode = store.getNode(edge.target)  // 目标是人设节点
      matchEdges.push({
        ...edge,
        sourceName: sourceNode?.name || edge.source.split(':').pop(),
        targetName: targetNode?.name || edge.target.split(':').pop()
      })
    }
  }

  // 按分数从高到低排序，无分数的放最后
  return matchEdges.sort((a, b) => {
    const aScore = a.score ?? -Infinity
    const bScore = b.score ?? -Infinity
    return bScore - aScore
  })
})

// 获取匹配边的分数颜色
function getScoreColor(score) {
  return getEdgeStyle({ type: '匹配', score }).color
}

// 获取源节点颜色（帖子域节点）
function getSourceNodeColor(edge) {
  const postGraph = store.currentPostGraph
  const sourceNode = postGraph?.nodes?.[edge.source]
  if (sourceNode?.dimension) {
    return dimColors[sourceNode.dimension] || '#888'
  }
  return '#888'
}

// 获取目标节点颜色（人设域节点）
function getTargetNodeColor(edge) {
  const targetNode = store.getNode(edge.target)
  if (targetNode?.dimension) {
    return dimColors[targetNode.dimension] || '#888'
  }
  return '#888'
}

// 显示的节点（hover 优先于 selected）
const displayNode = computed(() => store.hoverNode || store.selectedNode)

// 显示节点的样式
const displayNodeStyle = computed(() => {
  if (!displayNode.value) return { color: '#888', shape: 'circle', hollow: false }
  return getNodeStyle(displayNode.value)
})

// 选中节点的样式（兼容旧代码）
const selectedNodeStyle = computed(() => {
  if (!store.selectedNode) return { color: '#888', shape: 'circle', hollow: false }
  return getNodeStyle(store.selectedNode)
})

// 选中节点的颜色（兼容）
const selectedNodeColor = computed(() => selectedNodeStyle.value.color)

// 选中边的颜色
const selectedEdgeColor = computed(() => {
  if (!store.selectedEdge) return '#888'
  return edgeTypeColors[store.selectedEdge.type] || '#888'
})

// 显示的边（hover 优先于 selected）
const displayEdge = computed(() => {
  return store.hoverEdgeData || store.selectedEdge
})

// 显示的边颜色
const displayEdgeColor = computed(() => {
  if (!displayEdge.value) return '#888'
  return edgeTypeColors[displayEdge.value.type] || '#888'
})

// 节点的入边列表（按分数降序）
const nodeInEdges = computed(() => {
  if (!displayNode.value) return []
  const nodeId = displayNode.value.id || displayNode.value.data?.id
  if (!nodeId) return []

  const postGraph = store.currentPostGraph
  if (!postGraph?.edges) return []

  return Object.values(postGraph.edges)
    .filter(e => e.target === nodeId)
    .sort((a, b) => (b.score || 0) - (a.score || 0))
})

// 节点的出边列表（按分数降序）
const nodeOutEdges = computed(() => {
  if (!displayNode.value) return []
  const nodeId = displayNode.value.id || displayNode.value.data?.id
  if (!nodeId) return []

  const postGraph = store.currentPostGraph
  if (!postGraph?.edges) return []

  return Object.values(postGraph.edges)
    .filter(e => e.source === nodeId)
    .sort((a, b) => (b.score || 0) - (a.score || 0))
})

// 获取节点名称（根据节点ID）
function getNodeName(nodeId) {
  if (!nodeId) return '-'

  // 先从当前帖子图中查找
  const postGraph = store.currentPostGraph
  if (postGraph?.nodes?.[nodeId]) {
    return postGraph.nodes[nodeId].name || nodeId.split(':').pop()
  }

  // 再从人设节点中查找
  const personaNode = store.getNode(nodeId)
  if (personaNode) {
    return personaNode.name || nodeId.split(':').pop()
  }

  // 回退到从ID提取名称
  return nodeId.split(':').pop() || nodeId
}

// 获取边ID
function getEdgeId(edge) {
  return `${edge.source}|${edge.type}|${edge.target}`
}

// 点击匹配项
function onMatchClick(edge) {
  store.selectEdge({
    source: edge.source,
    target: edge.target,
    type: edge.type,
    score: edge.score
  })
}

// zoom 实例和主 g 元素
let zoom = null
let mainG = null
let treeWidth = 0
let treeHeight = 0

// 选择帖子
function selectPost(index) {
  selectedPostIdx.value = index
  store.selectPost(index)
}

// 格式化帖子标题（简短显示）
function formatPostTitle(post) {
  const title = post.postTitle || post.postId
  return title.length > 10 ? title.slice(0, 10) + '…' : title
}

// 格式化帖子选项显示（完整）
function formatPostOption(post) {
  const date = post.createTime ? new Date(post.createTime * 1000).toLocaleDateString() : ''
  const title = post.postTitle || post.postId
  const shortTitle = title.length > 20 ? title.slice(0, 20) + '...' : title
  return date ? `${date} ${shortTitle}` : shortTitle
}

// 节点元素映射（统一存储所有节点位置）
let nodeElements = {}
let baseNodeElements = {}  // 基础节点（帖子树+匹配层），不含游走节点
let currentRoot = null

// 处理节点点击
function handleNodeClick(event, d) {
  event.stopPropagation()
  // 锁定状态下点击节点无效果，但提醒用户
  if (store.lockedHoverNodeId) {
    shakeLockButton()
    return
  }
  store.selectNode(d)
}

// 渲染树
function renderTree() {
  const svg = d3.select(svgRef.value)
  svg.selectAll('*').remove()
  nodeElements = {}

  const treeData = store.postTreeData
  if (!treeData || !treeData.id) return

  const container = containerRef.value
  if (!container) return

  const width = container.clientWidth
  const height = container.clientHeight

  svg.attr('viewBox', `0 0 ${width} ${height}`)

  // 创建层级数据
  const root = d3.hierarchy(treeData)
  currentRoot = root

  // 智能计算树的尺寸（垂直布局）
  const allNodes = root.descendants()
  const maxDepth = d3.max(allNodes, d => d.depth)
  const leafCount = allNodes.filter(d => !d.children).length

  // 计算最长文字长度（用于动态调整间距）
  const maxTextLen = d3.max(allNodes, d => {
    const name = d.data.name || ''
    return Math.min(name.length, 10) // 最多显示10个字
  }) || 6

  // 动态计算节点间距（根据文字长度）
  const nodeSpacing = Math.max(60, maxTextLen * 12)

  // 宽度：基于叶子节点数量和文字间距
  treeWidth = Math.max(400, leafCount * nodeSpacing + 100)
  // 高度：根据深度，增大层间距避免垂直重叠
  treeHeight = Math.max(400, (maxDepth + 1) * 120 + 50)

  // 创建缩放行为
  zoom = d3.zoom()
    .scaleExtent([0.1, 3])
    .on('zoom', (e) => {
      mainG.attr('transform', e.transform)
    })

  svg.call(zoom)

  // 创建主组
  mainG = svg.append('g')

  // 创建树布局（垂直方向：从上到下）
  const treeLayout = d3.tree()
    .size([treeWidth - 100, treeHeight - 50])
    .separation((a, b) => {
      // 同级节点间距根据是否有子节点调整
      if (a.parent === b.parent) {
        // 叶子节点需要更大间距放文字
        const aIsLeaf = !a.children
        const bIsLeaf = !b.children
        if (aIsLeaf || bIsLeaf) return 1.5
        return 1
      }
      return 2
    })

  treeLayout(root)

  // 内容组（带偏移）
  const contentG = mainG.append('g')
    .attr('transform', 'translate(50, 25)')

  // 绘制边（垂直方向）
  contentG.append('g')
    .attr('class', 'tree-edges')
    .selectAll('.tree-link')
    .data(root.links())
    .join('path')
    .attr('class', 'tree-link')
    .attr('fill', 'none')
    .attr('stroke', '#3498db')
    .attr('stroke-opacity', 0.3)
    .attr('stroke-width', 1)
    .attr('d', d => {
      const midY = (d.source.y + d.target.y) / 2
      return `M${d.source.x},${d.source.y} C${d.source.x},${midY} ${d.target.x},${midY} ${d.target.x},${d.target.y}`
    })

  // 绘制节点（垂直布局：x 是水平位置，y 是垂直位置）
  const nodes = contentG.append('g')
    .attr('class', 'tree-nodes')
    .selectAll('.tree-node')
    .data(root.descendants())
    .join('g')
    .attr('class', 'tree-node')
    .attr('transform', d => `translate(${d.x},${d.y})`)
    .style('cursor', 'pointer')
    .on('click', handleNodeClick)

  // 节点形状（使用统一配置）
  nodes.each(function(d) {
    const el = d3.select(this)
    // 帖子树节点属于帖子域（空心）
    d.data.domain = '帖子'
    const style = getNodeStyle(d)
    applyNodeShape(el, style).attr('class', 'tree-shape')
    nodeElements[d.data.id] = { element: this, x: d.x + 50, y: d.y + 25 }
  })

  // 节点标签（使用统一配置）
  nodes.append('text')
    .attr('dy', d => d.children ? -10 : 4)
    .attr('dx', d => d.children ? 0 : 10)
    .attr('text-anchor', d => d.children ? 'middle' : 'start')
    .attr('fill', d => getNodeStyle(d).text.fill)
    .attr('font-size', d => getNodeStyle(d).text.fontSize)
    .attr('font-weight', d => getNodeStyle(d).text.fontWeight)
    .text(d => {
      const name = d.data.name
      const maxLen = 10
      return name.length > maxLen ? name.slice(0, maxLen) + '…' : name
    })

  // ========== 绘制匹配层 ==========
  renderMatchLayer(contentG, root, treeHeight)

  // 初始适应视图
  fitToView()

  // 设置 hover 处理器（需要在所有元素创建后）
  nextTick(() => setupHoverHandlers())
}

// 绘制匹配层（人设节点 + 连线）
function renderMatchLayer(contentG, root, baseTreeHeight) {
  const postGraph = store.currentPostGraph
  if (!postGraph || !postGraph.edges) return

  // 提取匹配边（只取帖子->人设方向的）
  const matchEdges = []
  for (const edge of Object.values(postGraph.edges)) {
    if (edge.type === '匹配' && edge.source.startsWith('帖子:') && edge.target.startsWith('人设:')) {
      matchEdges.push(edge)
    }
  }

  if (matchEdges.length === 0) return

  // 收集匹配的人设节点（去重）
  const matchedPersonaMap = new Map()
  for (const edge of matchEdges) {
    if (!matchedPersonaMap.has(edge.target)) {
      // 从人设节点ID提取信息: "人设:目的点:标签:进行产品种草"
      const parts = edge.target.split(':')
      const name = parts[parts.length - 1]
      const dimension = parts[1] // 灵感点/目的点/关键点
      const type = parts[2] // 标签/分类/点
      matchedPersonaMap.set(edge.target, {
        id: edge.target,
        name: name,
        dimension: dimension,
        type: type,
        domain: '人设', // 人设节点：实心
        sourceEdges: [] // 连接的帖子节点
      })
    }
    matchedPersonaMap.get(edge.target).sourceEdges.push({
      sourceId: edge.source,
      score: edge.score
    })
  }

  const matchedPersonas = Array.from(matchedPersonaMap.values())
  if (matchedPersonas.length === 0) return

  // 计算匹配层的 Y 位置（树的最大深度 + 间距）
  const maxY = d3.max(root.descendants(), d => d.y) || 0
  const matchLayerY = maxY + 100

  // 计算匹配节点的 X 位置（均匀分布）
  const minX = d3.min(root.descendants(), d => d.x) || 0
  const maxX = d3.max(root.descendants(), d => d.x) || 0
  const matchSpacing = (maxX - minX) / Math.max(matchedPersonas.length - 1, 1)

  matchedPersonas.forEach((persona, i) => {
    persona.x = matchedPersonas.length === 1
      ? (minX + maxX) / 2
      : minX + i * matchSpacing
    persona.y = matchLayerY
  })

  // 收集匹配边数据（统一数据结构：source, target）
  const matchLinksData = []
  for (const persona of matchedPersonas) {
    for (const srcEdge of persona.sourceEdges) {
      const sourceNode = nodeElements[srcEdge.sourceId]
      if (!sourceNode) continue
      matchLinksData.push({
        source: srcEdge.sourceId,
        target: persona.id,
        score: srcEdge.score,
        srcX: sourceNode.x - 50,
        srcY: sourceNode.y - 25,
        tgtX: persona.x,
        tgtY: persona.y
      })
    }
  }

  // 绘制匹配连线（使用 data binding）
  const matchLinksG = contentG.append('g').attr('class', 'match-links')

  matchLinksG.selectAll('.match-link')
    .data(matchLinksData)
    .join('path')
    .attr('class', 'match-link')
    .attr('fill', 'none')
    .attr('stroke', d => getEdgeStyle({ type: '匹配', score: d.score }).color)
    .attr('stroke-opacity', d => getEdgeStyle({ type: '匹配', score: d.score }).opacity)
    .attr('stroke-width', d => getEdgeStyle({ type: '匹配', score: d.score }).strokeWidth)
    .attr('stroke-dasharray', d => getEdgeStyle({ type: '匹配', score: d.score }).strokeDasharray)
    .style('cursor', 'pointer')
    .on('click', (e, d) => {
      e.stopPropagation()
      store.selectEdge({
        source: d.source,
        target: d.target,
        type: '匹配',
        score: d.score
      })
    })
    .attr('d', d => {
      const midY = (d.srcY + d.tgtY) / 2
      return `M${d.srcX},${d.srcY} C${d.srcX},${midY} ${d.tgtX},${midY} ${d.tgtX},${d.tgtY}`
    })

  // 绘制分数标签（使用 data binding）
  const scoreData = matchLinksData.filter(d => getEdgeStyle({ type: '匹配', score: d.score }).scoreText)

  const scoreGroups = matchLinksG.selectAll('.match-score')
    .data(scoreData)
    .join('g')
    .attr('class', 'match-score')
    .attr('transform', d => {
      const midX = (d.srcX + d.tgtX) / 2
      const midY = (d.srcY + d.tgtY) / 2
      return `translate(${midX}, ${midY})`
    })

  scoreGroups.append('rect')
    .attr('x', -14)
    .attr('y', -6)
    .attr('width', 28)
    .attr('height', 12)
    .attr('rx', 2)
    .attr('fill', '#1d232a')
    .attr('opacity', 0.9)

  scoreGroups.append('text')
    .attr('text-anchor', 'middle')
    .attr('dy', '0.35em')
    .attr('fill', d => getEdgeStyle({ type: '匹配', score: d.score }).color)
    .attr('font-size', '8px')
    .text(d => getEdgeStyle({ type: '匹配', score: d.score }).scoreText)

  // 绘制匹配节点
  const matchNodesG = contentG.append('g').attr('class', 'match-nodes')

  const matchNodes = matchNodesG.selectAll('.match-node')
    .data(matchedPersonas)
    .join('g')
    .attr('class', 'match-node')
    .attr('transform', d => `translate(${d.x},${d.y})`)
    .style('cursor', 'pointer')
    .on('click', handleMatchNodeClick)

  // 匹配节点形状（使用统一配置）
  matchNodes.each(function(d) {
    const el = d3.select(this)
    const style = getNodeStyle(d, { isMatch: true })
    applyNodeShape(el, style).attr('class', 'tree-shape')
    nodeElements[d.id] = { element: this, x: d.x + 50, y: d.y + 25 }
  })

  // 匹配节点标签（使用统一配置）
  matchNodes.append('text')
    .attr('dy', 4)
    .attr('dx', 10)
    .attr('text-anchor', 'start')
    .attr('fill', d => getNodeStyle(d, { isMatch: true }).text.fill)
    .attr('font-size', d => getNodeStyle(d, { isMatch: true }).text.fontSize)
    .attr('font-weight', d => getNodeStyle(d, { isMatch: true }).text.fontWeight)
    .text(d => {
      const name = d.name
      const maxLen = 10
      return name.length > maxLen ? name.slice(0, maxLen) + '…' : name
    })

  // 更新总高度（用于 fitToView）
  treeHeight = matchLayerY + 50

  // 保存匹配层 Y 位置，供游走层使用
  lastMatchLayerY = matchLayerY

  // 保存基础节点快照（帖子树+匹配层），供游走层判断已有节点
  baseNodeElements = { ...nodeElements }
}

// 保存匹配层 Y 位置
let lastMatchLayerY = 0

// 绘制游走层（按层级渲染路径）
function renderWalkedLayer() {
  if (!mainG) return

  // 移除旧的游走层
  mainG.selectAll('.walked-layer').remove()

  // 重置 nodeElements 为基础节点（清除之前的游走节点）
  nodeElements = { ...baseNodeElements }

  const paths = store.postWalkedPaths
  if (!paths.length) return

  const contentG = mainG.select('g')
  if (contentG.empty()) return

  // 创建游走层组
  const walkedG = contentG.append('g').attr('class', 'walked-layer')

  // ========== 按路径位置分层遍历，确定每个新节点的层数 ==========
  // nodeLayer: nodeId -> layer (已有节点从 nodeElements 推断，新节点计算得出)
  const nodeLayer = new Map()
  const newNodes = new Set()  // 新增的节点

  console.log('=== renderWalkedLayer 层数计算 ===')
  console.log('路径数:', paths.length)
  console.log('lastMatchLayerY:', lastMatchLayerY)

  // 初始化：已有节点的层数（帖子树和匹配层）
  // 使用 baseNodeElements（不含之前的游走节点）
  // 匹配层的 Y 坐标是 lastMatchLayerY，作为基准层 0
  for (const [nodeId, info] of Object.entries(baseNodeElements)) {
    // 根据 Y 坐标推断层数（匹配层为0，帖子树在上面为负数）
    const layer = Math.round((info.y - 25 - lastMatchLayerY) / 80)
    nodeLayer.set(nodeId, Math.min(layer, 0))  // 已有节点层数 <= 0
  }
  console.log('基础节点数:', nodeLayer.size)

  // 找出所有路径的最大长度
  const maxPathLen = Math.max(...paths.map(p => p.nodes.length))
  console.log('最大路径长度:', maxPathLen)

  // 按位置分层遍历（从第2个节点开始，index=1）
  for (let pos = 1; pos < maxPathLen; pos++) {
    console.log(`--- 处理位置 ${pos} ---`)
    for (const path of paths) {
      if (pos >= path.nodes.length) continue

      const nodeId = path.nodes[pos]
      const prevNodeId = path.nodes[pos - 1]

      // 如果已经在树上，跳过
      if (nodeLayer.has(nodeId)) {
        console.log(`  节点 ${nodeId}: 已存在，层数=${nodeLayer.get(nodeId)}，跳过`)
        continue
      }

      // 找前驱节点的层数
      const prevLayer = nodeLayer.get(prevNodeId) ?? 0
      const newLayer = prevLayer + 1

      console.log(`  节点 ${nodeId}: 前驱=${prevNodeId}(层${prevLayer}) → 新层数=${newLayer}`)

      // 新节点层数 = 前驱层数 + 1
      nodeLayer.set(nodeId, newLayer)
      newNodes.add(nodeId)
    }
  }

  console.log('新增节点数:', newNodes.size)
  console.log('新增节点:', Array.from(newNodes))

  // ========== 按层分组新节点 ==========
  const layerGroups = new Map()  // layer -> Set of nodeIds
  for (const nodeId of newNodes) {
    const layer = nodeLayer.get(nodeId)
    if (!layerGroups.has(layer)) {
      layerGroups.set(layer, new Set())
    }
    layerGroups.get(layer).add(nodeId)
  }

  // ========== 计算新节点位置 ==========
  const treeNodes = Object.values(nodeElements)
  const minX = d3.min(treeNodes, d => d.x) || 50
  const maxX = d3.max(treeNodes, d => d.x) || 300
  const layerSpacing = 80  // 层间距

  // nodePositions 只存储新节点的位置
  const nodePositions = {}

  for (const [layer, nodeIds] of layerGroups) {
    const nodesAtLayer = Array.from(nodeIds)
    const layerY = lastMatchLayerY + layer * layerSpacing

    // 新节点均匀分布
    const spacing = (maxX - minX - 100) / Math.max(nodesAtLayer.length - 1, 1)
    nodesAtLayer.forEach((nodeId, i) => {
      nodePositions[nodeId] = {
        x: nodesAtLayer.length === 1 ? (minX + maxX) / 2 - 50 : (minX - 50) + i * spacing,
        y: layerY
      }
    })
  }

  // ========== 收集所有边 ==========
  const allEdges = new Map()
  for (const path of paths) {
    for (const edge of path.edges) {
      const edgeKey = `${edge.source}->${edge.target}`
      if (!allEdges.has(edgeKey)) {
        allEdges.set(edgeKey, edge)
      }
    }
  }

  // 获取节点位置的辅助函数（优先新节点位置，然后已有节点位置）
  function getNodePos(nodeId) {
    if (nodePositions[nodeId]) {
      return nodePositions[nodeId]
    }
    if (nodeElements[nodeId]) {
      return {
        x: nodeElements[nodeId].x - 50,
        y: nodeElements[nodeId].y - 25
      }
    }
    return null
  }

  // ========== 绘制所有路径上的边 ==========
  const edgesData = []
  console.log('=== 绘制边 ===')
  console.log('总边数:', allEdges.size)

  for (const edge of allEdges.values()) {
    const srcPos = getNodePos(edge.source)
    const tgtPos = getNodePos(edge.target)

    console.log(`边 ${edge.source} -> ${edge.target}:`,
      'srcPos=', srcPos ? `(${srcPos.x.toFixed(0)},${srcPos.y.toFixed(0)})` : 'null',
      'tgtPos=', tgtPos ? `(${tgtPos.x.toFixed(0)},${tgtPos.y.toFixed(0)})` : 'null'
    )

    if (srcPos && tgtPos) {
      edgesData.push({
        ...edge,
        srcX: srcPos.x,
        srcY: srcPos.y,
        tgtX: tgtPos.x,
        tgtY: tgtPos.y
      })
    } else {
      console.log('  ⚠️ 跳过：位置缺失')
    }
  }
  console.log('实际绘制边数:', edgesData.length)

  walkedG.selectAll('.walked-link')
    .data(edgesData)
    .join('path')
    .attr('class', 'walked-link')
    .attr('fill', 'none')
    .attr('stroke', d => getEdgeStyle({ type: d.type, score: d.score }).color)
    .attr('stroke-opacity', d => getEdgeStyle({ type: d.type, score: d.score }).opacity)
    .attr('stroke-width', d => getEdgeStyle({ type: d.type, score: d.score }).strokeWidth)
    .attr('stroke-dasharray', d => getEdgeStyle({ type: d.type, score: d.score }).strokeDasharray)
    .style('cursor', 'pointer')
    .on('click', (e, d) => {
      e.stopPropagation()
      store.selectEdge({
        source: d.source,
        target: d.target,
        type: d.type,
        score: d.score
      })
    })
    .attr('d', d => {
      // 同一层的边（Y坐标相近）用向下弯曲的曲线
      if (Math.abs(d.srcY - d.tgtY) < 10) {
        const controlY = d.srcY + 50  // 向下弯曲
        const midX = (d.srcX + d.tgtX) / 2
        return `M${d.srcX},${d.srcY} Q${midX},${controlY} ${d.tgtX},${d.tgtY}`
      }
      // 不同层的边用 S 形曲线
      const midY = (d.srcY + d.tgtY) / 2
      return `M${d.srcX},${d.srcY} C${d.srcX},${midY} ${d.tgtX},${midY} ${d.tgtX},${d.tgtY}`
    })

  // 绘制分数标签
  const scoreData = edgesData.filter(d => getEdgeStyle({ type: d.type, score: d.score }).scoreText)
  const scoreGroups = walkedG.selectAll('.walked-score')
    .data(scoreData)
    .join('g')
    .attr('class', 'walked-score')
    .attr('transform', d => {
      const midX = (d.srcX + d.tgtX) / 2
      // 同一层的边用向下弯曲曲线，分数标签放在曲线中点（t=0.5 时 y = srcY + 25）
      const midY = Math.abs(d.srcY - d.tgtY) < 10 ? d.srcY + 25 : (d.srcY + d.tgtY) / 2
      return `translate(${midX}, ${midY})`
    })

  scoreGroups.append('rect')
    .attr('x', -14).attr('y', -6).attr('width', 28).attr('height', 12)
    .attr('rx', 2).attr('fill', '#1d232a').attr('opacity', 0.9)

  scoreGroups.append('text')
    .attr('text-anchor', 'middle').attr('dy', '0.35em')
    .attr('fill', d => getEdgeStyle({ type: d.type, score: d.score }).color)
    .attr('font-size', '8px')
    .text(d => getEdgeStyle({ type: d.type, score: d.score }).scoreText)

  // ========== 绘制新节点（nodePositions 中的都是新节点） ==========
  const newNodesData = []
  for (const [nodeId, pos] of Object.entries(nodePositions)) {
    // 获取节点数据
    const nodeData = store.postWalkedNodes.find(n => n.id === nodeId)
    if (nodeData) {
      newNodesData.push({ ...nodeData, x: pos.x, y: pos.y })
    }
  }

  const walkedNodeGroups = walkedG.selectAll('.walked-node')
    .data(newNodesData)
    .join('g')
    .attr('class', 'walked-node')
    .attr('transform', d => `translate(${d.x},${d.y})`)
    .style('cursor', 'pointer')
    .on('click', (event, d) => {
      event.stopPropagation()
      // 锁定状态下点击节点无效果，但提醒用户
      if (store.lockedHoverNodeId) {
        shakeLockButton()
        return
      }
      store.selectNode(d)
    })

  // 节点形状
  walkedNodeGroups.each(function(d) {
    const el = d3.select(this)
    const style = getNodeStyle(d)
    applyNodeShape(el, style).attr('class', 'walked-shape')
    nodeElements[d.id] = { element: this, x: d.x + 50, y: d.y + 25 }
  })

  // 节点标签
  walkedNodeGroups.append('text')
    .attr('dy', 4).attr('dx', 10).attr('text-anchor', 'start')
    .attr('fill', d => getNodeStyle(d).text.fill)
    .attr('font-size', d => getNodeStyle(d).text.fontSize)
    .attr('font-weight', d => getNodeStyle(d).text.fontWeight)
    .text(d => {
      const name = d.name
      const maxLen = 10
      return name.length > maxLen ? name.slice(0, maxLen) + '…' : name
    })

  // 更新高度（如果有新节点）
  if (Object.keys(nodePositions).length > 0) {
    const maxY = Math.max(...Object.values(nodePositions).map(p => p.y))
    treeHeight = Math.max(treeHeight, maxY + 50)
  }

  // ========== 添加 hover 效果（所有元素创建后统一添加） ==========
  setupHoverHandlers()
}

// 在节点文字后面添加锁定按钮（作为独立的 tspan）
function showLockButton(nodeEl, immediate = false) {
  if (!nodeEl) return

  const node = d3.select(nodeEl)
  const textEl = node.select('text')
  if (textEl.empty()) return

  // 获取当前节点 ID
  const nodeData = node.datum()
  const currentNodeId = nodeData?.data?.id || nodeData?.id

  // 判断当前节点是否是已锁定的节点
  const isThisNodeLocked = store.lockedHoverNodeId && store.lockedHoverNodeId === currentNodeId

  // 如果已有按钮，只更新状态
  let btn = textEl.select('.lock-btn')
  if (!btn.empty()) {
    btn.text(isThisNodeLocked ? ' 🔓解锁' : ' 🔒锁定')
       .attr('fill', isThisNodeLocked ? '#f6ad55' : '#63b3ed')
    if (!isThisNodeLocked) startBreathingAnimation(btn)
    return
  }

  // 创建按钮的函数
  const createBtn = () => {
    // 先清除当前 SVG 内其他节点的按钮（不影响另一边）
    if (svgRef.value) {
      d3.select(svgRef.value).selectAll('.lock-btn').remove()
    }

    // 添加按钮 tspan（紧跟在文字后面）
    const btn = textEl.append('tspan')
      .attr('class', 'lock-btn')
      .attr('fill', isThisNodeLocked ? '#f6ad55' : '#63b3ed')
      .attr('font-weight', 'bold')
      .style('cursor', 'pointer')
      .text(isThisNodeLocked ? ' 🔓解锁' : ' 🔒锁定')
      .on('click', (e) => {
        e.stopPropagation()
        handleLockClick()
      })

    // 呼吸灯动画（未锁定时）
    if (!isThisNodeLocked) {
      startBreathingAnimation(btn)
    }
  }

  createBtn()
}

// 呼吸灯动画（只有按钮部分，蓝色呼吸）
function startBreathingAnimation(btn) {
  function breathe() {
    if (btn.empty() || !btn.node()) return
    if (store.lockedHoverNodeId) return
    btn
      .transition()
      .duration(800)
      .attr('fill', '#90cdf4')
      .transition()
      .duration(800)
      .attr('fill', '#63b3ed')
      .on('end', breathe)
  }
  breathe()
}

// 隐藏锁定按钮
function hideLockButton() {
  if (svgRef.value) {
    d3.select(svgRef.value).selectAll('.lock-btn').interrupt().remove()
  }
}

// 抖动锁定按钮（提醒用户需要先解锁）
function shakeLockButton() {
  d3.selectAll('.lock-btn')
    .interrupt()
    .attr('fill', '#fc8181')  // 红色警告
    .transition().duration(50).attr('dx', 3)
    .transition().duration(50).attr('dx', -3)
    .transition().duration(50).attr('dx', 3)
    .transition().duration(50).attr('dx', -3)
    .transition().duration(50).attr('dx', 0)
    .transition().duration(200).attr('fill', '#f6ad55')  // 恢复橙色
}

// 处理锁定按钮点击
function handleLockClick() {
  const startNodeId = store.selectedNodeId
  const currentHoverNodeId = store.hoverNodeId

  // 判断是解锁还是新锁定
  if (store.lockedHoverNodeId && store.lockedHoverNodeId === currentHoverNodeId) {
    // 点击的是当前锁定节点的按钮 → 解锁（弹出栈）
    store.clearLockedHover()
    // 如果还有上一层锁定，更新按钮状态
    if (store.lockedHoverNodeId) {
      d3.selectAll('.lock-btn')
        .interrupt()
        .text(' 🔓解锁')
        .attr('fill', '#f6ad55')
    } else {
      // 完全解锁，清除按钮
      d3.selectAll('.lock-btn').interrupt().remove()
    }
  } else if (currentHoverNodeId) {
    // 点击的是新 hover 节点的按钮 → 锁定新路径（压入栈）
    store.lockCurrentHover(startNodeId)
    // 更新按钮状态
    d3.selectAll('.lock-btn')
      .interrupt()
      .text(' 🔓解锁')
      .attr('fill', '#f6ad55')
  }
}

// 设置 hover 处理器（在所有元素创建后调用）
function setupHoverHandlers() {
  if (!svgRef.value || !store.selectedNodeId) return

  const svg = d3.select(svgRef.value)
  const allNodes = svg.selectAll('.tree-node, .match-node, .walked-node')
  const startNodeId = store.selectedNodeId

  // 如果已有锁定状态，显示解锁按钮
  if (store.lockedHoverNodeId) {
    const lockedNodeInfo = nodeElements[store.lockedHoverNodeId]
    if (lockedNodeInfo?.element) {
      showLockButton(lockedNodeInfo.element, true)
    }
  }

  // 添加 hover 处理器（路径计算由 store 统一处理）
  allNodes
    .on('mouseenter', function(event, d) {
      const nodeId = d.data?.id || d.id
      // 排除起始节点
      if (nodeId === startNodeId) return

      store.computeHoverPath(startNodeId, nodeId, 'post-tree')
      store.setHoverNode(d)  // 设置 hover 节点数据用于详情显示

      // 显示锁定按钮（在当前hover节点上）
      if (store.hoverPathNodes.size > 0) {
        // 如果已锁定，按钮显示在锁定节点上
        if (store.lockedHoverNodeId) {
          const lockedNodeInfo = nodeElements[store.lockedHoverNodeId]
          if (lockedNodeInfo?.element) {
            showLockButton(lockedNodeInfo.element, true)
          }
        } else {
          // 未锁定，按钮显示在当前hover节点上
          showLockButton(this)
        }
      }
    })
    .on('mouseleave', () => {
      // 调用 clearHover 恢复状态（如果已锁定会恢复到锁定路径）
      store.clearHover()

      if (store.lockedHoverNodeId) {
        // 已锁定：恢复锁定路径高亮，并在锁定节点上显示按钮
        const svg = d3.select(svgRef.value)
        const allNodes = svg.selectAll('.tree-node, .match-node, .walked-node')
        const allLinks = svg.selectAll('.tree-link, .match-link, .walked-link')
        const allLabels = svg.selectAll('.match-score, .walked-score')
        // 恢复到纯锁定路径高亮（不传 lockedPath，因为这就是唯一的路径）
        applyHoverHighlight(allNodes, allLinks, allLabels, store.hoverPathNodes, null)
        // 在锁定节点上显示解锁按钮
        const lockedNodeInfo = nodeElements[store.lockedHoverNodeId]
        if (lockedNodeInfo?.element) {
          showLockButton(lockedNodeInfo.element, true)
        }
      } else {
        hideLockButton()
      }
    })
}

// 匹配节点点击处理
function handleMatchNodeClick(event, d) {
  event.stopPropagation()
  // 锁定状态下点击节点无效果，但提醒用户
  if (store.lockedHoverNodeId) {
    shakeLockButton()
    return
  }
  store.selectNode(d)
}

// ========== 详情显示格式化函数 ==========

// 格式化字段名（camelCase/snake_case -> 中文/可读）
function formatKey(key) {
  const keyMap = {
    'id': 'ID',
    'name': '名称',
    'type': '类型',
    'dimension': '维度',
    'source': '源节点',
    'target': '目标节点',
    'score': '分数',
    'detail': '详情',
    'postId': '帖子ID',
    'postTitle': '帖子标题',
    'createTime': '创建时间',
    'parentId': '父节点',
    'children': '子节点',
    'description': '描述',
    'content': '内容',
    'tags': '标签',
    'category': '分类',
    'level': '层级',
    'depth': '深度',
    'weight': '权重',
    'count': '数量',
    'status': '状态',
    'reason': '原因',
    'explanation': '说明',
    'matchReason': '匹配原因',
    'similarity': '相似度',
    'confidence': '置信度'
  }
  return keyMap[key] || key
}

// 格式化普通值
function formatValue(value) {
  if (value === null || value === undefined) return '-'
  if (typeof value === 'boolean') return value ? '是' : '否'
  if (typeof value === 'number') {
    // 如果是小数，保留2位
    if (!Number.isInteger(value)) return value.toFixed(2)
    return value.toString()
  }
  if (Array.isArray(value)) {
    if (value.length === 0) return '-'
    return value.join(', ')
  }
  if (typeof value === 'object') {
    return JSON.stringify(value)
  }
  return String(value)
}

// 格式化边的值（特殊处理 source/target 显示名称）
function formatEdgeValue(key, value) {
  if (key === 'source' || key === 'target') {
    // 使用 getNodeName 获取节点名称
    return getNodeName(value)
  }
  return formatValue(value)
}

// 适应视图（自动缩放以显示全部内容）
function fitToView() {
  if (!zoom || !mainG || !containerRef.value) return

  const container = containerRef.value
  const width = container.clientWidth
  const height = container.clientHeight

  // 计算缩放比例以适应容器
  const scaleX = width / treeWidth
  const scaleY = height / treeHeight
  const scale = Math.min(scaleX, scaleY, 1) * 0.9 // 留一点边距

  // 计算居中偏移
  const translateX = (width - treeWidth * scale) / 2
  const translateY = (height - treeHeight * scale) / 2

  const svg = d3.select(svgRef.value)
  svg.call(zoom.transform, d3.zoomIdentity.translate(translateX, translateY).scale(scale))
}

// 定位到指定节点
function zoomToNode(nodeId) {
  const nodeInfo = nodeElements[nodeId]
  if (!nodeInfo || !zoom || !containerRef.value) return

  const container = containerRef.value
  const width = container.clientWidth
  const height = container.clientHeight

  // 计算平移使节点居中
  const scale = 1
  const translateX = width / 2 - nodeInfo.x * scale
  const translateY = height / 2 - nodeInfo.y * scale

  const svg = d3.select(svgRef.value)
  svg.transition().duration(300).call(
    zoom.transform,
    d3.zoomIdentity.translate(translateX, translateY).scale(scale)
  )
}

// 定位到指定边（显示边的两个端点）
function zoomToEdge(sourceId, targetId) {
  const sourceInfo = nodeElements[sourceId]
  const targetInfo = nodeElements[targetId]
  if (!sourceInfo || !targetInfo || !zoom || !containerRef.value) return

  const container = containerRef.value
  const width = container.clientWidth
  const height = container.clientHeight

  // 计算边的中心点和范围
  const centerX = (sourceInfo.x + targetInfo.x) / 2
  const centerY = (sourceInfo.y + targetInfo.y) / 2
  const edgeWidth = Math.abs(sourceInfo.x - targetInfo.x) + 100
  const edgeHeight = Math.abs(sourceInfo.y - targetInfo.y) + 100

  // 计算合适的缩放比例（让边的两端都能显示）
  const scaleX = width / edgeWidth
  const scaleY = height / edgeHeight
  const scale = Math.min(scaleX, scaleY, 1.2) // 最大缩放1.2

  // 计算平移使边居中
  const translateX = width / 2 - centerX * scale
  const translateY = height / 2 - centerY * scale

  const svg = d3.select(svgRef.value)
  svg.transition().duration(300).call(
    zoom.transform,
    d3.zoomIdentity.translate(translateX, translateY).scale(scale)
  )
}

// 更新高亮/置灰状态
function updateHighlight() {
  // 使用帖子游走的边集合（如果有），否则用人设游走的边集合
  const edgeSet = store.postWalkedEdgeSet.size > 0 ? store.postWalkedEdgeSet : store.walkedEdgeSet
  applyHighlight(svgRef.value, store.highlightedNodeIds, edgeSet, store.selectedNodeId)
}

// 点击空白取消（锁定状态下无效果）
function handleSvgClick(event) {
  // 锁定状态下，点击空白无效果
  if (store.lockedHoverNodeId) return

  const target = event.target
  if (!target.closest('.tree-node') && !target.closest('.match-node') && !target.closest('.walked-node')) {
    store.clearSelection()
    hideLockButton()
  }
}

// 监听选中/高亮变化，统一更新
watch(() => store.selectedNodeId, (nodeId, oldNodeId) => {
  updateHighlight()
  if (nodeId && nodeId !== oldNodeId) {
    zoomToNode(nodeId)
  }
  // 重新设置 hover 处理器（起点改变了）
  nextTick(() => setupHoverHandlers())
})

watch(() => store.selectedEdgeId, updateHighlight)

// 监听聚焦边端点变化（由 store 统一管理）
watch(() => store.focusEdgeEndpoints, (endpoints) => {
  if (endpoints) {
    nextTick(() => {
      zoomToEdge(endpoints.source, endpoints.target)
    })
  }
})

watch(() => store.highlightedNodeIds.size, updateHighlight)

// 监听 hover 状态变化（用于左右联动）
watch([() => store.hoverPathNodes.size, () => store.hoverNodeId], () => {
  if (!svgRef.value) return
  const svg = d3.select(svgRef.value)

  const allNodes = svg.selectAll('.tree-node, .match-node, .walked-node')
  const allLinks = svg.selectAll('.tree-link, .match-link, .walked-link')
  const allLabels = svg.selectAll('.match-score, .walked-score')

  if (store.hoverPathNodes.size > 0) {
    // 应用 hover 高亮（支持嵌套：传入锁定路径）
    const lockedPath = store.lockedHoverPathNodes.size > 0 ? store.lockedHoverPathNodes : null
    applyHoverHighlight(allNodes, allLinks, allLabels, store.hoverPathNodes, lockedPath)

    // 如果是从 GraphView 触发的，缩放到显示完整路径
    if (store.hoverSource === 'graph') {
      zoomToPathNodes(store.hoverPathNodes)
    }

    // 在对应节点上显示锁定按钮（无论来源）
    if (store.hoverNodeId) {
      const nodeInfo = nodeElements[store.hoverNodeId]
      if (nodeInfo?.element) {
        showLockButton(nodeInfo.element)
      }
    }
  } else {
    // 清除 hover，恢复原有高亮
    updateHighlight()
    // 如果没有锁定，隐藏按钮
    if (!store.lockedHoverNodeId) {
      hideLockButton()
    }
  }
})

// 缩放到显示路径上的所有节点
function zoomToPathNodes(pathNodes) {
  if (!zoom || !containerRef.value || !svgRef.value) return

  // 收集路径节点的位置
  const positions = []
  for (const nodeId of pathNodes) {
    const nodeInfo = nodeElements[nodeId]
    if (nodeInfo) {
      positions.push({ x: nodeInfo.x, y: nodeInfo.y })
    }
  }

  if (positions.length === 0) return

  // 计算边界框
  const minX = Math.min(...positions.map(p => p.x))
  const maxX = Math.max(...positions.map(p => p.x))
  const minY = Math.min(...positions.map(p => p.y))
  const maxY = Math.max(...positions.map(p => p.y))

  const width = containerRef.value.clientWidth
  const height = containerRef.value.clientHeight
  const padding = 60

  // 计算需要的缩放和平移
  const boxWidth = maxX - minX + padding * 2
  const boxHeight = maxY - minY + padding * 2
  const scale = Math.min(width / boxWidth, height / boxHeight, 1.5)
  const centerX = (minX + maxX) / 2
  const centerY = (minY + maxY) / 2
  const translateX = width / 2 - centerX * scale
  const translateY = height / 2 - centerY * scale

  const svg = d3.select(svgRef.value)
  svg.transition().duration(200).call(
    zoom.transform,
    d3.zoomIdentity.translate(translateX, translateY).scale(scale)
  )
}

// 监听帖子游走结果变化，渲染游走层
watch(() => store.postWalkedNodes.length, () => {
  nextTick(renderWalkedLayer)
})

// 监听当前帖子变化，重新渲染树
watch(() => store.currentPostGraph, () => {
  nextTick(() => {
    renderTree()
  })
}, { immediate: false })

// 监听 selectedPostIndex 变化，同步下拉框
watch(() => store.selectedPostIndex, (newIdx) => {
  selectedPostIdx.value = newIdx
})

// 恢复锁定的 hover 状态（重新渲染后调用）
function restoreLockedHover() {
  if (!store.lockedHoverNodeId || !svgRef.value) return

  const svg = d3.select(svgRef.value)
  const allNodes = svg.selectAll('.tree-node, .match-node, .walked-node')
  const allLinks = svg.selectAll('.tree-link, .match-link, .walked-link')
  const allLabels = svg.selectAll('.match-score, .walked-score')

  // 恢复高亮效果（传入锁定路径）
  if (store.hoverPathNodes.size > 0) {
    const lockedPath = store.lockedHoverPathNodes.size > 0 ? store.lockedHoverPathNodes : null
    applyHoverHighlight(allNodes, allLinks, allLabels, store.hoverPathNodes, lockedPath)
  }

  // 恢复锁定按钮
  const lockedNodeInfo = nodeElements[store.lockedHoverNodeId]
  if (lockedNodeInfo?.element) {
    showLockButton(lockedNodeInfo.element, true)
  }
}

// 监听布局变化，过渡结束后重新适应视图
function handleTransitionEnd(e) {
  if (['width', 'height', 'flex', 'flex-grow', 'flex-shrink'].includes(e.propertyName)) {
    nextTick(() => {
      renderTree()
      nextTick(() => {
        renderWalkedLayer()  // 重新渲染游走层
        nextTick(() => {
          updateHighlight()  // 重新应用高亮状态
          restoreLockedHover()  // 恢复锁定的 hover 状态
        })
      })
    })
  }
}

let transitionParent = null

onMounted(() => {
  nextTick(() => {
    renderTree()
  })

  // 监听父容器的过渡结束事件
  if (containerRef.value) {
    let parent = containerRef.value.parentElement
    while (parent && !parent.classList.contains('transition-all')) {
      parent = parent.parentElement
    }
    if (parent) {
      transitionParent = parent
      parent.addEventListener('transitionend', handleTransitionEnd)
    }
  }
})

onUnmounted(() => {
  if (transitionParent) {
    transitionParent.removeEventListener('transitionend', handleTransitionEnd)
  }
})
</script>
