<template>
	<div class="wiki-graph">
		<div class="wiki-graph__header">
			<div>
				<div class="wiki-graph__title">知识图谱</div>
				<div class="wiki-graph__desc">基于 Wiki 页面与关系边构建的图谱视图，可拖拽、缩放。</div>
				<div class="wiki-graph__summary">
					<div v-for="item in graphSummary" :key="item.label" class="wiki-graph-stat">
						<span class="wiki-graph-stat__label">{{ item.label }}</span>
						<span class="wiki-graph-stat__value">{{ item.value }}</span>
					</div>
				</div>
			</div>

			<div class="wiki-graph__legend">
				<div v-for="item in legendItems" :key="item.type" class="wiki-graph-legend">
					<span class="wiki-graph-legend__dot" :style="{ backgroundColor: item.color }"></span>
					<span>{{ item.label }}</span>
				</div>
			</div>
		</div>

		<div ref="graphContainerRef" class="wiki-graph__canvas" v-loading="graphLoading"></div>
	</div>
</template>

<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import * as d3 from 'd3'
import { wiki } from '@repo/api-service'
import type { KnowledgeBaseItem } from './types'

type WikiPageType = 'summary' | 'entity' | 'concept' | 'index' | 'log' | string

interface WikiGraphNode {
	id: string
	title: string
	slug: string
	page_type: WikiPageType
	linkCount: number
}

interface WikiGraphEdge {
	id: string
	source: string
	target: string
	weight: number
}

interface WikiStats {
	total_links: number
}

interface ForceNode extends WikiGraphNode {
	fx?: number | null
	fy?: number | null
	x?: number
	y?: number
}

interface ForceLink {
	id: string
	source: string | ForceNode
	target: string | ForceNode
	weight: number
}

const props = defineProps<{
	currentBase: KnowledgeBaseItem
}>()

const graphContainerRef = ref<HTMLDivElement>()
const graphLoading = ref(false)
const selectedSlug = ref('')
const stats = ref<WikiStats>({
	total_links: 0
})

const graphData = ref<{ nodes: WikiGraphNode[]; edges: WikiGraphEdge[] }>({
	nodes: [],
	edges: []
})

const legendItems = [
	{ type: 'summary', label: '摘要', color: '#1d6ff2' },
	{ type: 'entity', label: '实体', color: '#2cb67d' },
	{ type: 'concept', label: '概念', color: '#f08c24' },
	{ type: 'index', label: '索引', color: '#3fa7ff' },
	{ type: 'log', label: '日志', color: '#eb5757' }
]

const graphSummary = computed(() => {
	const duplicateCount =
		stats.value.total_links > graphData.value.edges.length
			? stats.value.total_links - graphData.value.edges.length
			: 0

	return [
		{ label: '节点数', value: graphData.value.nodes.length },
		{ label: '关系数', value: graphData.value.edges.length },
		{ label: '合并重复边', value: duplicateCount }
	]
})

let simulation: any = null

function getNodeColor(type: WikiPageType) {
	switch (type) {
		case 'summary':
			return '#1d6ff2'
		case 'entity':
			return '#2cb67d'
		case 'concept':
			return '#f08c24'
		case 'index':
			return '#3fa7ff'
		case 'log':
			return '#eb5757'
		default:
			return '#9aa4b2'
	}
}

function truncateLabel(label: string, max = 14) {
	return label.length > max ? `${label.slice(0, max)}...` : label
}

async function loadStats() {
	if (!props.currentBase.id) return
	const res = await wiki.postAiWikiStats({ knowledge_base_id: props.currentBase.id })
	if (res?.isSuccess) {
		stats.value = {
			total_links: res.result?.total_links || 0
		}
	}
}

async function loadGraph() {
	if (!props.currentBase.id) return

	graphLoading.value = true
	try {
		const res = await wiki.postAiWikiGraph({ knowledge_base_id: props.currentBase.id })
		if (res?.isSuccess) {
			const nodes = res.result?.nodes || []
			const nodeSlugSet = new Set(nodes.map((item) => item.slug).filter(Boolean))
			const edgeMap = new Map<string, WikiGraphEdge>()

			for (const item of res.result?.edges || []) {
				if (!item.source || !item.target) continue
				if (item.source === item.target) continue
				if (!nodeSlugSet.has(item.source) || !nodeSlugSet.has(item.target)) continue

				const edgeKey = `${item.source}__${item.target}`
				const current = edgeMap.get(edgeKey)

				if (current) {
					current.weight += 1
					continue
				}

				edgeMap.set(edgeKey, {
					id: item.id,
					source: item.source,
					target: item.target,
					weight: 1
				})
			}

			graphData.value = {
				nodes: nodes.map((item) => ({
					id: item.slug,
					title: item.title,
					slug: item.slug,
					page_type: item.page_type,
					linkCount: item.linkCount
				})),
				edges: Array.from(edgeMap.values())
			}
			await nextTick()
			renderGraph()
		}
	} finally {
		graphLoading.value = false
	}
}

function highlightSelectedNode(slug = '') {
	const root = graphContainerRef.value
	if (!root) return

	const relationSet = new Set<string>()

	if (slug) {
		for (const edge of graphData.value.edges) {
			if (edge.source === slug) relationSet.add(edge.target)
			if (edge.target === slug) relationSet.add(edge.source)
		}
	}

	root.querySelectorAll<SVGCircleElement>('.wiki-node-circle').forEach((node) => {
		const isActive = node.dataset.slug === slug
		const isRelated = relationSet.has(node.dataset.slug || '')

		node.setAttribute(
			'stroke',
			isActive ? '#ffffff' : isRelated ? 'rgba(255,255,255,0.92)' : 'rgba(255,255,255,0.78)'
		)
		node.setAttribute('stroke-width', isActive ? '3.5' : isRelated ? '2.2' : '1.5')
		node.setAttribute('opacity', slug && !isActive && !isRelated ? '0.35' : '1')
	})

	root.querySelectorAll<SVGLineElement>('.wiki-link-line').forEach((line) => {
		const source = line.dataset.source || ''
		const target = line.dataset.target || ''
		const isActive = !!slug && (source === slug || target === slug)

		line.setAttribute('stroke', isActive ? 'rgba(255,255,255,0.68)' : 'rgba(255,255,255,0.22)')
		line.setAttribute('opacity', slug && !isActive ? '0.18' : '1')
	})

	root.querySelectorAll<SVGTextElement>('.wiki-node-label').forEach((label) => {
		const isActive = label.dataset.slug === slug
		const isRelated = relationSet.has(label.dataset.slug || '')

		label.setAttribute('opacity', slug && !isActive && !isRelated ? '0.38' : '1')
		label.setAttribute('font-weight', isActive ? '700' : isRelated ? '600' : '500')
	})
}

function renderGraph() {
	const container = graphContainerRef.value
	if (!container) return

	simulation?.stop()
	container.innerHTML = ''

	const width = Math.max(container.clientWidth, 720)
	const height = Math.max(container.clientHeight, 520)

	const nodes: ForceNode[] = graphData.value.nodes.map((item) => ({ ...item }))
	const links: ForceLink[] = graphData.value.edges.map((item) => ({ ...item }))

	const svg = d3
		.select(container)
		.append('svg')
		.attr('width', width)
		.attr('height', height)
		.attr('viewBox', `0 0 ${width} ${height}`)

	const defs = svg.append('defs')

	defs
		.append('marker')
		.attr('id', 'wiki-arrow')
		.attr('viewBox', '0 -5 10 10')
		.attr('refX', 15)
		.attr('refY', 0)
		.attr('markerWidth', 4.5)
		.attr('markerHeight', 4.5)
		.attr('orient', 'auto')
		.append('path')
		.attr('d', 'M0,-5L10,0L0,5')
		.attr('fill', 'rgba(255,255,255,0.65)')

	const root = svg.append('g')

	svg.call(
		d3
			.zoom()
			.scaleExtent([0.4, 2.5])
			.on('zoom', (event: any) => {
				root.attr('transform', event.transform)
			})
	)

	const link = root
		.append('g')
		.selectAll('line')
		.data(links)
		.enter()
		.append('line')
		.attr('class', 'wiki-link-line')
		.attr('data-source', (d: ForceLink) => `${d.source}`)
		.attr('data-target', (d: ForceLink) => `${d.target}`)
		.attr('stroke', 'rgba(255,255,255,0.22)')
		.attr('stroke-width', (d: ForceLink) => 0.75 + Math.min(1.8, d.weight * 0.24))
		.attr('marker-end', 'url(#wiki-arrow)')

	const nodeGroup = root
		.append('g')
		.selectAll('g')
		.data(nodes)
		.enter()
		.append('g')
		.style('cursor', 'pointer')
		.on('click', (_event: any, datum: ForceNode) => {
			selectedSlug.value = datum.slug
			highlightSelectedNode(datum.slug)
		})

	nodeGroup
		.append('circle')
		.attr('class', 'wiki-node-circle')
		.attr('data-slug', (d: ForceNode) => d.slug)
		.attr('r', (d: ForceNode) => Math.max(8, Math.min(18, 8 + d.linkCount * 0.8)))
		.attr('fill', (d: ForceNode) => getNodeColor(d.page_type))
		.attr('stroke', 'rgba(255,255,255,0.9)')
		.attr('stroke-width', 1.5)

	nodeGroup
		.append('text')
		.attr('class', 'wiki-node-label')
		.attr('data-slug', (d: ForceNode) => d.slug)
		.text((d: ForceNode) => truncateLabel(d.title))
		.attr('fill', '#e5e7eb')
		.attr('font-size', 11)
		.attr('text-anchor', 'middle')
		.attr('font-weight', 500)
		.attr('dy', 24)

	simulation = d3
		.forceSimulation(nodes)
		.force(
			'link',
			d3
				.forceLink(links as any)
				.id((d: any) => d.id)
				.distance((d: ForceLink) => {
					const sourceCount = (d.source as ForceNode).linkCount || 0
					const targetCount = (d.target as ForceNode).linkCount || 0
					const density = Math.max(sourceCount, targetCount)
					return 44 + Math.min(48, density * 2) - Math.min(18, d.weight * 2)
				})
				.strength((d: ForceLink) => Math.min(0.95, 0.22 + d.weight * 0.12))
		)
		.force('charge', d3.forceManyBody().strength(-160))
		.force('center', d3.forceCenter(width / 2, height / 2))
		.force(
			'collision',
			d3.forceCollide().radius((d: any) => Math.max(16, 10 + d.linkCount * 0.8))
		)
		.force('x', d3.forceX(width / 2).strength(0.04))
		.force('y', d3.forceY(height / 2).strength(0.04))
		.on('tick', () => {
			link
				.attr('x1', (d: ForceLink) => (d.source as ForceNode).x || 0)
				.attr('y1', (d: ForceLink) => (d.source as ForceNode).y || 0)
				.attr('x2', (d: ForceLink) => (d.target as ForceNode).x || 0)
				.attr('y2', (d: ForceLink) => (d.target as ForceNode).y || 0)

			nodeGroup.attr('transform', (d: ForceNode) => `translate(${d.x || 0},${d.y || 0})`)
		})

	nodeGroup.call(
		d3
			.drag()
			.on('start', (event: any, d: ForceNode) => {
				if (!event.active) simulation?.alphaTarget(0.3).restart()
				d.fx = d.x
				d.fy = d.y
			})
			.on('drag', (event: any, d: ForceNode) => {
				d.fx = event.x
				d.fy = event.y
			})
			.on('end', (event: any, d: ForceNode) => {
				if (!event.active) simulation?.alphaTarget(0)
				d.fx = null
				d.fy = null
			})
	)

	highlightSelectedNode(selectedSlug.value)
}

async function loadAll() {
	if (!props.currentBase.id) return
	await loadStats()
	await loadGraph()
}

watch(
	() => props.currentBase.id,
	() => {
		selectedSlug.value = ''
		graphData.value = { nodes: [], edges: [] }
		stats.value = { total_links: 0 }
		if (props.currentBase.id) {
			void loadAll()
		}
	},
	{ immediate: true }
)

onMounted(() => {
	window.addEventListener('resize', renderGraph)
})

onBeforeUnmount(() => {
	window.removeEventListener('resize', renderGraph)
	simulation?.stop()
})
</script>

<style scoped lang="less">
.wiki-graph {
	height: calc(100vh - 250px);
	min-height: 520px;
	padding: 12px;
	border: 1px solid var(--border-light);
	border-radius: 10px;
	background: var(--bg-base);
	box-sizing: border-box;
}

.wiki-graph__header {
	display: flex;
	align-items: flex-start;
	justify-content: space-between;
	gap: 12px;
	margin-bottom: 10px;
}

.wiki-graph__title {
	font-size: 15px;
	font-weight: 700;
	color: var(--text-primary);
}

.wiki-graph__desc {
	margin-top: 4px;
	font-size: 12px;
	color: var(--text-secondary);
}

.wiki-graph__summary {
	display: flex;
	flex-wrap: wrap;
	gap: 10px;
	margin-top: 12px;
}

.wiki-graph-stat {
	display: inline-flex;
	align-items: center;
	gap: 8px;
	padding: 6px 10px;
	border: 1px solid var(--border-light);
	border-radius: 999px;
	background: var(--bg-container);
}

.wiki-graph-stat__label {
	font-size: 12px;
	color: var(--text-secondary);
}

.wiki-graph-stat__value {
	font-size: 12px;
	font-weight: 700;
	color: var(--text-primary);
}

.wiki-graph__legend {
	display: flex;
	flex-wrap: wrap;
	gap: 12px;
}

.wiki-graph-legend {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	font-size: 12px;
	color: var(--text-secondary);
}

.wiki-graph-legend__dot {
	width: 10px;
	height: 10px;
	border-radius: 999px;
}

.wiki-graph__canvas {
	height: calc(100% - 78px);
	min-height: 460px;
	border-radius: 12px;
	background:
		radial-gradient(circle at top, rgba(255, 255, 255, 0.08), transparent 42%),
		linear-gradient(180deg, #272727 0%, #202020 100%);
	overflow: hidden;
}

@media (max-width: 960px) {
	.wiki-graph {
		height: auto;
		min-height: 0;
	}

	.wiki-graph__header {
		flex-direction: column;
	}

	.wiki-graph__canvas {
		height: 420px;
		min-height: 420px;
	}
}
</style>
