"use client"

import { useState, useRef, useEffect } from "react"
import { PageHeader } from "@/components/page-header"
import { servers, greTunnels, type Server, type ServerType } from "@/lib/data"
import { Button } from "@/components/ui/button"
import { StatusBadge } from "@/components/status-badge"
import {
  DownloadIcon, XIcon, LockIcon, LockOpenIcon, WifiIcon,
  ZoomInIcon, ZoomOutIcon, Maximize2Icon, SearchIcon,
  LayersIcon, MapIcon, HomeIcon, TerminalIcon, ShieldIcon,
  CableIcon, CopyIcon, ActivityIcon, ExternalLinkIcon,
} from "lucide-react"
import { cn } from "@/lib/utils"

// ─── Resource metrics (mirrors uptime/page.tsx INIT_RESOURCES formula) ───────

interface SrvRes {
  cpu: number
  cpuHistory: number[]
  ramUsed: number; ramTotal: number
  hddUsed: number; hddTotal: number
  uptimeSeconds: number
  temp?: number
}

const BOARD_MAP: Record<ServerType, string> = {
  "jump-host":   "RB5009UG+S+IN",
  "exit-node":   "RB4011iGS+RM",
  "home-router": "hAP ax²",
}

const SERVER_RESOURCES: Record<string, SrvRes> = Object.fromEntries(
  servers.map(s => {
    const h        = s.id.split("").reduce((a, c) => a + c.charCodeAt(0), 0)
    const cpu      = 4 + (h % 68)
    const ramTotal = s.type === "jump-host" ? 8192 : s.type === "exit-node" ? 4096 : 1024
    const hddTotal = s.type === "home-router" ? 2048 : 16384
    const ramPct   = 12 + (h % 72)
    const hddPct   = 8  + (h % 75)
    return [s.id, {
      cpu,
      cpuHistory: Array.from({ length: 32 }, (_, i) =>
        Math.max(1, Math.min(99, cpu + Math.round(Math.sin(i * 0.7 + h * 0.1) * 15))),
      ),
      ramUsed:  Math.round(ramTotal * ramPct / 100), ramTotal,
      hddUsed:  Math.round(hddTotal * hddPct / 100), hddTotal,
      uptimeSeconds: (1 + h % 200) * 86400 + (h % 24) * 3600 + (h % 60) * 60,
      temp: s.type !== "home-router" ? 34 + (h % 32) : undefined,
    } satisfies SrvRes]
  })
)

function fmtMB(mb: number): string {
  if (mb >= 1024) return `${(mb / 1024).toFixed(mb >= 10240 ? 0 : 1)} ГБ`
  return `${mb} МБ`
}
function fmtUptime(sec: number): string {
  const d = Math.floor(sec / 86400)
  const h = Math.floor((sec % 86400) / 3600)
  const m = Math.floor((sec % 3600) / 60)
  if (d > 0) return `${d}д ${h}ч`
  if (h > 0) return `${h}ч ${m}м`
  return `${m}м`
}
function resBarColor(pct: number): string {
  if (pct >= 85) return "bg-red-500"
  if (pct >= 70) return "bg-amber-500"
  return "bg-emerald-500"
}
function resPctColor(pct: number): string {
  if (pct >= 85) return "text-red-400"
  if (pct >= 70) return "text-amber-400"
  return "text-emerald-400"
}

function MiniBar({ pct }: { pct: number }) {
  return (
    <div className="h-1.5 flex-1 rounded-full bg-muted overflow-hidden">
      <div
        className={cn("h-full rounded-full transition-all duration-700", resBarColor(pct))}
        style={{ width: `${Math.min(100, Math.max(0, pct))}%` }}
      />
    </div>
  )
}

function Sparkline({ history }: { history: number[] }) {
  const max = Math.max(...history, 1)
  const W = 80, H = 24
  const pts = history.map((v, i) => {
    const x = (i / (history.length - 1)) * W
    const y = H - (v / max) * H
    return `${x},${y}`
  }).join(" ")
  return (
    <svg width={W} height={H} className="shrink-0">
      <polyline points={pts} fill="none" stroke="currentColor" strokeWidth="1.2"
        className="text-emerald-500/60" strokeLinejoin="round" strokeLinecap="round" />
    </svg>
  )
}

// ─── Canvas dimensions ────────────────────────────────────────────────────────

const W = 1060
const H = 580
const ZOOM_MIN = 0.2
const ZOOM_MAX = 6

// ─── Visual config ────────────────────────────────────────────────────────────

const STATUS_STYLE = {
  online:   { fill: "#0f2d1f", stroke: "#4ade80", text: "#4ade80", glow: "rgba(74,222,128,0.15)"  },
  degraded: { fill: "#2d1e06", stroke: "#fbbf24", text: "#fbbf24", glow: "rgba(251,191,36,0.15)"  },
  offline:  { fill: "#2d0f0f", stroke: "#f87171", text: "#f87171", glow: "transparent"             },
}

const TYPE_STYLE: Record<ServerType, { label: string; fill: string; r: number }> = {
  "jump-host":   { label: "JH", fill: "#7c3aed", r: 36 },
  "exit-node":   { label: "EN", fill: "#0369a1", r: 36 },
  "home-router": { label: "HR", fill: "#16a34a", r: 30 },
}

const WAN_COLORS = ["#0ea5e9", "#f97316", "#a855f7", "#ec4899", "#14b8a6"]

const TUNNEL_STYLE = {
  up:       { stroke: "#4ade80", opacity: 0.5,  dotOpacity: 0.85 },
  degraded: { stroke: "#fbbf24", opacity: 0.4,  dotOpacity: 0.7  },
  down:     { stroke: "#f87171", opacity: 0.15, dotOpacity: 0    },
}

// ─── Positions ────────────────────────────────────────────────────────────────

const POS: Record<string, { x: number; y: number }> = {
  srv1:  { x: 500, y: 170 },
  srv2:  { x: 130, y: 75  },
  srv3:  { x: 820, y: 75  },
  srv4:  { x: 910, y: 265 },
  srv5:  { x: 700, y: 385 },
  srv6:  { x: 310, y: 345 },
  srv7:  { x: 125, y: 255 },
  home1: { x: 270, y: 470 },
  home2: { x: 790, y: 470 },
}

const WAN_SAT_POS: Record<string, { x: number; y: number }[]> = {
  home1: [{ x: 155, y: 510 }, { x: 385, y: 510 }],
  home2: [{ x: 665, y: 510 }, { x: 915, y: 510 }],
}

// ─── WAN→JH edges ────────────────────────────────────────────────────────────

interface WanJhEdge {
  homeId: string; wanIdx: number; jhId: string
  pingMs: number; dlMbps: number; active: boolean
}

const WAN_JH_EDGES: WanJhEdge[] = [
  { homeId: "home1", wanIdx: 0, jhId: "srv1", pingMs: 4,  dlMbps: 480, active: true  },
  { homeId: "home1", wanIdx: 0, jhId: "srv7", pingMs: 6,  dlMbps: 480, active: true  },
  { homeId: "home1", wanIdx: 1, jhId: "srv1", pingMs: 12, dlMbps: 95,  active: false },
  { homeId: "home2", wanIdx: 0, jhId: "srv1", pingMs: 18, dlMbps: 280, active: true  },
  { homeId: "home2", wanIdx: 0, jhId: "srv7", pingMs: 20, dlMbps: 278, active: false },
  { homeId: "home2", wanIdx: 1, jhId: "srv1", pingMs: 22, dlMbps: 185, active: false },
]

// ─── Tunnel probes ────────────────────────────────────────────────────────────

interface TunnelProbe { pingMs: number | null; dlMbps: number | null; ulMbps: number | null }

const TUNNEL_PROBES: Record<string, TunnelProbe> = {
  gre1: { pingMs: 28,   dlMbps: 614, ulMbps: 421 },
  gre2: { pingMs: 41,   dlMbps: 488, ulMbps: 352 },
  gre3: { pingMs: 88,   dlMbps: 143, ulMbps: 104 },
  gre4: { pingMs: null, dlMbps: null, ulMbps: null },
  gre5: { pingMs: 14,   dlMbps: 578, ulMbps: 412 },
  gre6: { pingMs: 92,   dlMbps: 155, ulMbps: 112 },
}

const TYPE_LABELS: Record<ServerType, string> = {
  "jump-host":   "JumpHost",
  "exit-node":   "Exit Node",
  "home-router": "Home Router",
}

// ─── Helpers ──────────────────────────────────────────────────────────────────

function pingColor(ms: number | null) {
  if (ms == null) return "#f87171"
  if (ms < 15) return "#4ade80"
  if (ms < 50) return "#fbbf24"
  return "#fb923c"
}
function latencyColor(ms: number | null) {
  if (ms == null) return "#f87171"
  if (ms > 60)   return "#fbbf24"
  return "#4ade80"
}
function shortName(name: string) {
  return name.replace(/^(home|mt)-/, "").split("-").slice(0, 3).join("-")
}

// ─── Filter ───────────────────────────────────────────────────────────────────

type FilterKey = "all" | "online" | "degraded" | "offline" | "jump-host" | "exit-node" | "home-router"

// ─── SVG sub-components ───────────────────────────────────────────────────────

function PingBadge({ mx, my, ping, dl, ul, color }: {
  mx: number; my: number
  ping: number | null; dl?: number | null; ul?: number | null
  color: string
}) {
  const hasSpeed = dl != null && dl > 0
  const h = hasSpeed ? 36 : 20
  return (
    <g transform={`translate(${mx},${my})`}>
      <rect x="-30" y={-h / 2} width="60" height={h} rx="4"
        fill="#060d1a" stroke={color} strokeWidth="0.7" opacity="0.92" />
      <text textAnchor="middle" y={hasSpeed ? "-3" : "4"}
        fontSize="8.5" fontWeight="600" fill={color} fontFamily="ui-monospace,monospace">
        {ping == null ? "timeout" : `${ping} мс`}
      </text>
      {hasSpeed && (
        <text textAnchor="middle" y="10" fontSize="7" fill="#64748b" fontFamily="ui-monospace,monospace">
          {`↓${dl} ↑${ul}`}
        </text>
      )}
    </g>
  )
}

function SvgTooltip({ n }: { n: Server & { x: number; y: number } }) {
  const ss = STATUS_STYLE[n.status]
  const ts = TYPE_STYLE[n.type]
  const lc = latencyColor(n.latency)
  const ox = n.x + ts.r + 14
  const oy = n.y - 32
  const lines = [
    n.name,
    `${TYPE_LABELS[n.type]} · ${n.site}`,
    n.latency != null ? `${n.latency} мс` : "Офлайн",
    n.model ?? "",
  ].filter(Boolean)
  const bw = 148, bh = lines.length * 14 + 14
  return (
    <g transform={`translate(${ox},${oy})`} style={{ pointerEvents: "none" }}>
      <rect x="0" y="0" width={bw} height={bh} rx="6"
        fill="#0a1628" stroke={ss.stroke} strokeWidth="0.8" opacity="0.97" />
      {/* colour accent line */}
      <rect x="0" y="0" width="3" height={bh} rx="2" fill={ts.fill} />
      {lines.map((l, i) => (
        <text key={i}
          x="10" y={12 + i * 14}
          fontSize={i === 0 ? "9.5" : "8"} fontWeight={i === 0 ? "700" : "400"}
          fill={i === 2 ? lc : i === 0 ? "#f1f5f9" : "#94a3b8"}
          fontFamily="ui-monospace,monospace">
          {l}
        </text>
      ))}
    </g>
  )
}

function ServerNode({ n, isSel, isVis, isDragged, onClick, onDblClick, onHoverChange, onContextMenu, onMouseDown }: {
  n: Server & { x: number; y: number }
  isSel: boolean; isVis: boolean; isDragged: boolean
  onClick: () => void
  onDblClick: () => void
  onHoverChange: (hov: boolean) => void
  onContextMenu: (e: React.MouseEvent) => void
  onMouseDown: (e: React.MouseEvent) => void
}) {
  const ss = STATUS_STYLE[n.status]
  const ts = TYPE_STYLE[n.type]
  const lc = latencyColor(n.latency)

  return (
    <g transform={`translate(${n.x},${n.y})`}
      style={{ cursor: isDragged ? "grabbing" : "grab", transition: isDragged ? "none" : "opacity 0.25s" }}
      opacity={isVis ? 1 : 0.07}
      onMouseDown={e => { e.stopPropagation(); onMouseDown(e) }}
      onClick={e => { e.stopPropagation(); onClick() }}
      onDoubleClick={e => { e.stopPropagation(); onDblClick() }}
      onContextMenu={e => { e.preventDefault(); e.stopPropagation(); onContextMenu(e) }}
      onMouseEnter={() => onHoverChange(true)}
      onMouseLeave={() => onHoverChange(false)}
    >
      {n.status === "online" && isVis && (
        <circle r={ts.r + 14} fill={ss.glow} opacity="0.7">
          <animate attributeName="r" values={`${ts.r+10};${ts.r+18};${ts.r+10}`} dur="3s" repeatCount="indefinite" />
          <animate attributeName="opacity" values="0.8;0.15;0.8" dur="3s" repeatCount="indefinite" />
        </circle>
      )}
      {n.status === "degraded" && isVis && (
        <circle r={ts.r + 12} fill={ss.glow} />
      )}
      {isSel && (
        <circle r={ts.r + 10} fill="none" stroke="rgba(255,255,255,0.45)" strokeWidth="1.5" strokeDasharray="4 3" />
      )}
      <circle r={ts.r} fill={ss.fill} stroke={ss.stroke} strokeWidth={isSel ? 2.5 : 1.8} />
      <g transform={`translate(${ts.r - 10},-${ts.r - 10})`}>
        <circle r="10" fill={ts.fill} />
        <text textAnchor="middle" y="4" fontSize="6.5" fontWeight="bold" fill="#fff"
          fontFamily="system-ui,sans-serif">{ts.label}</text>
      </g>
      <text textAnchor="middle" y="-4" fontSize={ts.r > 32 ? "13" : "11"} fontWeight="700"
        fill="#f1f5f9" fontFamily="ui-monospace,monospace">{n.site}</text>
      <text textAnchor="middle" y="12" fontSize="8.5" fill={lc} fontFamily="ui-monospace,monospace">
        {n.latency == null ? "офлайн" : `${n.latency} мс`}
      </text>
      <text textAnchor="middle" y={ts.r + 18} fontSize="8.5" fill="#94a3b8"
        fontFamily="ui-monospace,monospace">{shortName(n.name)}</text>
    </g>
  )
}

function WanSatNode({ x, y, wan, color, active, isSel, isDragged, onSelect, onMouseDown }: {
  x: number; y: number
  wan: { name: string; isp: string; maxDl: number; maxUl: number }
  color: string; active: boolean; isSel: boolean; isDragged: boolean
  onSelect: () => void
  onMouseDown: (e: React.MouseEvent) => void
}) {
  const r = 18
  return (
    <g transform={`translate(${x},${y})`}
      style={{ cursor: isDragged ? "grabbing" : "grab" }}
      onMouseDown={e => { e.stopPropagation(); onMouseDown(e) }}
      onClick={e => { e.stopPropagation(); onSelect() }}>
      {active && (
        <circle r={r + 6} fill={color} opacity="0.12">
          <animate attributeName="opacity" values="0.12;0.04;0.12" dur="2.5s" repeatCount="indefinite" />
        </circle>
      )}
      {isSel && (
        <circle r={r + 8} fill="none" stroke={color} strokeWidth="1.2" strokeDasharray="3 2" opacity="0.6" />
      )}
      <circle r={r} fill={`${color}1a`} stroke={color}
        strokeWidth={active ? 2 : 1.2} opacity={active ? 1 : 0.65} />
      <g opacity={active ? 0.9 : 0.5}>
        <path d="M -5 2 Q 0 -5 5 2"   fill="none" stroke={color} strokeWidth="1.5" strokeLinecap="round" />
        <path d="M -8 5 Q 0 -10 8 5"  fill="none" stroke={color} strokeWidth="1"   strokeLinecap="round" opacity="0.6" />
        <circle cx="0" cy="4" r="2" fill={color} />
      </g>
      <text textAnchor="middle" y={r + 14} fontSize="8" fontWeight="700"
        fill={color} fontFamily="ui-monospace,monospace">{wan.name}</text>
      <text textAnchor="middle" y={r + 24} fontSize="7" fill="#64748b" fontFamily="ui-monospace,monospace">
        {wan.isp.length > 10 ? wan.isp.slice(0, 9) + "…" : wan.isp}
      </text>
      <text textAnchor="middle" y={r + 34} fontSize="6.5" fill="#475569" fontFamily="ui-monospace,monospace">
        ↓{wan.maxDl} ↑{wan.maxUl}
      </text>
    </g>
  )
}

// ─── Context menu ─────────────────────────────────────────────────────────────

interface CtxMenu { x: number; y: number; server: Server }

function ContextMenu({ menu, onClose }: { menu: CtxMenu; onClose: () => void }) {
  const items = [
    {
      icon: <TerminalIcon className="size-3" />,
      label: "Открыть терминал",
      href: `/terminal?host=${menu.server.host}`,
    },
    {
      icon: <ShieldIcon className="size-3" />,
      label: "Firewall",
      href: `/firewall?server=${menu.server.id}`,
    },
    {
      icon: <CableIcon className="size-3" />,
      label: "GRE-туннели",
      href: `/gre?server=${menu.server.id}`,
    },
    {
      icon: <ActivityIcon className="size-3" />,
      label: "Трафик",
      href: `/traffic?server=${menu.server.id}`,
    },
  ]

  return (
    <>
      {/* backdrop */}
      <div className="fixed inset-0 z-40" onClick={onClose} />
      <div
        className="fixed z-50 min-w-[190px] rounded-lg border border-border bg-popover shadow-xl py-1 overflow-hidden"
        style={{ left: menu.x, top: menu.y }}>
        {/* header */}
        <div className="flex items-center gap-2 px-3 py-2 border-b border-border/60 mb-1">
          <span className={cn(
            "size-2 rounded-full",
            menu.server.status === "online"   ? "bg-emerald-400" :
            menu.server.status === "degraded" ? "bg-amber-400"   : "bg-red-400",
          )} />
          <span className="text-xs font-mono font-semibold truncate">{menu.server.name}</span>
          <span className="ml-auto text-[10px] font-mono text-muted-foreground">{menu.server.host}</span>
        </div>

        {items.map(item => (
          <a
            key={item.label}
            href={item.href}
            onClick={onClose}
            className="flex items-center gap-2.5 px-3 py-2 text-xs text-foreground hover:bg-muted transition-colors cursor-pointer">
            <span className="text-muted-foreground">{item.icon}</span>
            {item.label}
            <ExternalLinkIcon className="size-2.5 ml-auto text-muted-foreground/50" />
          </a>
        ))}

        <div className="border-t border-border/60 mt-1 pt-1">
          <button
            onClick={() => { navigator.clipboard.writeText(menu.server.host); onClose() }}
            className="w-full flex items-center gap-2.5 px-3 py-2 text-xs text-foreground hover:bg-muted transition-colors">
            <span className="text-muted-foreground"><CopyIcon className="size-3" /></span>
            Скопировать IP
          </button>
        </div>
      </div>
    </>
  )
}

// ─── Minimap ──────────────────────────────────────────────────────────────────

const MM_W = 172, MM_H = 94

function Minimap({ pan, zoom, nodes, greEdges, satPos, onClose, onPan }: {
  pan: { x: number; y: number }; zoom: number
  nodes: (Server & { x: number; y: number })[]
  greEdges: { tunnel: typeof greTunnels[0]; from: typeof nodes[0]; to: typeof nodes[0] }[]
  satPos: Record<string, { x: number; y: number }[]>
  onClose: () => void
  onPan: (x: number, y: number) => void
}) {
  const vpW = Math.min(W, W / zoom)
  const vpH = Math.min(H, H / zoom)

  function handleClick(e: React.MouseEvent<SVGSVGElement>) {
    e.stopPropagation()
    const rect = e.currentTarget.getBoundingClientRect()
    const svgX = (e.clientX - rect.left) / rect.width  * W
    const svgY = (e.clientY - rect.top)  / rect.height * H
    onPan(svgX - vpW / 2, svgY - vpH / 2)
  }

  return (
    <div className="absolute bottom-16 right-4 z-20 rounded-lg overflow-hidden
      border border-white/10 bg-[#060d1a]/90 backdrop-blur-sm shadow-xl"
      style={{ width: MM_W, height: MM_H }}>
      <svg width={MM_W} height={MM_H} viewBox={`0 0 ${W} ${H}`}
        style={{ display: "block", cursor: "crosshair" }} onClick={handleClick}>
        <rect width={W} height={H} fill="#060d1a" />
        {/* simplified edges */}
        {greEdges.map(e => (
          <line key={e.tunnel.id} x1={e.from.x} y1={e.from.y} x2={e.to.x} y2={e.to.y}
            stroke={TUNNEL_STYLE[e.tunnel.status].stroke} strokeWidth="5" opacity="0.25" />
        ))}
        {/* WAN edges */}
        {WAN_JH_EDGES.map((edge, i) => {
          const sat = satPos[edge.homeId]?.[edge.wanIdx]
          const jh  = nodes.find(n => n.id === edge.jhId)
          if (!sat || !jh) return null
          return <line key={i} x1={sat.x} y1={sat.y} x2={jh.x} y2={jh.y}
            stroke={WAN_COLORS[edge.wanIdx]} strokeWidth="3" opacity="0.25" />
        })}
        {/* nodes */}
        {nodes.map(n => (
          <circle key={n.id} cx={n.x} cy={n.y}
            r={TYPE_STYLE[n.type].r * 0.65}
            fill={TYPE_STYLE[n.type].fill}
            stroke={STATUS_STYLE[n.status].stroke}
            strokeWidth="3"
            opacity="0.85"
          />
        ))}
        {/* WAN satellites */}
        {servers.filter(s => s.type === "home-router").flatMap(r =>
          (satPos[r.id] ?? []).map((p, i) => (
            <circle key={`${r.id}-${i}`} cx={p.x} cy={p.y} r="10"
              fill={WAN_COLORS[i] + "33"} stroke={WAN_COLORS[i]} strokeWidth="3" opacity="0.7" />
          ))
        )}
        {/* viewport rect */}
        <rect x={pan.x} y={pan.y} width={W / zoom} height={H / zoom}
          fill="rgba(255,255,255,0.04)" stroke="rgba(255,255,255,0.6)" strokeWidth="6" rx="6" />
      </svg>
      <button
        onClick={e => { e.stopPropagation(); onClose() }}
        className="absolute top-1 right-1 text-white/30 hover:text-white/80 transition-colors leading-none">
        <XIcon className="size-2.5" />
      </button>
      <span className="absolute bottom-1 left-2 text-[7px] font-mono text-white/20 pointer-events-none">minimap</span>
    </div>
  )
}

// ─── Zoom controls overlay ────────────────────────────────────────────────────

function ZoomControls({ zoom, onZoomIn, onZoomOut, onFit }: {
  zoom: number
  onZoomIn: () => void
  onZoomOut: () => void
  onFit: () => void
}) {
  return (
    <div className="absolute bottom-4 left-1/2 -translate-x-1/2 z-20 flex items-center gap-0
      rounded-xl border border-white/10 bg-black/75 backdrop-blur-md shadow-xl overflow-hidden">
      <button onClick={onZoomOut}
        className="px-3 py-2 text-white/60 hover:text-white hover:bg-white/5 transition-colors"
        title="Уменьшить (-)">
        <ZoomOutIcon className="size-3.5" />
      </button>
      <span className="px-3 py-2 text-xs font-mono text-white/70 min-w-[52px] text-center border-x border-white/10 select-none">
        {Math.round(zoom * 100)}%
      </span>
      <button onClick={onZoomIn}
        className="px-3 py-2 text-white/60 hover:text-white hover:bg-white/5 transition-colors"
        title="Увеличить (+)">
        <ZoomInIcon className="size-3.5" />
      </button>
      <div className="w-px h-5 bg-white/10" />
      <button onClick={onFit}
        className="px-3 py-2 text-white/60 hover:text-white hover:bg-white/5 transition-colors"
        title="Вписать (F / 0)">
        <Maximize2Icon className="size-3.5" />
      </button>
    </div>
  )
}

// ════════════════════════════════════════════════════════════════════════════

export default function NetworkMapPage() {

  // ── View state ──────────────────────────────────────────────────────────────
  const [pan,        setPan]        = useState({ x: 0, y: 0 })
  const [zoom,       setZoom]       = useState(1)
  const [isDragging, setIsDragging] = useState(false)

  // ── Interaction ─────────────────────────────────────────────────────────────
  const [selected,   setSelected]   = useState<Server | null>(null)
  const [selWanIdx,  setSelWanIdx]  = useState<number | null>(null)
  const [hoveredId,  setHoveredId]  = useState<string | null>(null)

  // ── Node positions (overrides POS defaults) ─────────────────────────────────
  const [nodePositions, setNodePositions] = useState<Record<string, { x: number; y: number }>>({})
  const [satPositions,  setSatPositions]  = useState<Record<string, { x: number; y: number }[]>>({})

  // ── Context menu ────────────────────────────────────────────────────────────
  const [ctxMenu, setCtxMenu] = useState<CtxMenu | null>(null)

  // ── Toggles ─────────────────────────────────────────────────────────────────
  const [filter,         setFilter]         = useState<FilterKey>("all")
  const [search,         setSearch]         = useState("")
  const [showPingBadges, setShowPingBadges] = useState(true)
  const [showAnimDots,   setShowAnimDots]   = useState(true)
  const [showMinimap,    setShowMinimap]    = useState(true)
  const [showHints,      setShowHints]      = useState(false)
  const [showLayers,     setShowLayers]     = useState(false)

  // ── Refs ─────────────────────────────────────────────────────────────────────
  const svgRef  = useRef<SVGSVGElement>(null)

  // Canvas pan drag
  const dragRef = useRef<{
    startX: number; startY: number
    panX: number; panY: number
    moved: boolean
  } | null>(null)

  // Node / WAN-sat drag
  type NodeDrag =
    | { kind: "server";  nodeId: string; startX: number; startY: number; origX: number; origY: number; moved: boolean }
    | { kind: "wan-sat"; homeId: string; wanIdx: number; startX: number; startY: number; origX: number; origY: number; moved: boolean }
  const nodeDragRef     = useRef<NodeDrag | null>(null)
  const suppressClickRef = useRef(false)
  const [draggedNodeId,  setDraggedNodeId]  = useState<string | null>(null)
  const [draggedSatKey,  setDraggedSatKey]  = useState<string | null>(null) // `${homeId}-${wanIdx}`

  const zoomRef = useRef(zoom)
  const panRef  = useRef(pan)
  zoomRef.current = zoom
  panRef.current  = pan

  // ── SVG export ───────────────────────────────────────────────────────────
  function exportSvg() {
    const svg = svgRef.current
    if (!svg) return
    // Clone with full viewBox (reset pan/zoom)
    const clone = svg.cloneNode(true) as SVGSVGElement
    clone.setAttribute("viewBox", `0 0 ${W} ${H}`)
    clone.setAttribute("width", String(W))
    clone.setAttribute("height", String(H))
    const blob = new Blob(
      [`<?xml version="1.0" encoding="UTF-8"?>\n`, clone.outerHTML],
      { type: "image/svg+xml;charset=utf-8" },
    )
    const url = URL.createObjectURL(blob)
    const a = document.createElement("a")
    a.href = url
    a.download = `network-map-${new Date().toISOString().slice(0, 10)}.svg`
    a.click()
    URL.revokeObjectURL(url)
  }

  // ── Non-passive wheel zoom ────────────────────────────────────────────────
  useEffect(() => {
    const svg = svgRef.current
    if (!svg) return
    const handler = (e: WheelEvent) => {
      e.preventDefault()
      const z    = zoomRef.current
      const p    = panRef.current
      const rect = svg.getBoundingClientRect()
      const svgX = p.x + (e.clientX - rect.left) / rect.width  * (W / z)
      const svgY = p.y + (e.clientY - rect.top)  / rect.height * (H / z)
      // trackpad pinch sends ctrlKey, use smaller step
      const step = e.ctrlKey ? 1.06 : 1.14
      const factor  = e.deltaY < 0 ? step : 1 / step
      const newZoom = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, z * factor))
      setPan({
        x: svgX - (e.clientX - rect.left) / rect.width  * (W / newZoom),
        y: svgY - (e.clientY - rect.top)  / rect.height * (H / newZoom),
      })
      setZoom(newZoom)
    }
    svg.addEventListener("wheel", handler, { passive: false })
    return () => svg.removeEventListener("wheel", handler)
  }, [])

  // ── Keyboard shortcuts ────────────────────────────────────────────────────
  useEffect(() => {
    function onKey(e: KeyboardEvent) {
      if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return
      if (e.key === "Escape") { setSelected(null); setSelWanIdx(null) }
      if (e.key === "=" || e.key === "+") applyZoomCenter(1.25)
      if (e.key === "-")                  applyZoomCenter(1 / 1.25)
      if (e.key === "0" || e.key.toLowerCase() === "f") fitView()
      if (e.key.toLowerCase() === "m")   setShowMinimap(v => !v)
      if (e.key.toLowerCase() === "p")   setShowPingBadges(v => !v)
    }
    window.addEventListener("keydown", onKey)
    return () => window.removeEventListener("keydown", onKey)
  }, [zoom, pan])   // re-register when zoom/pan change so closure has fresh values

  // ── Zoom helpers ──────────────────────────────────────────────────────────
  function applyZoomCenter(factor: number) {
    const newZoom = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, zoom * factor))
    setPan(p => ({
      x: p.x + (W / zoom - W / newZoom) / 2,
      y: p.y + (H / zoom - H / newZoom) / 2,
    }))
    setZoom(newZoom)
  }

  function fitView() {
    setPan({ x: 0, y: 0 })
    setZoom(1)
  }

  function focusNode(x: number, y: number) {
    const newZoom = Math.min(ZOOM_MAX, Math.max(zoom * 1.8, 2))
    setPan({ x: x - W / (2 * newZoom), y: y - H / (2 * newZoom) })
    setZoom(newZoom)
  }

  // ── Drag ─────────────────────────────────────────────────────────────────
  function onSvgMouseDown(e: React.MouseEvent<SVGSVGElement>) {
    // Only pan on background clicks (node mousedown stops propagation)
    dragRef.current = { startX: e.clientX, startY: e.clientY, panX: pan.x, panY: pan.y, moved: false }
    setIsDragging(true)
  }

  function onSvgMouseMove(e: React.MouseEvent<SVGSVGElement>) {
    const nd = nodeDragRef.current
    if (nd) {
      const dx = e.clientX - nd.startX
      const dy = e.clientY - nd.startY
      if (Math.abs(dx) > 2 || Math.abs(dy) > 2) nd.moved = true
      const rect = e.currentTarget.getBoundingClientRect()
      const svgDx = dx * (W / zoom) / rect.width
      const svgDy = dy * (H / zoom) / rect.height
      const nx = nd.origX + svgDx
      const ny = nd.origY + svgDy

      if (nd.kind === "server") {
        setNodePositions(prev => ({ ...prev, [nd.nodeId]: { x: nx, y: ny } }))
      } else {
        setSatPositions(prev => {
          const arr = [...(prev[nd.homeId] ?? (WAN_SAT_POS[nd.homeId] ?? []))]
          arr[nd.wanIdx] = { x: nx, y: ny }
          return { ...prev, [nd.homeId]: arr }
        })
      }
      return
    }

    if (!dragRef.current) return
    const dx = e.clientX - dragRef.current.startX
    const dy = e.clientY - dragRef.current.startY
    if (Math.abs(dx) > 4 || Math.abs(dy) > 4) dragRef.current.moved = true
    const rect = e.currentTarget.getBoundingClientRect()
    setPan({
      x: dragRef.current.panX - dx * (W / zoom / rect.width),
      y: dragRef.current.panY - dy * (H / zoom / rect.height),
    })
  }

  function finishNodeDrag() {
    if (!nodeDragRef.current) return false
    suppressClickRef.current = nodeDragRef.current.moved
    nodeDragRef.current = null
    setDraggedNodeId(null)
    setDraggedSatKey(null)
    return true
  }

  function onSvgMouseUp() {
    if (finishNodeDrag()) return
    const moved = dragRef.current?.moved ?? false
    dragRef.current = null
    setIsDragging(false)
    if (!moved) { setSelected(null); setSelWanIdx(null) }
  }

  // ── Node drag start ──────────────────────────────────────────────────────
  function onNodeMouseDown(e: React.MouseEvent, n: Server & { x: number; y: number }) {
    nodeDragRef.current = { kind: "server", nodeId: n.id, startX: e.clientX, startY: e.clientY, origX: n.x, origY: n.y, moved: false }
    setDraggedNodeId(n.id)
  }
  function onSatMouseDown(e: React.MouseEvent, homeId: string, wanIdx: number, x: number, y: number) {
    nodeDragRef.current = { kind: "wan-sat", homeId, wanIdx, startX: e.clientX, startY: e.clientY, origX: x, origY: y, moved: false }
    setDraggedSatKey(`${homeId}-${wanIdx}`)
  }

  // ── Data ─────────────────────────────────────────────────────────────────
  const nodes = servers.map(s => {
    const pos = nodePositions[s.id] ?? POS[s.id] ?? { x: 0, y: 0 }
    return { ...s, ...pos }
  })
  const nodeById = Object.fromEntries(nodes.map(n => [n.id, n]))

  // Satellite positions merging defaults with overrides
  const effectiveSatPos: Record<string, { x: number; y: number }[]> = {}
  servers.filter(s => s.type === "home-router").forEach(r => {
    effectiveSatPos[r.id] = (WAN_SAT_POS[r.id] ?? []).map((def, i) =>
      satPositions[r.id]?.[i] ?? def
    )
  })

  const greEdges = greTunnels.flatMap(t => {
    const from = nodes.find(n => n.id === t.serverId)
    const to   = nodes.find(n => n.host === t.remoteAddress)
    if (!from || !to || from.type === "home-router" || to.type === "home-router") return []
    return [{ tunnel: t, from, to }]
  })

  // ── Visibility / search ──────────────────────────────────────────────────
  const sq = search.toLowerCase().trim()
  const matchesSearch = (n: typeof nodes[0]) =>
    !sq ||
    n.name.toLowerCase().includes(sq) ||
    n.site.toLowerCase().includes(sq) ||
    n.model?.toLowerCase().includes(sq) ||
    n.os?.toLowerCase().includes(sq)

  const isVisible = (n: typeof nodes[0]) => {
    if (!matchesSearch(n)) return false
    if (filter === "all") return true
    if (filter === "online" || filter === "degraded" || filter === "offline") return n.status === filter
    return n.type === filter
  }

  // ── Side panel ────────────────────────────────────────────────────────────
  function selectServer(s: Server) {
    setSelected(prev => prev?.id === s.id ? null : s)
    setSelWanIdx(null)
    setHoveredId(null)
  }
  function selectWan(s: Server, wanIdx: number) {
    setSelected(s)
    setSelWanIdx(prev => prev === wanIdx && selected?.id === s.id ? null : wanIdx)
  }

  const connectedTunnels = selected && selected.type !== "home-router"
    ? greTunnels.filter(t =>
        t.serverId === selected.id ||
        nodes.find(n => n.id === selected.id)?.host === t.remoteAddress
      )
    : []

  const hoveredNode = hoveredId ? nodes.find(n => n.id === hoveredId) : null

  const filterBtns: { value: FilterKey; label: string }[] = [
    { value: "all",         label: "Все"        },
    { value: "online",      label: "Онлайн"     },
    { value: "degraded",    label: "Внимание"   },
    { value: "offline",     label: "Оффлайн"    },
    { value: "jump-host",   label: "JumpHost"   },
    { value: "exit-node",   label: "Exit Node"  },
    { value: "home-router", label: "Home Router"},
  ]

  // ═════════════════════════════════════════════════════════════════════════

  return (
    <div className="flex flex-col h-full">
      <PageHeader
        crumbs={[{ label: "Обзор" }, { label: "Карта сети" }]}
        actions={
          <Button variant="outline" size="sm" onClick={exportSvg}>
            <DownloadIcon className="size-4" />Экспорт SVG
          </Button>
        }
      />

      {/* ── Toolbar ── */}
      <div className="flex items-center gap-2 px-3 py-2 border-b bg-background/60 shrink-0 flex-wrap">

        {/* Filter pills */}
        <div className="flex items-center gap-0.5 rounded-md border border-border bg-muted/40 p-0.5">
          {filterBtns.map(b => (
            <button key={b.value} onClick={() => setFilter(b.value)}
              className={cn(
                "px-2.5 py-1 text-xs rounded transition-colors whitespace-nowrap",
                filter === b.value
                  ? "bg-background text-foreground shadow-sm"
                  : "text-muted-foreground hover:text-foreground",
              )}>
              {b.label}
            </button>
          ))}
        </div>

        {/* Search */}
        <div className="relative">
          <SearchIcon className="absolute left-2 top-1/2 -translate-y-1/2 size-3 text-muted-foreground pointer-events-none" />
          <input
            value={search}
            onChange={e => setSearch(e.target.value)}
            placeholder="Поиск узла…"
            className="h-7 pl-6 pr-3 text-xs rounded-md border border-border bg-muted/40
              placeholder:text-muted-foreground/50 focus:outline-none focus:ring-1 focus:ring-ring w-44"
          />
          {search && (
            <button onClick={() => setSearch("")}
              className="absolute right-1.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground">
              <XIcon className="size-3" />
            </button>
          )}
        </div>

        {/* Layers dropdown */}
        <div className="relative">
          <button
            onClick={() => setShowLayers(v => !v)}
            className={cn(
              "flex items-center gap-1.5 h-7 px-2.5 text-xs rounded-md border transition-colors",
              showLayers
                ? "bg-muted text-foreground border-border"
                : "border-border/60 text-muted-foreground hover:text-foreground hover:border-border",
            )}>
            <LayersIcon className="size-3" />Слои
          </button>
          {showLayers && (
            <div className="absolute top-9 left-0 z-30 rounded-lg border border-border bg-popover shadow-xl p-2 flex flex-col gap-0.5 min-w-[170px]"
              onMouseLeave={() => setShowLayers(false)}>
              {([
                { key: "showPingBadges", label: "Ping-значки",      val: showPingBadges, set: setShowPingBadges, hint: "P" },
                { key: "showAnimDots",   label: "Анимация трафика", val: showAnimDots,   set: setShowAnimDots,   hint: "" },
                { key: "showMinimap",    label: "Минимап",          val: showMinimap,    set: setShowMinimap,    hint: "M" },
                { key: "showHints", label: "Горячие клавиши", val: showHints, set: setShowHints, hint: "" },
              ] as const).map(item => (
                <button key={item.key}
                  onClick={() => item.set((v: boolean) => !v)}
                  className={cn(
                    "flex items-center justify-between px-3 py-1.5 rounded-md text-xs transition-colors",
                    item.val ? "bg-muted text-foreground" : "text-muted-foreground hover:bg-muted/50 hover:text-foreground",
                  )}>
                  <span>{item.label}</span>
                  <span className="flex items-center gap-2">
                    {item.hint && (
                      <kbd className="text-[9px] font-mono px-1 py-0.5 rounded border border-border bg-muted/40">
                        {item.hint}
                      </kbd>
                    )}
                    <span className={cn(
                      "size-2 rounded-full",
                      item.val ? "bg-emerald-400" : "bg-muted-foreground/30",
                    )} />
                  </span>
                </button>
              ))}
              {(Object.keys(nodePositions).length > 0 || Object.keys(satPositions).length > 0) && (
                <div className="border-t border-border/50 mt-1 pt-1">
                  <button
                    onClick={() => { setNodePositions({}); setSatPositions({}) }}
                    className="w-full flex items-center gap-2 px-3 py-1.5 rounded-md text-xs
                      text-amber-400 hover:bg-amber-500/10 transition-colors">
                    ↺ Сбросить расположение
                  </button>
                </div>
              )}
            </div>
          )}
        </div>

        {/* Stats */}
        <div className="ml-auto flex items-center gap-4 text-xs text-muted-foreground">
          <span>
            <span className="font-semibold text-emerald-400">{servers.filter(s => s.status === "online").length}</span> онлайн
          </span>
          <span>
            <span className="font-semibold text-amber-400">{servers.filter(s => s.status === "degraded").length}</span> с проблемами
          </span>
          <span>
            <span className="font-semibold text-red-400">{servers.filter(s => s.status === "offline").length}</span> оффлайн
          </span>
          <span>
            <span className="font-semibold text-foreground">{greTunnels.length}</span> GRE
          </span>
        </div>
      </div>

      {/* ── Main area ── */}
      <div className="flex flex-1 overflow-hidden">

        {/* ── Canvas ── */}
        <div className="flex-1 relative overflow-hidden bg-[#060d1a] select-none">
          <svg
            ref={svgRef}
            viewBox={`${pan.x} ${pan.y} ${W / zoom} ${H / zoom}`}
            style={{ width: "100%", height: "100%", display: "block", cursor: isDragging ? "grabbing" : "grab", userSelect: "none", WebkitUserSelect: "none" }}
            onMouseDown={e => { e.preventDefault(); onSvgMouseDown(e) }}
            onMouseMove={onSvgMouseMove}
            onMouseUp={onSvgMouseUp}
            onMouseLeave={() => {
              finishNodeDrag()
              dragRef.current = null; setIsDragging(false); setHoveredId(null)
            }}
          >
            <defs>
              <pattern id="map-dots" width="22" height="22" patternUnits="userSpaceOnUse">
                <circle cx="1" cy="1" r="0.8" fill="rgba(148,163,184,0.08)" />
              </pattern>
              {WAN_COLORS.map((_, i) => (
                <filter key={i} id={`glow-wan-${i}`} x="-80%" y="-80%" width="260%" height="260%">
                  <feGaussianBlur stdDeviation="3" result="blur" />
                  <feMerge><feMergeNode in="blur" /><feMergeNode in="SourceGraphic" /></feMerge>
                </filter>
              ))}
            </defs>

            {/* Background grid */}
            <rect width={W} height={H} fill="url(#map-dots)" />

            {/* ── GRE edges ── */}
            {greEdges.map((e, i) => {
              const ts     = TUNNEL_STYLE[e.tunnel.status]
              const dimmed = filter !== "all" && !isVisible(e.from) && !isVisible(e.to)
              const probe  = TUNNEL_PROBES[e.tunnel.id]
              const pc     = pingColor(probe?.pingMs ?? null)
              const mx     = (e.from.x + e.to.x) / 2
              const my     = (e.from.y + e.to.y) / 2
              return (
                <g key={e.tunnel.id} opacity={dimmed ? 0.05 : 1} style={{ transition: "opacity 0.3s" }}>
                  <line
                    x1={e.from.x} y1={e.from.y} x2={e.to.x} y2={e.to.y}
                    stroke={ts.stroke} strokeWidth="1.5"
                    strokeDasharray={e.tunnel.ipsec ? "7 4" : "none"}
                    opacity={ts.opacity}
                  />
                  {showAnimDots && ts.dotOpacity > 0 && (
                    <circle r="3" fill={ts.stroke} opacity={ts.dotOpacity}>
                      <animateMotion dur={`${2.4 + (i % 5) * 0.35}s`} repeatCount="indefinite"
                        path={`M ${e.from.x} ${e.from.y} L ${e.to.x} ${e.to.y}`} />
                    </circle>
                  )}
                  {showPingBadges && probe && (
                    <PingBadge mx={mx} my={my} ping={probe.pingMs} dl={probe.dlMbps} ul={probe.ulMbps} color={pc} />
                  )}
                </g>
              )
            })}

            {/* ── Home Router → WAN satellite connectors ── */}
            {servers.filter(s => s.type === "home-router").flatMap(router => {
              const rNode = nodes.find(n => n.id === router.id)
              if (!rNode) return []
              return (router.wanUplinks ?? []).map((wan, wIdx) => {
                const sat   = effectiveSatPos[router.id]?.[wIdx]
                const color = WAN_COLORS[wIdx] ?? "#888"
                const vis   = filter === "all" || filter === "home-router" || filter === "online"
                if (!sat) return null
                return (
                  <g key={`${router.id}-conn-${wIdx}`} opacity={vis ? 1 : 0.06} style={{ transition: "opacity 0.3s" }}>
                    <line x1={rNode.x} y1={rNode.y} x2={sat.x} y2={sat.y}
                      stroke={color} strokeWidth="2" opacity="0.55" />
                  </g>
                )
              }).filter(Boolean)
            })}

            {/* ── WAN→JH edges ── */}
            {WAN_JH_EDGES.map((edge, i) => {
              const jh     = nodeById[edge.jhId]
              const satPos = effectiveSatPos[edge.homeId]?.[edge.wanIdx]
              if (!jh || !satPos) return null
              const color = WAN_COLORS[edge.wanIdx] ?? "#888"
              const vis   = filter === "all" || filter === "home-router" || filter === "jump-host" || filter === "online"
              const mx    = (satPos.x + jh.x) / 2
              const my    = (satPos.y + jh.y) / 2
              const isHL  = selected?.id === edge.homeId && (selWanIdx === null || selWanIdx === edge.wanIdx)
              return (
                <g key={`wan-jh-${i}`} opacity={vis ? (isHL ? 1 : 0.45) : 0.05}
                  style={{ transition: "opacity 0.3s" }}>
                  <line
                    x1={satPos.x} y1={satPos.y} x2={jh.x} y2={jh.y}
                    stroke={color}
                    strokeWidth={edge.active ? 2 : 1.2}
                    strokeDasharray={edge.active ? "none" : "5 4"}
                    opacity={edge.active ? 0.7 : 0.4}
                    filter={isHL ? `url(#glow-wan-${edge.wanIdx})` : undefined}
                  />
                  {showAnimDots && edge.active && (
                    <circle r="3.5" fill={color} opacity="0.85">
                      <animateMotion dur={`${1.8 + i * 0.3}s`} repeatCount="indefinite"
                        path={`M ${satPos.x} ${satPos.y} L ${jh.x} ${jh.y}`} />
                    </circle>
                  )}
                  {showPingBadges && (
                    <PingBadge mx={mx} my={my} ping={edge.pingMs} color={pingColor(edge.pingMs)} />
                  )}
                </g>
              )
            })}

            {/* ── Server nodes ── */}
            {nodes.map(n => (
              <ServerNode
                key={n.id} n={n}
                isSel={selected?.id === n.id}
                isVis={isVisible(n)}
                isDragged={draggedNodeId === n.id}
                onClick={() => {
                  if (suppressClickRef.current) { suppressClickRef.current = false; return }
                  selectServer(n)
                }}
                onDblClick={() => { selectServer(n); focusNode(n.x, n.y) }}
                onHoverChange={h => setHoveredId(h ? n.id : null)}
                onContextMenu={e => setCtxMenu({ x: e.clientX, y: e.clientY, server: n })}
                onMouseDown={e => onNodeMouseDown(e, n)}
              />
            ))}

            {/* ── WAN satellite nodes ── */}
            {servers.filter(s => s.type === "home-router").flatMap(router => {
              const vis = filter === "all" || filter === "home-router" || filter === "online"
              return (router.wanUplinks ?? []).map((wan, wIdx) => {
                const sat = effectiveSatPos[router.id]?.[wIdx]
                if (!sat) return null
                const color    = WAN_COLORS[wIdx] ?? "#888"
                const isActive = WAN_JH_EDGES.filter(e => e.homeId === router.id && e.wanIdx === wIdx).some(e => e.active)
                const isSatSel = selected?.id === router.id && selWanIdx === wIdx
                const satKey   = `${router.id}-${wIdx}`
                return (
                  <g key={`${router.id}-wan-${wIdx}`} opacity={vis ? 1 : 0.06} style={{ transition: "opacity 0.3s" }}>
                    <WanSatNode
                      x={sat.x} y={sat.y} wan={wan} color={color}
                      active={isActive} isSel={isSatSel}
                      isDragged={draggedSatKey === satKey}
                      onSelect={() => {
                        if (suppressClickRef.current) { suppressClickRef.current = false; return }
                        selectWan(router, wIdx)
                      }}
                      onMouseDown={e => onSatMouseDown(e, router.id, wIdx, sat.x, sat.y)}
                    />
                  </g>
                )
              }).filter(Boolean)
            })}

            {/* ── Hover tooltip ── */}
            {hoveredNode && !isDragging && (
              <SvgTooltip n={hoveredNode} />
            )}

            {/* ── Legend (viewport-fixed) ── */}
            <g transform={`translate(${pan.x + 14}, ${pan.y + 14})`}>
              <rect width="140" height="224" rx="8"
                fill="rgba(6,13,26,0.88)" stroke="rgba(255,255,255,0.07)" strokeWidth="1" />
              <text x="10" y="22" fontSize="8" fontWeight="700" fill="#64748b"
                fontFamily="system-ui" letterSpacing="0.08em">ЛЕГЕНДА</text>

              {(["online","degraded","offline"] as const).map((s, i) => (
                <g key={s} transform={`translate(10, ${34 + i * 18})`}>
                  <circle cx="5" cy="0" r="4.5" fill={STATUS_STYLE[s].stroke} opacity="0.85" />
                  <text x="16" y="4" fontSize="8.5" fill="#cbd5e1" fontFamily="system-ui">
                    {s === "online" ? "Онлайн" : s === "degraded" ? "Внимание" : "Оффлайн"}
                  </text>
                </g>
              ))}

              <line x1="10" y1="100" x2="130" y2="100" stroke="rgba(255,255,255,0.07)" strokeWidth="1" />

              {[
                { fill: "#7c3aed", label: "JH", text: "JumpHost"    },
                { fill: "#0369a1", label: "EN", text: "Exit Node"   },
                { fill: "#16a34a", label: "HR", text: "Home Router" },
              ].map((t, i) => (
                <g key={t.label} transform={`translate(10, ${110 + i * 18})`}>
                  <rect width="14" height="14" rx="3" fill={t.fill} />
                  <text x="5.5" y="11" fontSize="6" fontWeight="bold" fill="#fff"
                    textAnchor="middle" fontFamily="system-ui">{t.label}</text>
                  <text x="22" y="11" fontSize="8.5" fill="#cbd5e1" fontFamily="system-ui">{t.text}</text>
                </g>
              ))}

              <line x1="10" y1="166" x2="130" y2="166" stroke="rgba(255,255,255,0.07)" strokeWidth="1" />

              <text x="10" y="180" fontSize="7.5" fontWeight="700" fill="#475569"
                fontFamily="system-ui" letterSpacing="0.05em">WAN АПЛИНКИ</text>
              {WAN_COLORS.slice(0, 2).map((c, i) => (
                <g key={i} transform={`translate(10, ${190 + i * 14})`}>
                  <circle cx="5" cy="4" r="4" fill={c} opacity="0.9" />
                  <text x="16" y="8" fontSize="8" fill="#94a3b8" fontFamily="ui-monospace,monospace">
                    WAN{i + 1}
                  </text>
                  <line x1="38" y1="4" x2="60" y2="4" stroke={c}
                    strokeWidth={i === 0 ? 2 : 1.2} strokeDasharray={i === 0 ? "none" : "4 3"} opacity="0.8" />
                  <text x="66" y="8" fontSize="7.5" fill="#475569" fontFamily="system-ui">
                    {i === 0 ? "primary" : "backup"}
                  </text>
                </g>
              ))}
            </g>

            {/* ── Keyboard hints (viewport-fixed) ── */}
            {showHints && (
              <g transform={`translate(${pan.x + W / zoom - 14}, ${pan.y + H / zoom - 14})`}>
                {[
                  ["Scroll",   "zoom"],
                  ["+/-",      "zoom in/out"],
                  ["F / 0",    "вписать"],
                  ["M",        "minimap"],
                  ["P",        "ping"],
                  ["Esc",      "снять выбор"],
                  ["2× клик", "навигация"],
                ].reverse().map(([k, l], i) => (
                  <g key={k} transform={`translate(0, ${-(i * 16)})`}>
                    <rect x="-120" y="-12" width="120" height="14" rx="3"
                      fill="rgba(6,13,26,0.82)" />
                    <text x="-60" y="-1" textAnchor="middle" fontSize="7.5"
                      fill="#64748b" fontFamily="system-ui">
                      <tspan fontWeight="600" fill="#94a3b8">{k}</tspan>  {l}
                    </text>
                  </g>
                ))}
              </g>
            )}
          </svg>

          {/* ── Zoom controls ── */}
          <ZoomControls
            zoom={zoom}
            onZoomIn={() => applyZoomCenter(1.3)}
            onZoomOut={() => applyZoomCenter(1 / 1.3)}
            onFit={fitView}
          />

          {/* ── Minimap ── */}
          {showMinimap && (
            <Minimap
              pan={pan} zoom={zoom}
              nodes={nodes}
              greEdges={greEdges}
              satPos={effectiveSatPos}
              onClose={() => setShowMinimap(false)}
              onPan={(x, y) => setPan({ x, y })}
            />
          )}

          {/* ── "Show minimap" button when hidden ── */}
          {!showMinimap && (
            <button
              onClick={() => setShowMinimap(true)}
              className="absolute bottom-16 right-4 z-20 p-2 rounded-lg border border-white/10
                bg-black/70 backdrop-blur-sm text-white/40 hover:text-white/80 transition-colors"
              title="Показать миникарту (M)">
              <MapIcon className="size-3.5" />
            </button>
          )}

          {/* ── Search result count ── */}
          {sq && (
            <div className="absolute top-3 left-1/2 -translate-x-1/2 z-20 px-3 py-1.5 rounded-full
              border border-white/10 bg-black/75 backdrop-blur-sm text-xs font-mono text-white/70">
              {nodes.filter(isVisible).length === 0
                ? "Ничего не найдено"
                : `Найдено: ${nodes.filter(isVisible).length} узл${nodes.filter(isVisible).length === 1 ? "" : nodes.filter(isVisible).length < 5 ? "а" : "ов"}`}
            </div>
          )}
        </div>

        {/* ── Side panel ── */}
        {selected && (
          <div className="border-l flex flex-col overflow-hidden shrink-0 bg-background" style={{ width: 300 }}>
            <div className="flex items-start gap-2 px-4 py-3 border-b">
              <div className="flex-1 min-w-0">
                <p className="font-mono font-semibold text-sm truncate">{selected.name}</p>
                <p className="text-xs text-muted-foreground font-mono mt-0.5">{selected.host}</p>
              </div>
              <div className="flex items-center gap-1 mt-0.5">
                <button
                  onClick={() => { const n = nodes.find(x => x.id === selected.id); if (n) focusNode(n.x, n.y) }}
                  className="text-muted-foreground hover:text-foreground transition-colors"
                  title="Навести на карте">
                  <HomeIcon className="size-3.5" />
                </button>
                <button onClick={() => { setSelected(null); setSelWanIdx(null) }}
                  className="text-muted-foreground hover:text-foreground transition-colors">
                  <XIcon className="size-4" />
                </button>
              </div>
            </div>

            <div className="flex-1 overflow-y-auto px-4 py-4 flex flex-col gap-5">
              {/* Badges */}
              <div className="flex flex-wrap gap-2">
                <StatusBadge status={selected.status} />
                <span className={cn(
                  "inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium",
                  selected.type === "jump-host"   ? "bg-violet-950/40 text-violet-400" :
                  selected.type === "home-router" ? "bg-emerald-950/40 text-emerald-400" :
                  "bg-sky-950/40 text-sky-400",
                )}>
                  {TYPE_STYLE[selected.type].label} · {TYPE_LABELS[selected.type]}
                </span>
              </div>

              {/* Info rows */}
              <div className="flex flex-col gap-0">
                {[
                  ["Площадка",  selected.site],
                  ["Модель",    selected.model],
                  ["RouterOS",  selected.os],
                  ...(selected.type !== "home-router"
                    ? [["ASN", selected.asn], ["Сессий", String(selected.sessions)]]
                    : [["LAN", selected.lanSubnet ?? "—"]]),
                ].map(([k, v]) => (
                  <div key={k} className="flex items-center justify-between py-2 border-b border-border/50 last:border-0">
                    <span className="text-xs text-muted-foreground">{k}</span>
                    <span className="text-xs font-mono font-medium">{v}</span>
                  </div>
                ))}
                <div className="flex items-center justify-between py-2">
                  <span className="text-xs text-muted-foreground">Задержка</span>
                  <span className="text-xs font-mono font-semibold" style={{ color: latencyColor(selected.latency) }}>
                    {selected.latency == null ? "— офлайн" : `${selected.latency} мс`}
                  </span>
                </div>
              </div>

              {/* WAN uplinks */}
              {selected.type === "home-router" && selected.wanUplinks && (
                <div>
                  <p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2">
                    WAN-аплинки ({selected.wanUplinks.length})
                  </p>
                  <div className="flex flex-col gap-2">
                    {selected.wanUplinks.map((wan, wIdx) => {
                      const color    = WAN_COLORS[wIdx] ?? "#888"
                      const myEdges  = WAN_JH_EDGES.filter(e => e.homeId === selected.id && e.wanIdx === wIdx)
                      const isActive = myEdges.some(e => e.active)
                      const isSel    = selWanIdx === wIdx
                      return (
                        <div key={wan.id}
                          onClick={() => setSelWanIdx(prev => prev === wIdx ? null : wIdx)}
                          className={cn(
                            "rounded-lg border p-3 cursor-pointer transition-all",
                            isSel ? "bg-muted/60" : "bg-muted/20 hover:bg-muted/40",
                          )}
                          style={{ borderColor: isSel ? color : undefined }}>
                          <div className="flex items-center gap-2 mb-2">
                            <WifiIcon className="size-3.5 shrink-0" style={{ color }} />
                            <span className="font-mono text-xs font-bold" style={{ color }}>{wan.name}</span>
                            <span className={cn(
                              "ml-auto text-[10px] font-medium px-1.5 py-0.5 rounded-full",
                              isActive ? "bg-emerald-500/10 text-emerald-400" : "bg-muted text-muted-foreground",
                            )}>
                              {isActive ? "primary" : "backup"}
                            </span>
                          </div>
                          <div className="flex flex-col gap-1">
                            {[["ISP", wan.isp], ["Iface", wan.iface], ["IP", wan.ip],
                              ["BW", `↓${wan.maxDl} ↑${wan.maxUl} Мбит`]].map(([k, v]) => (
                              <div key={k} className="flex items-center justify-between">
                                <span className="text-[10px] text-muted-foreground">{k}</span>
                                <span className="text-[10px] font-mono">{v}</span>
                              </div>
                            ))}
                          </div>
                          {myEdges.length > 0 && (
                            <div className="mt-2 pt-2 border-t border-border/40">
                              <p className="text-[9px] uppercase text-muted-foreground tracking-wider mb-1.5">
                                Подключения к JH
                              </p>
                              {myEdges.map((e, ei) => {
                                const jh = servers.find(s => s.id === e.jhId)
                                return (
                                  <div key={ei} className="flex items-center justify-between py-0.5">
                                    <span className="text-[10px] font-mono text-muted-foreground truncate">
                                      → {jh?.name ?? e.jhId}
                                    </span>
                                    <span className="text-[10px] font-mono font-semibold shrink-0 ml-2"
                                      style={{ color: pingColor(e.pingMs) }}>
                                      {e.pingMs} мс
                                    </span>
                                  </div>
                                )
                              })}
                            </div>
                          )}
                        </div>
                      )
                    })}
                  </div>
                </div>
              )}

              {/* GRE tunnels */}
              {connectedTunnels.length > 0 && (
                <div>
                  <p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2">
                    GRE-туннели ({connectedTunnels.length})
                  </p>
                  <div className="flex flex-col gap-2">
                    {connectedTunnels.map(t => {
                      const ts    = TUNNEL_STYLE[t.status]
                      const peer  = servers.find(s => s.host === t.remoteAddress && s.id !== selected.id)
                               || servers.find(s => s.id === t.serverId && s.id !== selected.id)
                      const probe = TUNNEL_PROBES[t.id]
                      const pc    = pingColor(probe?.pingMs ?? null)
                      return (
                        <div key={t.id} className="rounded-md border border-border/60 px-3 py-2 bg-muted/20">
                          <div className="flex items-center justify-between mb-1">
                            <span className="text-xs font-mono font-medium">{t.name}</span>
                            <span className="text-[10px] font-medium" style={{ color: ts.stroke }}>
                              {t.status === "up" ? "Up" : t.status === "degraded" ? "Degraded" : "Down"}
                            </span>
                          </div>
                          <div className="flex items-center justify-between mb-1">
                            <span className="text-[10px] text-muted-foreground font-mono">
                              → {peer?.site ?? t.remoteAddress}
                            </span>
                            {t.ipsec
                              ? <span className="text-[10px] text-emerald-400 flex items-center gap-0.5"><LockIcon className="size-2.5" />IPsec</span>
                              : <span className="text-[10px] text-muted-foreground flex items-center gap-0.5"><LockOpenIcon className="size-2.5" />Plain</span>}
                          </div>
                          {probe?.pingMs != null && (
                            <div className="flex items-center justify-between pt-1 border-t border-border/30">
                              <span className="text-[9px] text-muted-foreground">ping / speed</span>
                              <span className="text-[10px] font-mono" style={{ color: pc }}>
                                {probe.pingMs} мс
                                {probe.dlMbps != null && (
                                  <span className="text-[9px] text-muted-foreground ml-1.5">
                                    ↓{probe.dlMbps} ↑{probe.ulMbps}
                                  </span>
                                )}
                              </span>
                            </div>
                          )}
                        </div>
                      )
                    })}
                  </div>
                </div>
              )}

              {/* Resources */}
              {(() => {
                const res = SERVER_RESOURCES[selected.id]
                if (!res) return null
                const cpuPct = res.cpu
                const ramPct = Math.round(res.ramUsed / res.ramTotal * 100)
                const hddPct = Math.round(res.hddUsed / res.hddTotal * 100)
                return (
                  <div>
                    <p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">
                      Ресурсы
                    </p>

                    {/* CPU with sparkline */}
                    <div className="mb-3">
                      <div className="flex items-center justify-between mb-1">
                        <span className="text-[10px] text-muted-foreground">CPU</span>
                        <span className={cn("text-[10px] font-mono font-semibold", resPctColor(cpuPct))}>
                          {cpuPct}%
                        </span>
                      </div>
                      <div className="flex items-center gap-2">
                        <MiniBar pct={cpuPct} />
                        <Sparkline history={res.cpuHistory} />
                      </div>
                    </div>

                    {/* RAM */}
                    <div className="mb-3">
                      <div className="flex items-center justify-between mb-1">
                        <span className="text-[10px] text-muted-foreground">RAM</span>
                        <span className={cn("text-[10px] font-mono font-semibold", resPctColor(ramPct))}>
                          {fmtMB(res.ramUsed)} / {fmtMB(res.ramTotal)}
                        </span>
                      </div>
                      <MiniBar pct={ramPct} />
                    </div>

                    {/* HDD */}
                    <div className="mb-3">
                      <div className="flex items-center justify-between mb-1">
                        <span className="text-[10px] text-muted-foreground">HDD</span>
                        <span className="text-[10px] font-mono text-muted-foreground">
                          {fmtMB(res.hddUsed)} / {fmtMB(res.hddTotal)}
                        </span>
                      </div>
                      <MiniBar pct={hddPct} />
                    </div>

                    {/* Misc metrics */}
                    <div className="flex flex-col gap-0">
                      <div className="flex items-center justify-between py-1.5 border-b border-border/40">
                        <span className="text-[10px] text-muted-foreground">Uptime</span>
                        <span className="text-[10px] font-mono">{fmtUptime(res.uptimeSeconds)}</span>
                      </div>
                      {res.temp != null && (
                        <div className="flex items-center justify-between py-1.5 border-b border-border/40">
                          <span className="text-[10px] text-muted-foreground">Температура</span>
                          <span className={cn(
                            "text-[10px] font-mono font-semibold",
                            res.temp >= 70 ? "text-red-400" : res.temp >= 55 ? "text-amber-400" : "text-emerald-400",
                          )}>
                            {res.temp}°C
                          </span>
                        </div>
                      )}
                      <div className="flex items-center justify-between py-1.5">
                        <span className="text-[10px] text-muted-foreground">Плата</span>
                        <span className="text-[10px] font-mono">{BOARD_MAP[selected.type]}</span>
                      </div>
                    </div>
                  </div>
                )
              })()}

              {/* Navigate hint */}
              <button
                onClick={() => { const n = nodes.find(x => x.id === selected.id); if (n) focusNode(n.x, n.y) }}
                className="w-full flex items-center justify-center gap-2 py-2 rounded-lg border border-dashed border-border/60
                  text-xs text-muted-foreground hover:text-foreground hover:border-border transition-colors">
                <HomeIcon className="size-3" />
                Навести карту на узел
              </button>
            </div>
          </div>
        )}
      </div>

      {/* ── Context menu ── */}
      {ctxMenu && <ContextMenu menu={ctxMenu} onClose={() => setCtxMenu(null)} />}
    </div>
  )
}
