<template>
  <section class="knowledge-graph-space">
    <header class="graph-head">
      <div>
        <span class="graph-eyebrow">LightRAG 知识图谱</span>
        <h4>实体关系空间</h4>
      </div>
      <div class="graph-toolbar">
        <label class="graph-search">
          <i class="mdi mdi-magnify"></i>
          <input
            v-model.trim="graphQuery"
            type="search"
            placeholder="搜索实体"
            @keydown.enter.prevent="focusMatchedNode"
          />
        </label>
        <button type="button" title="定位匹配实体" @click="focusMatchedNode">
          <i class="mdi mdi-crosshairs-gps"></i>
        </button>
        <button type="button" title="缩小" @click="zoomGraph(0.86)">
          <i class="mdi mdi-magnify-minus-outline"></i>
        </button>
        <button type="button" title="放大" @click="zoomGraph(1.16)">
          <i class="mdi mdi-magnify-plus-outline"></i>
        </button>
        <button type="button" title="适配画布" @click="fitGraph">
          <i class="mdi mdi-fit-to-page-outline"></i>
        </button>
      </div>
    </header>

    <div class="graph-stats">
      <span>{{ graphSummary.entityCount }} 实体</span>
      <span>{{ graphSummary.relationCount }} 关系</span>
      <span>{{ graphSummary.visibleNodeCount }} 节点可见</span>
      <span>{{ graphSummary.visibleEdgeCount }} 连线可见</span>
    </div>

    <div class="graph-body" :class="{ empty: !graphData.nodes.length }">
      <div class="graph-theater">
        <div v-if="graphData.nodes.length" ref="graphContainer" class="g6-canvas"></div>
        <div v-else class="graph-empty">
          <strong>暂无图谱数据</strong>
          <span>本次归集还没有返回可展示的实体关系。</span>
        </div>

        <div v-if="graphData.nodes.length" class="graph-hud">
          <span>G6 Force</span>
          <span>拖拽节点</span>
          <span>滚轮缩放</span>
          <span>点击查看详情</span>
        </div>
      </div>

      <aside class="graph-inspector">
        <template v-if="selectedNode">
          <div class="inspector-title">
            <span>节点详情</span>
            <h5>{{ selectedNode.name }}</h5>
          </div>

          <div class="node-facts">
            <div>
              <span>关系数</span>
              <strong>{{ selectedNode.degree }}</strong>
            </div>
            <div>
              <span>入边</span>
              <strong>{{ selectedNodeIncoming.length }}</strong>
            </div>
            <div>
              <span>出边</span>
              <strong>{{ selectedNodeOutgoing.length }}</strong>
            </div>
          </div>

          <div class="node-meta">
            <span>类型</span>
            <strong>{{ selectedNode.type || '实体' }}</strong>
          </div>

          <KnowledgeIngestNodeDetails
            :node="selectedNode"
            :incoming="selectedNodeIncoming"
            :outgoing="selectedNodeOutgoing"
            :relations="selectedNodeRelations"
            @focus-peer="focusRelationPeer"
          />
        </template>

        <div v-else class="inspector-empty">
          <strong>选择一个实体</strong>
          <span>点击图谱中的节点，查看它的入边、出边和关系明细。</span>
        </div>
      </aside>
    </div>
  </section>
</template>

<script setup>
import { Graph, NodeEvent } from '@antv/g6'
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'

import KnowledgeIngestNodeDetails from './KnowledgeIngestNodeDetails.vue'
import { useKnowledgeIngestGraph } from './useKnowledgeIngestGraph.js'

const props = defineProps({
  graph: {
    type: Object,
    default: () => ({})
  },
  documents: {
    type: Array,
    default: () => []
  }
})

const graphContainer = ref(null)
let graphInstance = null
let resizeObserver = null

const {
  activeNodeId,
  graphData,
  graphQuery,
  graphSummary,
  selectNodeById,
  focusFirstMatch,
  selectRelationPeer,
  selectedNode,
  selectedNodeIncoming,
  selectedNodeOutgoing,
  selectedNodeRelations
} = useKnowledgeIngestGraph(props)

onMounted(() => {
  initGraph()
})

onBeforeUnmount(() => {
  resizeObserver?.disconnect()
  graphInstance?.destroy()
  resizeObserver = null
  graphInstance = null
})

watch(
  graphData,
  () => {
    renderGraph()
  },
  { deep: true }
)

watch(activeNodeId, (nodeId) => {
  syncGraphSelection(nodeId)
})

async function initGraph() {
  if (!graphData.value.nodes.length) return
  await nextTick()
  if (!graphContainer.value || graphInstance) return

  graphInstance = new Graph(buildGraphOptions())
  graphInstance.on(NodeEvent.CLICK, async (event) => {
    const nodeId = String(event?.target?.id || '').trim()
    if (!nodeId) return
    selectNodeById(nodeId)
    await focusGraphNode(nodeId, { focus: false })
  })
  await graphInstance.render()
  await syncGraphSelection(activeNodeId.value)
  observeResize()
  await fitGraph()
}

function buildGraphOptions() {
  return {
    container: graphContainer.value,
    autoResize: true,
    background: 'transparent',
    zoomRange: [0.22, 2.8],
    autoFit: {
      type: 'view',
      options: { padding: [86, 122, 86, 122] },
      animation: { duration: 520, easing: 'ease-in-out' }
    },
    data: graphData.value,
    layout: {
      type: 'd3-force',
      preventOverlap: true,
      centerStrength: 0.34,
      linkDistance: (edge) => (edge?.data?.weight > 2 ? 155 : 210),
      edgeStrength: 0.22,
      nodeStrength: (node) => (node?.data?.degree >= 5 ? -520 : -360),
      collide: {
        radius: (node) => 40 + Math.sqrt(Math.max(node?.data?.degree || 1, 1)) * 12,
        strength: 0.88,
        iterations: 3
      },
      manyBody: {
        strength: (node) => (node?.data?.degree >= 5 ? -620 : -420),
        distanceMax: 620
      },
      center: {
        strength: 0.24
      },
      alpha: 0.88,
      alphaDecay: 0.035,
      velocityDecay: 0.32,
      iterations: 360
    },
    node: {
      type: 'circle',
      style: (datum) => datum.style,
      state: {
        selected: {
          lineWidth: 4,
          stroke: '#1d4ed8',
          halo: true,
          haloStroke: '#60a5fa',
          haloLineWidth: 24,
          haloStrokeOpacity: 0.24,
          shadowBlur: 22
        },
        active: {
          lineWidth: 3,
          halo: true,
          haloLineWidth: 16,
          haloStrokeOpacity: 0.28
        },
        inactive: {
          opacity: 0.2
        },
        dimmed: {
          opacity: 0.18
        }
      }
    },
    edge: {
      type: 'line',
      style: (datum) => datum.style,
      state: {
        selected: {
          stroke: '#2563eb',
          lineWidth: 2.8,
          opacity: 0.96,
          halo: true,
          haloStroke: '#93c5fd',
          haloLineWidth: 8,
          haloStrokeOpacity: 0.2
        },
        active: {
          stroke: '#60a5fa',
          lineWidth: 2.4,
          opacity: 0.9
        },
        inactive: {
          opacity: 0.14
        }
      }
    },
    behaviors: [
      { type: 'drag-canvas', key: 'drag-canvas' },
      { type: 'zoom-canvas', key: 'zoom-canvas', sensitivity: 1.15 },
      { type: 'drag-element-force', key: 'drag-element-force', fixed: true }
    ],
    animation: { duration: 420, easing: 'ease-out' }
  }
}

async function renderGraph() {
  if (!graphData.value.nodes.length) return
  if (!graphInstance) {
    await initGraph()
    return
  }
  graphInstance.setData(graphData.value)
  await graphInstance.render()
  await syncGraphSelection(activeNodeId.value)
  await fitGraph()
}

async function syncGraphSelection(nodeId) {
  if (!graphInstance || graphInstance.destroyed || !nodeId) return
  const stateMap = {}
  for (const node of graphData.value.nodes) {
    if (node.id === nodeId) {
      stateMap[node.id] = ['selected']
    } else if (node.states?.includes('dimmed')) {
      stateMap[node.id] = ['dimmed']
    } else {
      stateMap[node.id] = []
    }
  }
  for (const edge of graphData.value.edges) {
    stateMap[edge.id] = edge.source === nodeId || edge.target === nodeId ? ['selected'] : ['inactive']
  }
  await graphInstance.setElementState(stateMap, false)
}

async function fitGraph() {
  if (!graphInstance || graphInstance.destroyed) return
  await graphInstance.fitView(
    { padding: [84, 118, 84, 118] },
    { duration: 420, easing: 'ease-in-out' }
  )
}

async function zoomGraph(ratio) {
  if (!graphInstance || graphInstance.destroyed) return
  await graphInstance.zoomBy(ratio, { duration: 220, easing: 'ease-out' })
}

async function focusMatchedNode() {
  const nodeId = focusFirstMatch()
  if (nodeId) await focusGraphNode(nodeId)
}

async function focusRelationPeer(relation) {
  const nodeId = selectRelationPeer(relation)
  if (nodeId) await focusGraphNode(nodeId)
}

async function focusGraphNode(nodeId, options = {}) {
  if (!graphInstance || graphInstance.destroyed) return
  await syncGraphSelection(nodeId)
  if (options.focus === false) return
  await graphInstance.focusElement(nodeId, { duration: 360, easing: 'ease-in-out' })
}

function observeResize() {
  if (!graphContainer.value || typeof ResizeObserver === 'undefined') return
  resizeObserver = new ResizeObserver(() => {
    if (!graphInstance || graphInstance.destroyed) return
    graphInstance.setSize(graphContainer.value.clientWidth, graphContainer.value.clientHeight)
  })
  resizeObserver.observe(graphContainer.value)
}
</script>

<style scoped>
.knowledge-graph-space {
  min-height: 1080px;
  display: grid;
  grid-template-rows: auto auto minmax(1020px, 1fr);
  gap: 12px;
  overflow: hidden;
}

.graph-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
}

.graph-eyebrow {
  color: var(--theme-primary-active);
  font-size: 12px;
  font-weight: 850;
}

.graph-head h4 {
  margin: 4px 0 0;
  color: #0f172a;
  font-size: 15px;
}

.graph-toolbar,
.graph-stats {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 8px;
}

.graph-search {
  min-width: 230px;
  min-height: 32px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 0 9px;
  border: 1px solid #dbe6ef;
  border-radius: 8px;
  background: #fff;
  color: #64748b;
}

.graph-search input {
  width: 100%;
  min-width: 0;
  border: 0;
  outline: 0;
  background: transparent;
  color: #0f172a;
  font-size: 12px;
}

.graph-toolbar button {
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  border: 1px solid #dbe6ef;
  border-radius: 8px;
  background: #fff;
  color: #475569;
  cursor: pointer;
  transition: border-color 0.2s ease, color 0.2s ease, transform 0.2s ease;
}

.graph-toolbar button:hover {
  border-color: #2563eb;
  color: #2563eb;
  transform: translateY(-1px);
}

.graph-stats {
  justify-content: flex-start;
}

.graph-stats span {
  min-height: 26px;
  display: inline-flex;
  align-items: center;
  padding: 0 9px;
  border: 1px solid #dbe6ef;
  border-radius: 999px;
  background: #fff;
  color: #475569;
  font-size: 12px;
  font-weight: 750;
}

.graph-body {
  min-height: 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr) 360px;
  gap: 14px;
  overflow: hidden;
}

.graph-theater {
  position: relative;
  min-height: 1020px;
  overflow: hidden;
  border: 1px solid rgba(148, 163, 184, 0.24);
  border-radius: 8px;
  background: linear-gradient(180deg, #ffffff 0%, #f8fafc 54%, #eef4fb 100%);
  box-shadow: inset 0 0 0 1px rgba(226, 232, 240, 0.82);
}

.graph-theater::before {
  position: absolute;
  inset: 0;
  content: '';
  pointer-events: none;
  background:
    linear-gradient(rgba(148, 163, 184, 0.18) 1px, transparent 1px),
    linear-gradient(90deg, rgba(148, 163, 184, 0.18) 1px, transparent 1px);
  background-size: 34px 34px;
  mask-image: radial-gradient(circle at center, black 0%, black 58%, transparent 100%);
}

.g6-canvas {
  position: absolute;
  inset: 0;
  z-index: 1;
  cursor: grab;
}

.g6-canvas:active {
  cursor: grabbing;
}

.graph-hud {
  position: absolute;
  left: 14px;
  right: 14px;
  bottom: 12px;
  z-index: 2;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  pointer-events: none;
}

.graph-hud span {
  min-height: 24px;
  display: inline-flex;
  align-items: center;
  padding: 0 8px;
  border: 1px solid rgba(203, 213, 225, 0.86);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.76);
  color: #334155;
  font-size: 11px;
  font-weight: 750;
  backdrop-filter: blur(10px);
}

.graph-inspector {
  min-height: 0;
  display: grid;
  align-content: start;
  gap: 12px;
  overflow: auto;
  padding: 12px;
  border: 1px solid #dbe6ef;
  border-radius: 8px;
  background: #fff;
}

.inspector-title span {
  color: var(--theme-primary-active);
  font-size: 12px;
  font-weight: 850;
}

.inspector-title h5 {
  margin: 4px 0 0;
  color: #0f172a;
  font-size: 15px;
  line-height: 1.35;
  word-break: break-word;
}

.node-facts {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px;
}

.node-facts div,
.node-meta {
  min-width: 0;
  display: grid;
  gap: 4px;
  padding: 9px;
  border-radius: 8px;
  background: #f8fafc;
}

.node-meta {
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
}

.node-facts span,
.node-meta span {
  color: #64748b;
  font-size: 12px;
}

.node-facts strong,
.node-meta strong {
  min-width: 0;
  overflow: hidden;
  color: #0f172a;
  font-size: 15px;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.graph-empty,
.inspector-empty {
  min-height: 120px;
  display: grid;
  place-items: center;
  align-content: center;
  gap: 6px;
  color: #64748b;
  text-align: center;
}

.graph-empty {
  position: absolute;
  inset: 0;
  z-index: 1;
}

.graph-empty strong,
.inspector-empty strong {
  color: #0f172a;
  font-size: 14px;
}

.inspector-empty strong {
  color: #0f172a;
}

.graph-empty span,
.inspector-empty span {
  font-size: 12px;
}

@media (prefers-reduced-motion: reduce) {
  .graph-toolbar button {
    transition: none;
  }
}

@media (max-width: 980px) {
  .knowledge-graph-space {
    min-height: 980px;
    grid-template-rows: auto auto minmax(900px, 1fr);
  }

  .graph-head {
    flex-direction: column;
  }

  .graph-toolbar {
    justify-content: flex-start;
  }

  .graph-body {
    grid-template-columns: 1fr;
    grid-template-rows: minmax(780px, 1fr) minmax(220px, auto);
  }

  .graph-theater {
    min-height: 780px;
  }

  .graph-inspector {
    max-height: 240px;
  }
}

@media (max-width: 620px) {
  .graph-search {
    min-width: 100%;
  }

  .graph-toolbar {
    width: 100%;
  }
}
</style>
