import { useMemo, useCallback, useState, useRef, useEffect } from 'react';
import { IconRefresh, IconMaximize, IconMinimize, IconClock } from '@tabler/icons-react';

const FLAG_CDN = 'https://flagcdn.com';

const UNIFI_BG = '#0f172a';
const UNIFI_BG_DEEP = '#0b1220';
const UNIFI_CARD_BG = '#1e293b';
const UNIFI_CARD_BORDER = '#334155';
const UNIFI_GREEN = '#22c55e';
const UNIFI_GREEN_SOFT = '#34d399';
const UNIFI_BLUE_SOFT = '#38bdf8';
const UNIFI_WARNING = '#f59e0b';
const UNIFI_TEXT = '#f1f5f9';
const UNIFI_TEXT_MUTED = '#94a3b8';
const NODE_WIDTH = 124;
const NODE_HEIGHT = 64;
const STORAGE_KEY = 'network-map-unifi-positions';

function computeInitialLayout(servers, size) {
  const positions = {};
  if (!servers.length) return positions;

  // Группируем: сначала home, затем jumphost, затем exit, затем остальные
  const homes = servers.filter((s) => String(s.type || '').toLowerCase() === 'home');
  const jumphosts = servers.filter((s) => String(s.type || '').toLowerCase() === 'jumphost');
  const exits = servers.filter((s) => String(s.type || '').toLowerCase() === 'exit');
  const others = servers.filter((s) => {
    const t = String(s.type || '').toLowerCase();
    return t !== 'home' && t !== 'jumphost' && t !== 'exit';
  });

  const columns = [homes, jumphosts, exits, others].filter((col) => col.length > 0);
  const colCount = columns.length || 1;
  const paddingX = 80;
  const usableWidth = Math.max(200, size.w - paddingX * 2);
  const colStep = usableWidth / Math.max(1, colCount - 1);
  const baseX = paddingX;

  columns.forEach((col, colIdx) => {
    // В пределах колонки сортируем по dns/ip для стабильности/
    const sorted = [...col].sort((a, b) => {
      const la = (a.dns || a.ip || '').toString();
      const lb = (b.dns || b.ip || '').toString();
      return la.localeCompare(lb);
    });
    const n = sorted.length;
    const x = baseX + colStep * colIdx;
    const topPadding = 60;
    const bottomPadding = 60;
    const usableHeight = Math.max(120, size.h - topPadding - bottomPadding);
    sorted.forEach((s, i) => {
      const t = n > 1 ? i / (n - 1) : 0.5;
      const y = topPadding + usableHeight * t;
      positions[String(s.ip)] = { x, y };
    });
  });

  return positions;
}

/**
 * Карта сети в стиле UNIFI: тёмный фон, простые иконки узлов, зелёные линии, пинг на рёбрах.
 * Узлы можно перетаскивать; раскладка сохраняется в localStorage. Круг — только начальная раскладка.
 */
function formatMbps(bps) {
  if (bps == null || Number.isNaN(bps)) return '—';
  const mbps = bps / 1_000_000;
  if (!Number.isFinite(mbps)) return '—';
  return `${mbps.toFixed(1)} Мбит/с`;
}

/** Короткий формат скорости для подписи на ребре: "↓287 ↑170 Мбит/с" */
function formatSpeedShort(downloadBps, uploadBps) {
  const fmt = (v) => {
    if (v == null || Number.isNaN(v)) return '—';
    const mbps = v / 1_000_000;
    if (!Number.isFinite(mbps)) return '—';
    return mbps >= 10 ? Math.round(mbps) : mbps.toFixed(1);
  };
  const down = fmt(downloadBps);
  const up = fmt(uploadBps);
  if (down === '—' && up === '—') return null;
  if (down === '—') return `↑${up} Мбит/с`;
  if (up === '—') return `↓${down} Мбит/с`;
  return `↓${down} ↑${up} Мбит/с`;
}

function speedKey(key1, key2) {
  return [String(key1), String(key2)].sort().join(':');
}

function getNodeTypeMeta(type) {
  const t = String(type || '').toLowerCase();
  if (t === 'home') return { label: 'HOME', color: '#60a5fa' };
  if (t === 'jumphost') return { label: 'JUMP', color: '#34d399' };
  if (t === 'exit') return { label: 'EXIT', color: '#f59e0b' };
  return { label: 'NODE', color: '#94a3b8' };
}

function getTunnelColor(tunnelType) {
  const t = String(tunnelType || '').toLowerCase();
  if (t.includes('wireguard') || t === 'wg') return UNIFI_BLUE_SOFT;
  if (t.includes('ipsec')) return UNIFI_GREEN;
  return UNIFI_GREEN_SOFT;
}

function sideVector(side) {
  if (side === 'right') return { x: 1, y: 0 };
  if (side === 'left') return { x: -1, y: 0 };
  if (side === 'bottom') return { x: 0, y: 1 };
  return { x: 0, y: -1 };
}

function anchorPoint(pos, side, slotOffset = 0) {
  if (side === 'right') return { x: pos.x + NODE_WIDTH / 2, y: pos.y + slotOffset };
  if (side === 'left') return { x: pos.x - NODE_WIDTH / 2, y: pos.y + slotOffset };
  if (side === 'bottom') return { x: pos.x + slotOffset, y: pos.y + NODE_HEIGHT / 2 };
  return { x: pos.x + slotOffset, y: pos.y - NODE_HEIGHT / 2 };
}

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

function hash01(str) {
  let h = 2166136261;
  const s = String(str || '');
  for (let i = 0; i < s.length; i += 1) {
    h ^= s.charCodeAt(i);
    h = Math.imul(h, 16777619);
  }
  return (h >>> 0) / 4294967295;
}

export default function NetworkMapUnifi({
  servers = [],
  connections = [],
  pingMap = {},
  speedMap = {},
  pingStaleMap = {},
  speedStaleMap = {},
  onRefreshPingForConnection,
  onRefreshSpeedForConnection,
  pingLoading = false,
  speedLoading = false,
}) {
  const containerRef = useRef(null);
  const svgRef = useRef(null);
  const [size, setSize] = useState({ w: 800, h: 520 });
  const [positions, setPositions] = useState({});
  const [drag, setDrag] = useState(null);
  const [isFullscreen, setIsFullscreen] = useState(false);
  const [hoveredEdgeKey, setHoveredEdgeKey] = useState(null);
  const [selectedEdge, setSelectedEdge] = useState(null);

  useEffect(() => {
    const el = containerRef.current;
    if (!el) return;
    const ro = new ResizeObserver((entries) => {
      const { width, height } = entries[0]?.contentRect ?? {};
      if (width > 0 && height > 0) setSize({ w: width, h: height });
    });
    ro.observe(el);
    return () => ro.disconnect();
  }, []);

  // Трек полноэкранного режима
  useEffect(() => {
    const handler = () => {
      const fs = Boolean(document.fullscreenElement);
      setIsFullscreen(fs);
    };
    document.addEventListener('fullscreenchange', handler);
    return () => document.removeEventListener('fullscreenchange', handler);
  }, []);

  // Инициализация позиций: из localStorage или по кругу
  useEffect(() => {
    if (servers.length === 0) {
      setPositions({});
      return;
    }
    let saved = {};
    try {
      const raw = window.localStorage.getItem(STORAGE_KEY);
      if (raw) saved = JSON.parse(raw) || {};
    } catch {}
    const initial = computeInitialLayout(servers, size);
    const merged = {};
    servers.forEach((s, i) => {
      const ip = String(s.ip);
      if (saved[ip] && typeof saved[ip].x === 'number' && typeof saved[ip].y === 'number') {
        merged[ip] = { x: saved[ip].x, y: saved[ip].y };
      } else {
        merged[ip] = initial[ip] ?? { x: size.w / 2, y: size.h / 2 };
      }
    });
    setPositions(merged);
  }, [servers.length, size.w, size.h]);

  const savePositions = useCallback((next) => {
    setPositions(next);
    try {
      window.localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
    } catch {}
  }, []);

  const getSvgCoords = useCallback((clientX, clientY) => {
    const svg = svgRef.current;
    if (!svg) return null;
    const pt = svg.createSVGPoint();
    pt.x = clientX;
    pt.y = clientY;
    const p = pt.matrixTransform(svg.getScreenCTM().inverse());
    return { x: p.x, y: p.y };
  }, []);

  useEffect(() => {
    if (!drag) return;
    const onMove = (e) => {
      const p = getSvgCoords(e.clientX, e.clientY);
      if (!p) return;
      setPositions((prev) => {
        const pos = prev[drag.nodeId];
        if (!pos) return prev;
        const next = { ...prev, [drag.nodeId]: { x: drag.startX + (p.x - drag.mouseX), y: drag.startY + (p.y - drag.mouseY) } };
        try {
          window.localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
        } catch {}
        return next;
      });
    };
    const onUp = () => setDrag(null);
    window.addEventListener('mousemove', onMove);
    window.addEventListener('mouseup', onUp);
    return () => {
      window.removeEventListener('mousemove', onMove);
      window.removeEventListener('mouseup', onUp);
    };
  }, [drag, getSvgCoords]);

  const resetLayout = useCallback(() => {
    const initial = computeInitialLayout(servers, size);
    setPositions(initial);
    try {
      window.localStorage.setItem(STORAGE_KEY, JSON.stringify(initial));
    } catch {}
  }, [servers, size]);

  const onNodeMouseDown = useCallback(
    (e, ip) => {
      e.preventDefault();
      const p = getSvgCoords(e.clientX, e.clientY);
      const pos = positions[ip];
      if (!p || !pos) return;
      setDrag({ nodeId: ip, startX: pos.x, startY: pos.y, mouseX: p.x, mouseY: p.y });
    },
    [getSvgCoords, positions]
  );

  const edgePingKey = (a, b) => [String(a), String(b)].sort().join(':');

  const edgesWithPing = useMemo(() => {
    return (connections || []).map((c) => {
      const from = String(c.from);
      const to = String(c.to);
      const p1 = positions[from];
      const p2 = positions[to];
      const ekey = edgePingKey(from, to);
      const ms = pingMap[ekey];
      const pingStale = pingStaleMap[ekey] === true;
      const pingLabel = typeof ms === 'number' ? `${ms} ms` : null;
      const sk = speedKey(c.fromKey, c.toKey);
      const speed = speedMap[sk];
      const speedStale = speedStaleMap[sk] === true;
      const speedLabel =
        speed && (speed.tcpDownloadBps != null || speed.tcpUploadBps != null)
          ? formatSpeedShort(speed.tcpDownloadBps, speed.tcpUploadBps)
          : null;
      const compactEdgeLabel = pingLabel || speedLabel || null;
      const edgeLabel =
        pingLabel && speedLabel ? `${pingLabel} • ${speedLabel}` : (pingLabel || speedLabel);
      return {
        edgeKey: `${from}-${to}-${c.tunnelType || ''}`,
        from,
        to,
        p1,
        p2,
        pingMs: typeof ms === 'number' ? ms : null,
        pingLabel,
        speedLabel,
        compactEdgeLabel,
        edgeLabel,
        pingStale,
        speedStale,
        stale: pingStale || speedStale,
        speed,
        tunnelType: c.tunnelType,
        raw: c,
      };
    }).filter((e) => e.p1 && e.p2);
  }, [connections, positions, pingMap, speedMap, pingStaleMap, speedStaleMap]);

  // Роутинг рёбер: раскладываем рёбра по "портам" на сторонах узла (эффект трезубца),
  // а также даём параллельным связям разный изгиб.
  const routedEdges = useMemo(() => {
    const nodePortBuckets = new Map();
    const edgeRoutes = new Map();
    const pairBuckets = new Map();
    const slotStep = 9;

    const getEdgeSides = (e) => {
      const dx = e.p2.x - e.p1.x;
      const dy = e.p2.y - e.p1.y;
      if (Math.abs(dx) >= Math.abs(dy)) {
        return dx >= 0
          ? { fromSide: 'right', toSide: 'left' }
          : { fromSide: 'left', toSide: 'right' };
      }
      return dy >= 0
        ? { fromSide: 'bottom', toSide: 'top' }
        : { fromSide: 'top', toSide: 'bottom' };
    };

    const getSortValue = (side, targetPos) =>
      side === 'left' || side === 'right' ? targetPos.y : targetPos.x;

    edgesWithPing.forEach((e, idx) => {
      const { fromSide, toSide } = getEdgeSides(e);

      if (!nodePortBuckets.has(e.from)) nodePortBuckets.set(e.from, {});
      if (!nodePortBuckets.has(e.to)) nodePortBuckets.set(e.to, {});

      const fromBuckets = nodePortBuckets.get(e.from);
      const toBuckets = nodePortBuckets.get(e.to);

      fromBuckets[fromSide] = fromBuckets[fromSide] || [];
      toBuckets[toSide] = toBuckets[toSide] || [];

      fromBuckets[fromSide].push({
        idx,
        role: 'from',
        sortValue: getSortValue(fromSide, e.p2),
      });
      toBuckets[toSide].push({
        idx,
        role: 'to',
        sortValue: getSortValue(toSide, e.p1),
      });

      const pairKey = [e.from, e.to].sort().join(':');
      if (!pairBuckets.has(pairKey)) pairBuckets.set(pairKey, []);
      pairBuckets.get(pairKey).push(idx);

      edgeRoutes.set(idx, {
        fromSide,
        toSide,
        fromSlot: 0,
        toSlot: 0,
        bundleOffset: 0,
      });
    });

    nodePortBuckets.forEach((sides) => {
      Object.keys(sides).forEach((side) => {
        const list = sides[side];
        list.sort((a, b) => a.sortValue - b.sortValue);
        const center = (list.length - 1) / 2;
        list.forEach((item, order) => {
          const slotOffset = (order - center) * slotStep;
          const route = edgeRoutes.get(item.idx);
          if (!route) return;
          if (item.role === 'from') route.fromSlot = slotOffset;
          else route.toSlot = slotOffset;
        });
      });
    });

    pairBuckets.forEach((idxList) => {
      idxList.sort((a, b) => {
        const ea = edgesWithPing[a];
        const eb = edgesWithPing[b];
        return String(ea.tunnelType || '').localeCompare(String(eb.tunnelType || '')) || ea.edgeKey.localeCompare(eb.edgeKey);
      });
      const center = (idxList.length - 1) / 2;
      idxList.forEach((edgeIdx, order) => {
        const route = edgeRoutes.get(edgeIdx);
        if (!route) return;
        route.bundleOffset = (order - center) * 14;
      });
    });

    return edgesWithPing.map((e, idx) => ({
      ...e,
      route: edgeRoutes.get(idx),
    }));
  }, [edgesWithPing]);

  // Вычисляем геометрию ребра: выход из "порта" узла + короткий stem (трезубец) + плавная дуга.
  const computeCurvePoints = useCallback((e) => {
    const { p1, p2 } = e;
    const route = e.route || { fromSide: 'right', toSide: 'left', fromSlot: 0, toSlot: 0, bundleOffset: 0 };
    const stem = 14;

    const startAnchor = anchorPoint(p1, route.fromSide, route.fromSlot);
    const endAnchor = anchorPoint(p2, route.toSide, route.toSlot);
    const fromVec = sideVector(route.fromSide);
    const toVec = sideVector(route.toSide);

    const sx = startAnchor.x;
    const sy = startAnchor.y;
    const ex = endAnchor.x;
    const ey = endAnchor.y;
    const sOutX = sx + fromVec.x * stem;
    const sOutY = sy + fromVec.y * stem;
    const eOutX = ex + toVec.x * stem;
    const eOutY = ey + toVec.y * stem;

    const ddx = eOutX - sOutX;
    const ddy = eOutY - sOutY;
    const len = Math.hypot(ddx, ddy) || 1;

    const baseOffset = Math.min(44, len / 2);
    const bendOffset = baseOffset + (route.bundleOffset || 0);
    const bendX = (-ddy / len) * bendOffset;
    const bendY = (ddx / len) * bendOffset;

    const cpx = (sOutX + eOutX) / 2 + bendX;
    const cpy = (sOutY + eOutY) / 2 + bendY;

    // Середина квадратичной кривой при t=0.5
    const mx = 0.25 * sOutX + 0.5 * cpx + 0.25 * eOutX;
    const my = 0.25 * sOutY + 0.5 * cpy + 0.25 * eOutY;
    const perpLen = Math.hypot(-ddy, ddx) || 1;
    const ux = (-ddy) / perpLen;
    const uy = (ddx) / perpLen;

    return {
      sx,
      sy,
      ex,
      ey,
      sOutX,
      sOutY,
      eOutX,
      eOutY,
      cpx,
      cpy,
      mx,
      my,
      perpX: ux,
      perpY: uy,
    };
  }, []);

  const pathD = useCallback((e) => {
    const { sx, sy, ex, ey, sOutX, sOutY, eOutX, eOutY, cpx, cpy } = computeCurvePoints(e);
    return `M ${sx} ${sy} L ${sOutX} ${sOutY} Q ${cpx} ${cpy} ${eOutX} ${eOutY} L ${ex} ${ey}`;
  }, [computeCurvePoints]);

  /** Точка подписи на кривой с небольшим jitter и ограничением в пределах карты */
  const labelPlacement = useCallback((e, level = 0) => {
    const geom = computeCurvePoints(e);
    const jitter = (hash01(e.edgeKey) - 0.5) * 0.24;
    const t = clamp(0.42 + jitter, 0.26, 0.74);
    const omt = 1 - t;
    const xCurve = omt * omt * geom.sOutX + 2 * omt * t * geom.cpx + t * t * geom.eOutX;
    const yCurve = omt * omt * geom.sOutY + 2 * omt * t * geom.cpy + t * t * geom.eOutY;
    const baseOffset = 16;
    const stepOffset = 18;
    const x = xCurve + geom.perpX * (baseOffset + level * stepOffset);
    const y = yCurve + geom.perpY * (baseOffset + level * stepOffset);
    const margin = 22;
    return {
      x: clamp(x, margin, Math.max(margin, size.w - margin)),
      y: clamp(y, margin, Math.max(margin, size.h - margin)),
    };
  }, [computeCurvePoints, size.w, size.h]);

  const problematicNodes = useMemo(() => {
    const byIp = new Map();
    servers.forEach((s) => {
      byIp.set(String(s.ip), {
        ip: String(s.ip),
        dns: s.dns || '',
        type: String(s.type || '').toLowerCase(),
        linkCount: 0,
        staleCount: 0,
        maxPingMs: null,
        minSpeedMbps: null,
      });
    });

    (connections || []).forEach((c) => {
      const from = byIp.get(String(c.from));
      const to = byIp.get(String(c.to));
      if (from) from.linkCount += 1;
      if (to) to.linkCount += 1;
    });

    routedEdges.forEach((e) => {
      [e.from, e.to].forEach((nodeIp) => {
        const node = byIp.get(String(nodeIp));
        if (!node) return;
        if (e.stale) node.staleCount += 1;
        if (typeof e.pingMs === 'number') {
          node.maxPingMs = node.maxPingMs == null ? e.pingMs : Math.max(node.maxPingMs, e.pingMs);
        }
        const down = e.speed?.tcpDownloadBps != null ? e.speed.tcpDownloadBps / 1_000_000 : null;
        const up = e.speed?.tcpUploadBps != null ? e.speed.tcpUploadBps / 1_000_000 : null;
        const edgeMin = down != null && up != null ? Math.min(down, up) : (down ?? up);
        if (edgeMin != null && Number.isFinite(edgeMin)) {
          node.minSpeedMbps = node.minSpeedMbps == null ? edgeMin : Math.min(node.minSpeedMbps, edgeMin);
        }
      });
    });

    return Array.from(byIp.values())
      .map((n) => {
        const issues = [];
        let riskScore = 0;

        if (n.maxPingMs != null) {
          if (n.maxPingMs >= 120) {
            issues.push('Очень высокий ping');
            riskScore += 3;
          } else if (n.maxPingMs >= 80) {
            issues.push('Повышенный ping');
            riskScore += 2;
          }
        }

        if (n.minSpeedMbps != null) {
          if (n.minSpeedMbps < 40) {
            issues.push('Низкая скорость');
            riskScore += 3;
          } else if (n.minSpeedMbps < 80) {
            issues.push('Скорость ниже нормы');
            riskScore += 2;
          }
        }

        if (n.staleCount > 0) {
          issues.push('Есть устаревшие метрики');
          riskScore += n.staleCount >= 2 ? 2 : 1;
        }

        if (n.linkCount >= 7) {
          issues.push('Высокая связанность');
          riskScore += 1;
        }

        return {
          ...n,
          issues,
          riskScore,
          nodeLabel: n.dns ? String(n.dns).split('.')[0] : n.ip,
        };
      })
      .filter((n) => n.issues.length > 0)
      .sort((a, b) =>
        b.riskScore - a.riskScore ||
        (b.maxPingMs ?? -1) - (a.maxPingMs ?? -1) ||
        (a.minSpeedMbps ?? Infinity) - (b.minSpeedMbps ?? Infinity)
      );
  }, [servers, connections, routedEdges]);

  const criticalNodes = useMemo(
    () => problematicNodes.filter((n) => n.riskScore >= 4).slice(0, 8),
    [problematicNodes]
  );
  const warningNodes = useMemo(
    () => problematicNodes.filter((n) => n.riskScore >= 1 && n.riskScore < 4).slice(0, 12),
    [problematicNodes]
  );

  if (servers.length === 0) return null;

  return (
    <>
    <div
      ref={containerRef}
      className="network-map-unifi"
      style={{
        width: '100%',
        height: isFullscreen ? '100%' : '68vh',
        minHeight: isFullscreen ? 400 : 620,
        background: UNIFI_BG,
        borderRadius: 12,
        overflow: 'hidden',
        position: 'relative',
        userSelect: 'none',
      }}
    >
      <svg
        ref={svgRef}
        width="100%"
        height="100%"
        viewBox={`0 0 ${size.w} ${size.h}`}
        preserveAspectRatio="xMidYMid meet"
        style={{ display: 'block' }}
      >
        <defs>
          <linearGradient id="unifiBg" x1="0" y1="0" x2="0" y2="1">
            <stop offset="0%" stopColor={UNIFI_BG} />
            <stop offset="100%" stopColor={UNIFI_BG_DEEP} />
          </linearGradient>
          <filter id="edgeGlow" x="-50%" y="-50%" width="200%" height="200%">
            <feGaussianBlur stdDeviation="2.4" result="blur" />
            <feMerge>
              <feMergeNode in="blur" />
              <feMergeNode in="SourceGraphic" />
            </feMerge>
          </filter>
          <filter id="nodeShadow" x="-40%" y="-40%" width="180%" height="180%">
            <feDropShadow dx="0" dy="1" stdDeviation="2" floodColor="#020617" floodOpacity="0.55" />
          </filter>
        </defs>
        <rect x="0" y="0" width={size.w} height={size.h} fill="url(#unifiBg)" />
        {/* Рёбра: сглаженные линии с цветовой дифференциацией туннеля */}
        <g>
          {(() => {
            const used = [];
            const LABEL_W = 118;
            const LABEL_H = 22;
            const denseMode = routedEdges.length > 14;
            return routedEdges.map((e, idx) => {
              const dimFactor =
                hoveredEdgeKey && hoveredEdgeKey !== e.edgeKey ? 0.16 : 1;
              const strokeWidth = hoveredEdgeKey === e.edgeKey ? 3 : 1.8;
              const edgeColor = getTunnelColor(e.tunnelType);
              const isFocused =
                hoveredEdgeKey === e.edgeKey || selectedEdge?.edgeKey === e.edgeKey;
              const baseOpacity = denseMode ? 0.62 : 0.82;
              return (
            <g
              key={e.edgeKey || `${e.from}-${e.to}-${idx}`}
              onMouseEnter={() => setHoveredEdgeKey(e.edgeKey)}
              onMouseLeave={() =>
                setHoveredEdgeKey((prev) => (prev === e.edgeKey ? null : prev))
              }
              onClick={() => setSelectedEdge(e)}
              style={{ cursor: 'pointer' }}
            >
              <path
                d={pathD(e)}
                fill="none"
                stroke={edgeColor}
                strokeWidth={strokeWidth}
                strokeLinecap="round"
                opacity={baseOpacity * dimFactor}
                filter="url(#edgeGlow)"
              />
              {(() => {
                const compactOnly = denseMode && !isFocused && !e.stale;
                const labelText = compactOnly
                  ? (e.compactEdgeLabel || e.edgeLabel)
                  : e.edgeLabel;
                if (!labelText) return null;
                const minDist = denseMode ? 52 : 70;
                let level = 0;
                let x; let y;
                while (level < 10) {
                  const pos = labelPlacement(e, level);
                  x = pos.x;
                  y = pos.y;
                  const overlaps = used.some(
                    (p) => Math.hypot(p.x - x, p.y - y) < minDist
                  );
                  if (!overlaps) break;
                  level += 1;
                }
                used.push({ x, y });

                const pad = 10;
                const isStale = e.stale === true;
                const staleColor = UNIFI_WARNING;
                const textWidth = labelText.length * (compactOnly ? 5.9 : 6.2);
                const iconWidth = isStale ? 14 : 0;
                const minW = compactOnly ? 76 : LABEL_W;
                const maxW = compactOnly ? 160 : 210;
                const w = Math.max(minW, Math.min(textWidth + pad * 2 + iconWidth, maxW));
                const h = LABEL_H;
                const textX = x - (iconWidth / 2);
                return (
                  <g>
                    <rect
                      x={x - w / 2}
                      y={y - h / 2}
                      width={w}
                      height={h}
                      rx={7}
                      fill={isFocused ? 'rgba(2,6,23,0.94)' : 'rgba(2,6,23,0.85)'}
                      stroke={isStale ? staleColor : edgeColor}
                      strokeWidth={isFocused ? 1.3 : 1}
                      strokeDasharray={isStale ? '2,2' : 'none'}
                    />
                    {isStale && (
                      <circle
                        cx={x - w / 2 + 10}
                        cy={y}
                        r={4.4}
                        fill={staleColor}
                        opacity={0.95}
                      />
                    )}
                    <text
                      x={textX}
                      y={y}
                      textAnchor="middle"
                      dominantBaseline="middle"
                      fill={isStale ? staleColor : edgeColor}
                      fontSize={compactOnly ? 10.2 : 11}
                      fontWeight={600}
                    >
                      {labelText}
                    </text>
                  </g>
                );
              })()}
            </g>
            );
            });
          })()}
        </g>

        {/* Узлы: карточки серверов в стиле UNIFI (перетаскиваемые) */}
        {servers.map((s) => {
          const pos = positions[String(s.ip)];
          if (!pos) return null;
          const label = s.dns?.split('.')[0] || s.ip || '?';
          const typeMeta = getNodeTypeMeta(s.type);
          const isDragging = drag?.nodeId === s.ip;
          return (
            <g
              key={s.ip}
              transform={`translate(${pos.x - NODE_WIDTH / 2}, ${pos.y - NODE_HEIGHT / 2})`}
              onMouseDown={(e) => onNodeMouseDown(e, s.ip)}
              style={{ cursor: isDragging ? 'grabbing' : 'grab' }}
            >
              <rect
                width={NODE_WIDTH}
                height={NODE_HEIGHT}
                rx={8}
                ry={8}
                fill={UNIFI_CARD_BG}
                stroke={UNIFI_CARD_BORDER}
                strokeWidth={1}
                filter="url(#nodeShadow)"
              />
              <g transform={`translate(${NODE_WIDTH / 2}, ${NODE_HEIGHT / 2})`}>
                {/* Флаг страны в левом верхнем углу карточки (картинка, т.к. эмодзи в SVG часто отображаются как буквы) */}
                {s.country && (() => {
                  const cc = String(s.country).trim().toLowerCase();
                  if (cc.length !== 2) return null;
                  const flagW = 22;
                  const flagH = 16;
                  return (
                    <image
                      href={`${FLAG_CDN}/w40/${cc}.png`}
                      x={-NODE_WIDTH / 2 + 6}
                      y={-NODE_HEIGHT / 2 + 6}
                      width={flagW}
                      height={flagH}
                      preserveAspectRatio="xMidYMid meet"
                      style={{ pointerEvents: 'none' }}
                      className="network-map-node-flag"
                    >
                      <title>{s.country.toUpperCase()}</title>
                    </image>
                  );
                })()}
                {/* Иконка сервера в стиле UNIFI: прямоугольник с «портами» */}
                <g transform="translate(-10, -16)">
                  <rect x={2} y={0} width={16} height={12} rx={2} fill="none" stroke={typeMeta.color} strokeWidth={1.5} />
                  <line x1={5} y1={4} x2={15} y2={4} stroke={typeMeta.color} strokeWidth={1} opacity={0.8} />
                  <line x1={5} y1={8} x2={15} y2={8} stroke={typeMeta.color} strokeWidth={1} opacity={0.8} />
                </g>
                <text
                  x={0}
                  y={8}
                  textAnchor="middle"
                  fill={UNIFI_TEXT}
                  fontSize={11.5}
                  fontWeight={600}
                >
                  {label.length > 14 ? `${label.slice(0, 13)}…` : label}
                </text>
                <text
                  x={0}
                  y={22}
                  textAnchor="middle"
                  fill={UNIFI_TEXT_MUTED}
                  fontSize={9.5}
                >
                  {s.ip}
                </text>
                <rect
                  x={NODE_WIDTH / 2 - 42}
                  y={-NODE_HEIGHT / 2 + 5}
                  width={36}
                  height={14}
                  rx={7}
                  fill="rgba(15,23,42,0.9)"
                  stroke={typeMeta.color}
                  strokeWidth={1}
                />
                <text
                  x={NODE_WIDTH / 2 - 24}
                  y={-NODE_HEIGHT / 2 + 15}
                  textAnchor="middle"
                  fill={typeMeta.color}
                  fontSize={8}
                  fontWeight={700}
                  letterSpacing={0.5}
                >
                  {typeMeta.label}
                </text>
              </g>
            </g>
          );
        })}
      </svg>
      {selectedEdge && (() => {
        const fromServer = servers.find((s) => String(s.ip) === selectedEdge.from);
        const toServer = servers.find((s) => String(s.ip) === selectedEdge.to);
        const ekey = edgePingKey(selectedEdge.from, selectedEdge.to);
        const pingMs = pingMap[ekey];
        const pingStale = pingStaleMap[ekey] === true;
        const sk = speedKey(selectedEdge.raw?.fromKey, selectedEdge.raw?.toKey);
        const speed = speedMap[sk] ?? selectedEdge.speed;
        const speedStale = speedStaleMap[sk] === true;
        const canPingFrom =
          fromServer &&
          ['jumphost', 'home'].includes(String(fromServer.type || '').toLowerCase()) &&
          selectedEdge.raw?.internalFromTo;
        const canPingTo =
          toServer &&
          ['jumphost', 'home'].includes(String(toServer.type || '').toLowerCase()) &&
          selectedEdge.raw?.internalToFrom;
        const canPing = Boolean(canPingFrom || canPingTo);
        const canSpeed =
          Boolean(selectedEdge.raw?.speedTestServerId && selectedEdge.raw?.interfaceName);
        return (
          <div
            className="network-map-edge-modal-backdrop"
            style={{
              position: 'absolute',
              inset: 0,
              background: 'rgba(15,23,42,0.55)',
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
              zIndex: 50,
            }}
            onClick={() => setSelectedEdge(null)}
          >
            <div
              className="card shadow-lg"
              style={{
                minWidth: 360,
                maxWidth: 520,
                background: '#0f172a',
                color: '#e5e7eb',
                borderRadius: 12,
                border: `1px solid ${UNIFI_CARD_BORDER}`,
              }}
              onClick={(e) => e.stopPropagation()}
            >
              <div className="card-header d-flex align-items-center justify-content-between">
                <div className="fw-semibold">Детали соединения</div>
                <button
                  type="button"
                  className="btn-close btn-close-white"
                  aria-label="Закрыть"
                  onClick={() => setSelectedEdge(null)}
                  style={{ filter: 'invert(1)' }}
                />
              </div>
              <div className="card-body" style={{ fontSize: 13 }}>
                <div className="mb-2">
                  <div className="text-muted text-uppercase mb-1" style={{ fontSize: 11 }}>
                    От
                  </div>
                  <div>
                    <strong>{fromServer?.dns || fromServer?.ip || selectedEdge.from}</strong>{' '}
                    <span className="text-muted">({fromServer?.ip || '—'})</span>{' '}
                    <span className="badge bg-blue-lt text-blue ms-1">
                      {String(fromServer?.type || '').toUpperCase() || 'SERVER'}
                    </span>
                  </div>
                </div>
                <div className="mb-3">
                  <div className="text-muted text-uppercase mb-1" style={{ fontSize: 11 }}>
                    К
                  </div>
                  <div>
                    <strong>{toServer?.dns || toServer?.ip || selectedEdge.to}</strong>{' '}
                    <span className="text-muted">({toServer?.ip || '—'})</span>{' '}
                    <span className="badge bg-blue-lt text-blue ms-1">
                      {String(toServer?.type || '').toUpperCase() || 'SERVER'}
                    </span>
                  </div>
                </div>
                <div className="mb-3">
                  <div className="text-muted text-uppercase mb-1" style={{ fontSize: 11 }}>
                    Туннель
                  </div>
                  <div>
                    <span className="badge bg-green-lt text-green me-2">
                      {selectedEdge.tunnelType || 'TUNNEL'}
                    </span>
                    {selectedEdge.raw?.internalFromTo && (
                      <span className="text-muted">
                        {selectedEdge.raw.internalToFrom} ⇄ {selectedEdge.raw.internalFromTo}
                      </span>
                    )}
                  </div>
                </div>
                <div className="mb-3">
                  <div className="text-muted text-uppercase mb-1 d-flex align-items-center" style={{ fontSize: 11 }}>
                    Пинг
                    {pingStale && (
                      <span className="badge bg-warning text-dark ms-2" style={{ fontSize: 9 }}>
                        <IconClock size={10} className="me-1" />
                        Устаревшие данные
                      </span>
                    )}
                  </div>
                  <div>
                    {typeof pingMs === 'number' ? `${pingMs} ms` : 'нет данных'}
                  </div>
                </div>
                {speed && (speed.tcpDownloadBps != null || speed.tcpUploadBps != null) && (
                  <div className="mb-1">
                    <div className="text-muted text-uppercase mb-1 d-flex align-items-center" style={{ fontSize: 11 }}>
                      Скорость по туннелю
                      {speed.cached && (
                        <span className="ms-1 badge bg-secondary" style={{ fontSize: 10 }}>кеш</span>
                      )}
                      {speedStale && (
                        <span className="ms-1 badge bg-warning text-dark" style={{ fontSize: 9 }}>
                          <IconClock size={10} className="me-1" />
                          Устаревшие данные
                        </span>
                      )}
                    </div>
                    <div>
                      <div>
                        <span className="text-muted">↓ Download </span>
                        {formatMbps(speed.tcpDownloadBps)}
                      </div>
                      <div>
                        <span className="text-muted">↑ Upload </span>
                        {formatMbps(speed.tcpUploadBps)}
                      </div>
                      {speed.durationSeconds != null && (
                        <div className="text-muted small mt-1">
                          Замер за {speed.durationSeconds} сек
                        </div>
                      )}
                    </div>
                  </div>
                )}
                <div className="mt-3 d-flex flex-wrap gap-2 justify-content-end">
                  <button
                    type="button"
                    className="btn btn-outline-primary btn-sm"
                    disabled={!canPing || pingLoading || !onRefreshPingForConnection}
                    onClick={() => onRefreshPingForConnection && onRefreshPingForConnection(selectedEdge.raw)}
                  >
                    <IconRefresh
                      size={14}
                      className={pingLoading ? 'spin me-1' : 'me-1'}
                    />
                    {pingLoading ? 'Пинг…' : 'Проверить пинг'}
                  </button>
                  <button
                    type="button"
                    className="btn btn-outline-primary btn-sm"
                    disabled={!canSpeed || speedLoading || !onRefreshSpeedForConnection}
                    onClick={() => onRefreshSpeedForConnection && onRefreshSpeedForConnection(selectedEdge.raw)}
                  >
                    <IconRefresh
                      size={14}
                      className={speedLoading ? 'spin me-1' : 'me-1'}
                    />
                    {speedLoading ? 'Скорость…' : 'Проверить скорость'}
                  </button>
                </div>
              </div>
            </div>
          </div>
        );
      })()}
      <div
        style={{
          position: 'absolute',
          right: 12,
          top: 12,
          zIndex: 10,
        }}
      >
        <div className="btn-group btn-group-sm">
          <button
            type="button"
            className="btn"
            onClick={resetLayout}
            style={{
              background: 'rgba(30,41,59,0.95)',
              color: UNIFI_TEXT_MUTED,
              border: `1px solid ${UNIFI_CARD_BORDER}`,
            }}
            title="Вернуть узлы в исходную раскладку"
          >
            <IconRefresh size={14} className="me-1" />
            Сбросить
          </button>
          <button
            type="button"
            className="btn"
            onClick={() => {
              const el = containerRef.current;
              if (!el) return;
              if (!document.fullscreenElement) {
                el.requestFullscreen?.();
              } else {
                document.exitFullscreen?.();
              }
            }}
            style={{
              background: 'rgba(30,41,59,0.95)',
              color: UNIFI_TEXT_MUTED,
              border: `1px solid ${UNIFI_CARD_BORDER}`,
              borderLeft: 'none',
            }}
            title={isFullscreen ? 'Выйти из полноэкранного режима' : 'Открыть карту на весь экран'}
          >
            {isFullscreen ? <IconMinimize size={14} /> : <IconMaximize size={14} />}
          </button>
        </div>
      </div>
    </div>
    <div className="row g-3 mt-2">
      <div className="col-12 col-xl-6">
        <div
          className="card"
          style={{ background: '#0f172a', border: `1px solid ${UNIFI_CARD_BORDER}` }}
        >
          <div className="card-header">
            <h3 className="card-title mb-0">Критичные узлы</h3>
          </div>
          <div className="table-responsive">
            <table className="table card-table table-vcenter table-nowrap mb-0 table-sm">
              <thead>
                <tr>
                  <th scope="col">Узел</th>
                  <th scope="col">Тип</th>
                  <th scope="col">Ping max</th>
                  <th scope="col">Speed min</th>
                  <th scope="col">Связи</th>
                  <th scope="col">Проблемы</th>
                </tr>
              </thead>
              <tbody>
                {criticalNodes.length === 0 ? (
                  <tr>
                    <td colSpan={6} className="text-muted">Критичных узлов не найдено</td>
                  </tr>
                ) : (
                  criticalNodes.map((n) => (
                    <tr key={`critical-${n.ip}`}>
                      <td>
                        <div className="fw-semibold">{n.nodeLabel}</div>
                        <div className="text-muted small">{n.ip}</div>
                      </td>
                      <td>
                        <span className="badge bg-secondary-lt text-secondary">
                          {String(n.type || 'node').toUpperCase()}
                        </span>
                      </td>
                      <td>{n.maxPingMs != null ? `${n.maxPingMs} ms` : '—'}</td>
                      <td>{n.minSpeedMbps != null ? `${n.minSpeedMbps.toFixed(1)} Мбит/с` : '—'}</td>
                      <td>{n.linkCount}</td>
                      <td className="text-warning">{n.issues.slice(0, 2).join(', ')}</td>
                    </tr>
                  ))
                )}
              </tbody>
            </table>
          </div>
        </div>
      </div>
      <div className="col-12 col-xl-6">
        <div
          className="card"
          style={{ background: '#0f172a', border: `1px solid ${UNIFI_CARD_BORDER}` }}
        >
          <div className="card-header">
            <h3 className="card-title mb-0">Требуют внимания</h3>
          </div>
          <div className="table-responsive">
            <table className="table card-table table-vcenter table-nowrap mb-0 table-sm table-striped">
              <thead>
                <tr>
                  <th scope="col">Узел</th>
                  <th scope="col">Ping max</th>
                  <th scope="col">Speed min</th>
                  <th scope="col">Stale</th>
                  <th scope="col">Проблемы</th>
                </tr>
              </thead>
              <tbody>
                {warningNodes.length === 0 ? (
                  <tr>
                    <td colSpan={5} className="text-muted">Узлов с предупреждениями не найдено</td>
                  </tr>
                ) : (
                  warningNodes.map((n) => (
                    <tr key={`warn-${n.ip}`}>
                      <td>
                        <div className="fw-semibold">{n.nodeLabel}</div>
                        <div className="text-muted small">{n.ip}</div>
                      </td>
                      <td>{n.maxPingMs != null ? `${n.maxPingMs} ms` : '—'}</td>
                      <td>{n.minSpeedMbps != null ? `${n.minSpeedMbps.toFixed(1)} Мбит/с` : '—'}</td>
                      <td>{n.staleCount}</td>
                      <td className="text-muted">{n.issues.slice(0, 2).join(', ')}</td>
                    </tr>
                  ))
                )}
              </tbody>
            </table>
          </div>
        </div>
      </div>
    </div>
    </>
  );
}
