<template>
  <div class="page">
    <div class="page-header">
      <div>
        <div class="page-title">拓扑图</div>
        <div class="page-subtitle">展示机器之间的服务指向和关联关系</div>
      </div>
      <el-button v-if="isAdmin" type="primary" :icon="Plus" @click="$router.push('/machines')">添加机器</el-button>
    </div>

    <div class="topo-card">
      <div id="topo" class="topo-canvas"></div>
      <div class="legend">
        <div class="legend-item"><span class="dot online"></span> 在线</div>
        <div class="legend-item"><span class="dot offline"></span> 离线</div>
        <div class="legend-item"><span class="line service"></span> 服务指向</div>
        <div class="legend-item"><span class="line relation"></span> 关联关系</div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { onMounted, ref, onUnmounted } from 'vue'
import { Graph } from '@antv/g6'
import { Plus } from '@element-plus/icons-vue'
import { fetchMachines, fetchAllServices as fetchServicesAll, fetchRelationships } from '@/api'
import { getAuth } from '@/router'

const isAdmin = getAuth().is_admin
let graph = null
let observer = null

function getIsDark() {
  return document.documentElement.classList.contains('dark')
}

function themeFill() {
  return getIsDark() ? '#0f172a' : '#ffffff'
}

function themeText() {
  return getIsDark() ? '#f8fafc' : '#111827'
}

function themeBg() {
  return getIsDark() ? '#020617' : '#fafafa'
}

function renderGraph() {
  const container = document.getElementById('topo')
  if (!container || !graph) return
  container.style.background = themeBg()
  graph.getNodes().forEach(node => {
    const model = node.getModel()
    graph.updateItem(node, {
      style: {
        fill: themeFill(),
        stroke: osColor(model.os),
        lineWidth: 2,
        radius: 8,
        shadowColor: getIsDark() ? 'rgba(0,0,0,0.5)' : 'rgba(0,0,0,0.06)',
        shadowBlur: 8,
        shadowOffsetY: 2
      },
      labelCfg: {
        style: { fill: themeText(), fontSize: 13, fontWeight: 600 }
      },
      badgeCfg: {
        position: 'topRight',
        style: {
          fill: model.online ? '#22c55e' : '#ef4444',
          stroke: themeFill(),
          lineWidth: 2
        }
      }
    })
  })
  graph.getEdges().forEach(edge => {
    const model = edge.getModel()
    const isRel = String(model.id).startsWith('rel-')
    const color = isRel ? '#f97316' : '#3b82f6'
    graph.updateItem(edge, {
      labelCfg: {
        style: {
          fill: color,
          fontSize: 11,
          fontWeight: 500,
          background: { fill: themeFill(), padding: [2, 4], radius: 4 }
        }
      }
    })
  })
  graph.paint()
}

onMounted(async () => {
  const [machinesRes, servicesRes, relsRes] = await Promise.all([
    fetchMachines(),
    fetchServicesAll(),
    fetchRelationships()
  ])

  const machines = machinesRes.data
  const services = servicesRes.data
  const relationships = relsRes.data

  const machineMap = {}
  machines.forEach(m => machineMap[m.id] = m)

  const isDark = getIsDark()
  const fill = themeFill()
  const text = themeText()

  const nodes = machines.map(m => ({
    id: String(m.id),
    label: m.hostname,
    online: m.is_online,
    os: m.os_type,
    style: {
      fill,
      stroke: osColor(m.os_type),
      lineWidth: 2,
      radius: 8,
      shadowColor: isDark ? 'rgba(0,0,0,0.5)' : 'rgba(0,0,0,0.06)',
      shadowBlur: 8,
      shadowOffsetY: 2
    },
    labelCfg: {
      style: { fill: text, fontSize: 13, fontWeight: 600 }
    },
    badgeCfg: {
      position: 'topRight',
      style: {
        fill: m.is_online ? '#22c55e' : '#ef4444',
        stroke: fill,
        lineWidth: 2
      }
    }
  }))

  const edges = []

  relationships.forEach(r => {
    if (!machineMap[r.source_machine_id] || !machineMap[r.target_machine_id]) return
    edges.push({
      id: `rel-${r.id}`,
      source: String(r.source_machine_id),
      target: String(r.target_machine_id),
      label: relLabel(r),
      style: {
        stroke: '#f97316',
        lineDash: [4, 2],
        lineWidth: 1.5,
        endArrow: { path: 'M 0,0 L 8,4 L 8,-4 Z', fill: '#f97316' }
      },
      labelCfg: {
        style: { fill: '#f97316', fontSize: 11, fontWeight: 500, background: { fill, padding: [2, 4], radius: 4 } }
      }
    })
  })

  services.forEach(s => {
    if (s.target_machine_id && machineMap[s.target_machine_id]) {
      edges.push({
        id: `svc-${s.id}`,
        source: String(s.machine_id),
        target: String(s.target_machine_id),
        label: s.name,
        style: {
          stroke: '#3b82f6',
          lineWidth: 2,
          endArrow: { path: 'M 0,0 L 8,4 L 8,-4 Z', fill: '#3b82f6' }
        },
        labelCfg: {
          style: { fill: '#3b82f6', fontSize: 11, fontWeight: 500, background: { fill, padding: [2, 4], radius: 4 } }
        }
      })
    }
  })

  const container = document.getElementById('topo')
  container.style.background = themeBg()

  graph = new Graph({
    container: 'topo',
    width: container.clientWidth,
    height: container.clientHeight,
    layout: {
      type: 'force',
      preventOverlap: true,
      linkDistance: 140,
      nodeStrength: -80,
      edgeStrength: 0.2
    },
    defaultNode: {
      type: 'rect',
      size: [110, 40]
    },
    defaultEdge: {
      type: 'line',
      style: { endArrow: true }
    },
    modes: { default: ['drag-node', 'drag-canvas', 'zoom-canvas'] },
    fitView: true,
    fitViewPadding: 20
  })

  graph.data({ nodes, edges })
  graph.render()

  graph.on('node:click', e => {
    const nodeId = e.item.getModel().id
    highlightNode(nodeId)
  })

  window.addEventListener('resize', () => {
    const c = document.getElementById('topo')
    if (c && graph) {
      graph.changeSize(c.clientWidth, c.clientHeight)
      graph.fitView()
    }
  })

  // Watch for theme changes
  observer = new MutationObserver(() => {
    renderGraph()
  })
  observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] })
})

onUnmounted(() => {
  if (observer) observer.disconnect()
  if (graph) {
    graph.destroy()
    graph = null
  }
})

function highlightNode(nodeId) {
  if (!graph) return
  const edges = graph.getEdges()
  edges.forEach(edge => {
    const model = edge.getModel()
    if (model.source === nodeId || model.target === nodeId) {
      graph.setItemState(edge, 'active', true)
      edge.update({ style: { opacity: 1 } })
    } else {
      graph.setItemState(edge, 'inactive', true)
      edge.update({ style: { opacity: 0.15 } })
    }
  })
  setTimeout(() => {
    edges.forEach(edge => {
      graph.clearItemStates(edge)
      edge.update({ style: { opacity: 1 } })
    })
  }, 2000)
}

function osColor(os) {
  switch (os) {
    case 'Linux': return '#3b82f6'
    case 'Windows': return '#06b6d4'
    case 'macOS': return '#a855f7'
    default: return '#9ca3af'
  }
}

function relLabel(r) {
  const typeMap = { port_forward: '端口转发', dependency: '依赖', primary_secondary: '主从', custom: '自定义' }
  const t = typeMap[r.relation_type] || r.relation_type
  return r.source_port ? `${t} (${r.source_port})` : t
}
</script>

<style scoped>
.page-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 16px;
}
.page-title {
  font-size: 18px;
  font-weight: 700;
}
.page-subtitle {
  font-size: 13px;
  color: var(--text-secondary);
  margin-top: 2px;
}

.topo-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 12px;
  position: relative;
  height: calc(100vh - 220px);
  min-height: 420px;
}

.topo-canvas {
  width: 100%;
  height: 100%;
  border-radius: 10px;
  overflow: hidden;
  transition: background .2s ease;
}

.legend {
  position: absolute;
  left: 16px;
  bottom: 16px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 10px 12px;
  display: flex;
  gap: 14px;
  box-shadow: var(--shadow);
  backdrop-filter: blur(6px);
  transition: background .2s ease, border-color .2s ease;
}
.legend-item {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  color: var(--text-secondary);
}
.dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
}
.dot.online { background: #22c55e; }
.dot.offline { background: #ef4444; }
.line {
  width: 16px;
  height: 2px;
  border-radius: 1px;
}
.line.service { background: #3b82f6; }
.line.relation {
  background: repeating-linear-gradient(90deg, #f97316, #f97316 4px, transparent 4px, transparent 6px);
  height: 2px;
}
</style>
