import { computed, ref, watch } from 'vue'

import { useThemeColors } from '../../composables/useThemeColors.js'

const MAX_VISIBLE_NODES = 72
const MAX_VISIBLE_EDGES = 180
const MAX_RELATION_PREVIEW = 40

const BASE_NODE_TONES = {
  hub: {
    fill: '#2563eb',
    stroke: '#dbeafe',
    halo: '#93c5fd',
    shadow: 'rgba(37, 99, 235, 0.20)'
  },
  strong: {
    fill: '#3a7ca5',
    stroke: '#eaf4fa',
    halo: '#d4e8f3',
    shadow: 'rgba(58, 124, 165, 0.18)'
  },
  accent: {
    fill: '#d97706',
    stroke: '#fef3c7',
    halo: '#fbbf24',
    shadow: 'rgba(217, 119, 6, 0.16)'
  },
  normal: {
    fill: '#4f46e5',
    stroke: '#e0e7ff',
    halo: '#a5b4fc',
    shadow: 'rgba(79, 70, 229, 0.16)'
  },
  muted: {
    fill: '#64748b',
    stroke: '#e2e8f0',
    halo: '#cbd5e1',
    shadow: 'rgba(100, 116, 139, 0.12)'
  }
}

export function useKnowledgeIngestGraph(props) {
  const graphQuery = ref('')
  const activeNodeId = ref('')
  const themeColors = useThemeColors()

  const allRelations = computed(() => normalizeRelations(props.graph?.relations))
  const nodeTones = computed(() => ({
    ...BASE_NODE_TONES,
    strong: {
      fill: themeColors.value.chartPrimary,
      stroke: themeColors.value.primarySoft,
      halo: themeColors.value.primarySoft,
      shadow: toRgba(themeColors.value.chartPrimary, 0.18)
    }
  }))
  const rankedNodes = computed(() => buildRankedNodes(props.graph, allRelations.value))
  const visibleNodes = computed(() => {
    const query = graphQuery.value.toLowerCase()
    return rankedNodes.value.slice(0, MAX_VISIBLE_NODES).map((node, index) => ({
      ...node,
      rank: index + 1,
      matchesQuery: query ? node.name.toLowerCase().includes(query) : true
    }))
  })
  const visibleNodeNameSet = computed(() => new Set(visibleNodes.value.map((node) => node.name)))
  const visibleRelations = computed(() =>
    allRelations.value
      .filter(
        (relation) =>
          visibleNodeNameSet.value.has(relation.source) && visibleNodeNameSet.value.has(relation.target)
      )
      .slice(0, MAX_VISIBLE_EDGES)
  )
  const nodeIdByName = computed(() =>
    new Map(visibleNodes.value.map((node) => [node.name, node.id]))
  )
  const selectedNode = computed(() => {
    if (!visibleNodes.value.length) return null
    return visibleNodes.value.find((node) => node.id === activeNodeId.value) || visibleNodes.value[0]
  })
  const selectedNodeRelations = computed(() => {
    if (!selectedNode.value) return []
    return allRelations.value
      .filter(
        (relation) =>
          relation.source === selectedNode.value.name || relation.target === selectedNode.value.name
      )
      .slice(0, MAX_RELATION_PREVIEW)
  })
  const selectedNodeIncoming = computed(() =>
    selectedNodeRelations.value.filter((relation) => relation.target === selectedNode.value?.name)
  )
  const selectedNodeOutgoing = computed(() =>
    selectedNodeRelations.value.filter((relation) => relation.source === selectedNode.value?.name)
  )
  const graphData = computed(() => ({
    nodes: visibleNodes.value.map((node) => toG6Node(node, nodeTones.value)),
    edges: visibleRelations.value
      .map((relation, index) => toG6Edge(relation, index, nodeIdByName.value))
      .filter(Boolean)
  }))
  const graphSummary = computed(() => ({
    entityCount: formatNumber(resolveCount(props.graph?.entityCount, props.graph?.entity_count, rankedNodes.value.length)),
    relationCount: formatNumber(resolveCount(props.graph?.relationCount, props.graph?.relation_count, allRelations.value.length)),
    visibleNodeCount: formatNumber(visibleNodes.value.length),
    visibleEdgeCount: formatNumber(visibleRelations.value.length)
  }))

  watch(
    visibleNodes,
    (nextNodes) => {
      if (!nextNodes.length) {
        activeNodeId.value = ''
        return
      }
      if (!nextNodes.some((node) => node.id === activeNodeId.value)) {
        activeNodeId.value = nextNodes[0].id
      }
    },
    { immediate: true }
  )

  function selectNodeById(nodeId) {
    const matchedNode = visibleNodes.value.find((node) => node.id === nodeId)
    if (!matchedNode) return ''
    activeNodeId.value = matchedNode.id
    return matchedNode.id
  }

  function focusFirstMatch() {
    const matchedNode = visibleNodes.value.find((node) => node.matchesQuery)
    if (!matchedNode) return ''
    activeNodeId.value = matchedNode.id
    return matchedNode.id
  }

  function selectRelationPeer(relation) {
    if (!selectedNode.value) return ''
    const peerName = relation.source === selectedNode.value.name ? relation.target : relation.source
    const peerId = nodeIdByName.value.get(peerName)
    if (!peerId) return ''
    activeNodeId.value = peerId
    return peerId
  }

  return {
    activeNodeId,
    graphData,
    graphQuery,
    graphSummary,
    selectNodeById,
    focusFirstMatch,
    selectRelationPeer,
    selectedNode,
    selectedNodeIncoming,
    selectedNodeOutgoing,
    selectedNodeRelations,
    truncateText,
    visibleNodes,
    visibleRelations
  }
}

function buildRankedNodes(graph, relations) {
  const degreeMap = new Map()
  for (const relation of relations) {
    degreeMap.set(relation.source, (degreeMap.get(relation.source) || 0) + 1)
    degreeMap.set(relation.target, (degreeMap.get(relation.target) || 0) + 1)
  }

  const byName = new Map()
  for (const entity of normalizeEntities(graph?.entities)) {
    byName.set(entity.name, entity)
  }
  for (const relation of relations) {
    if (!byName.has(relation.source)) byName.set(relation.source, { name: relation.source, type: '关系实体' })
    if (!byName.has(relation.target)) byName.set(relation.target, { name: relation.target, type: '关系实体' })
  }

  return [...byName.values()]
    .map((entity) => ({
      ...entity,
      id: toNodeId(entity.name),
      degree: degreeMap.get(entity.name) || 0
    }))
    .sort((left, right) => {
      const degreeDelta = right.degree - left.degree
      if (degreeDelta !== 0) return degreeDelta
      return left.name.localeCompare(right.name, 'zh-CN')
    })
}

function toG6Node(node, nodeTones) {
  const tone = resolveNodeTone(node)
  const palette = nodeTones[tone] || nodeTones.normal || BASE_NODE_TONES.normal
  const size = clamp(34 + Math.sqrt(Math.max(node.degree, 1)) * 13, 38, node.rank === 1 ? 82 : 70)
  const opacity = node.matchesQuery ? 1 : 0.24

  return {
    id: node.id,
    data: {
      name: node.name,
      type: node.type || '实体',
      description: node.description || '',
      descriptions: node.descriptions || [],
      properties: node.properties || {},
      labels: node.labels || [],
      degree: node.degree,
      rank: node.rank,
      matchesQuery: node.matchesQuery,
      tone
    },
    style: {
      size,
      fill: palette.fill,
      stroke: palette.stroke,
      lineWidth: 2,
      opacity,
      shadowColor: palette.shadow,
      shadowBlur: 14,
      label: true,
      labelText: truncateText(node.name, node.rank === 1 ? 16 : 12),
      labelFill: '#0f172a',
      labelFontSize: node.rank === 1 ? 13 : 12,
      labelFontWeight: 850,
      labelPlacement: 'bottom',
      labelOffsetY: 10,
      labelBackground: true,
      labelBackgroundFill: 'rgba(255, 255, 255, 0.94)',
      labelBackgroundStroke: 'rgba(203, 213, 225, 0.95)',
      labelBackgroundLineWidth: 1,
      labelBackgroundRadius: 4,
      labelBackgroundPadding: [2, 6],
      halo: true,
      haloStroke: palette.halo,
      haloLineWidth: 10,
      haloStrokeOpacity: 0.16,
      badge: node.degree > 0,
      badges: [
        {
          text: String(node.degree),
          placement: 'right-top',
          fill: 'rgba(255, 255, 255, 0.94)',
          stroke: 'rgba(37, 99, 235, 0.28)',
          color: '#1e293b',
          fontSize: 10,
          padding: [2, 5]
        }
      ]
    },
    states: node.matchesQuery ? [] : ['dimmed']
  }
}

function toRgba(hexColor, alpha) {
  const normalized = String(hexColor || '').trim()
  const matched = normalized.match(/^#([0-9a-f]{6})$/i)
  if (!matched) {
    return `rgba(58, 124, 165, ${alpha})`
  }

  const value = matched[1]
  const red = parseInt(value.slice(0, 2), 16)
  const middle = parseInt(value.slice(2, 4), 16)
  const blue = parseInt(value.slice(4, 6), 16)
  return `rgba(${red}, ${middle}, ${blue}, ${alpha})`
}

function toG6Edge(relation, index, nodeIdByName) {
  const sourceId = nodeIdByName.get(relation.source)
  const targetId = nodeIdByName.get(relation.target)
  if (!sourceId || !targetId || sourceId === targetId) return null
  const weight = clamp(Number(relation.weight || relation.confidence || 1), 1, 6)

  return {
    id: `edge-${index}-${sourceId}-${targetId}`,
    source: sourceId,
    target: targetId,
    data: {
      sourceName: relation.source,
      targetName: relation.target,
      type: relation.type,
      description: relation.description || '',
      keywords: relation.keywords || [],
      properties: relation.properties || {},
      weight
    },
    style: {
      stroke: 'rgba(37, 99, 235, 0.34)',
      lineWidth: clamp(1 + weight * 0.22, 1.2, 2.6),
      opacity: 0.9,
      endArrow: true,
      endArrowSize: 8,
      label: index < 42,
      labelText: truncateText(relation.type, 10),
      labelFill: '#0f172a',
      labelFontSize: 10,
      labelFontWeight: 800,
      labelBackground: true,
      labelBackgroundFill: 'rgba(255, 255, 255, 0.94)',
      labelBackgroundStroke: 'rgba(203, 213, 225, 0.95)',
      labelBackgroundLineWidth: 1,
      labelBackgroundRadius: 5,
      labelBackgroundPadding: [2, 5]
    },
    states: []
  }
}

function resolveNodeTone(node) {
  if (node.rank === 1 || node.degree >= 6) return 'hub'
  if (node.degree >= 4) return 'strong'
  if (node.degree >= 2 && node.rank % 3 === 0) return 'accent'
  if (node.degree === 0) return 'muted'
  return 'normal'
}

function normalizeRelations(rawRelations) {
  if (!Array.isArray(rawRelations)) return []
  const seen = new Set()
  return rawRelations
    .map((relation) => ({
      source: String(relation?.source || relation?.from || relation?.head || '').trim(),
      target: String(relation?.target || relation?.to || relation?.tail || '').trim(),
      type: String(relation?.type || relation?.relation || relation?.label || '关联').trim(),
      description: String(relation?.description || '').trim(),
      keywords: dedupeTextList(relation?.keywords || []),
      properties: normalizeProperties(relation?.properties),
      weight: relation?.weight ?? relation?.confidence ?? 1
    }))
    .filter((relation) => relation.source && relation.target)
    .filter((relation) => {
      const key = `${relation.source}\u0000${relation.target}\u0000${relation.type}`
      if (seen.has(key)) return false
      seen.add(key)
      return true
    })
}

function normalizeEntities(rawEntities) {
  if (!Array.isArray(rawEntities)) return []
  const seen = new Set()
  return rawEntities
    .map((entity) => {
      if (typeof entity === 'string') {
        return {
          name: entity.trim(),
          type: '实体',
          description: '',
          descriptions: [],
          properties: {},
          labels: []
        }
      }
      return {
        name: String(
          entity?.name || entity?.entity || entity?.entity_id || entity?.title || entity?.id || ''
        ).trim(),
        type: String(entity?.type || entity?.entity_type || entity?.category || entity?.kind || '实体').trim(),
        description: String(entity?.description || '').trim(),
        descriptions: normalizeDescriptions(entity),
        properties: normalizeProperties(entity?.properties),
        labels: dedupeTextList(entity?.labels || [])
      }
    })
    .filter((entity) => entity.name)
    .filter((entity) => {
      if (seen.has(entity.name)) return false
      seen.add(entity.name)
      return true
    })
}

function toNodeId(value) {
  const text = String(value || '').trim()
  let hash = 0
  for (let index = 0; index < text.length; index += 1) {
    hash = (hash << 5) - hash + text.charCodeAt(index)
    hash |= 0
  }
  return `node-${Math.abs(hash).toString(36)}`
}

function normalizeDescriptions(entity) {
  const descriptions = dedupeTextList(entity?.descriptions)
  if (descriptions.length) return descriptions
  const description = String(entity?.description || '').trim()
  return description ? [description] : []
}

function normalizeProperties(rawProperties) {
  if (!rawProperties || typeof rawProperties !== 'object' || Array.isArray(rawProperties)) {
    return {}
  }
  const hiddenKeys = new Set(['source_id', 'file_path', 'truncate'])
  return Object.fromEntries(
    Object.entries(rawProperties)
      .filter(([key, value]) => !hiddenKeys.has(key) && String(value || '').trim())
      .map(([key, value]) => [key, String(value).trim()])
  )
}

function truncateText(value, maxLength) {
  const text = String(value || '').trim()
  if (text.length <= maxLength) return text
  return `${text.slice(0, maxLength - 1)}...`
}

function resolveCount(...values) {
  for (const value of values) {
    const number = Number(value)
    if (Number.isFinite(number) && number >= 0) return number
  }
  return 0
}

function formatNumber(value) {
  return new Intl.NumberFormat('zh-CN').format(resolveCount(value))
}

function dedupeTextList(items) {
  const result = []
  const seen = new Set()
  const sourceItems = Array.isArray(items)
    ? items
    : String(items || '')
        .split('<SEP>')
        .filter(Boolean)
  for (const item of sourceItems) {
    const text = String(item || '').trim()
    if (!text || seen.has(text)) continue
    seen.add(text)
    result.push(text)
  }
  return result
}

function clamp(value, min, max) {
  return Math.max(min, Math.min(max, value))
}
