"use client"

import { useMemo, useState, useEffect } from "react"
import { PageHeader } from "@/components/page-header"
import { DataPageCard } from "@/components/data-page-card"
import { OspfNeighborsDataGrid } from "@/components/data-grids/ospf-neighbors-data-grid"
import { OspfRoutesDataGrid, routeTypeClass } from "@/components/data-grids/ospf-routes-data-grid"
import { OspfBfdDataGrid } from "@/components/data-grids/ospf-bfd-data-grid"
import { Frame, FramePanel } from "@/components/reui/frame"
import { KpiStatGrid } from "@/components/reui-kit/kpi-stat-grid"
import { Button } from "@/components/ui/button"
import { Separator } from "@/components/ui/separator"
import { toast } from "sonner"
import {
  RefreshCwIcon, WandSparklesIcon, SaveIcon, GripVerticalIcon,
  NetworkIcon, RouteIcon, ShieldIcon, ActivityIcon, XIcon,
  LayersIcon, RouterIcon, UsersIcon, CheckCircleIcon, AlertCircleIcon,
} from "lucide-react"
import { cn } from "@/lib/utils"
import { useDataSource } from "@/lib/data-source"
import { requestJson } from "@/shared/api/http-client"
import { ServerRailLayout, ServerRailMobileButton } from "@/components/server-rail-layout"
import { ALL_SERVERS_ID, type ServerTileItem } from "@/components/server-tile-rail"
import { readStoredRouteOptimizerSettings } from "@/lib/route-optimizer-data"

// ─── types ────────────────────────────────────────────────────────────────────

type OspfTab = "interfaces" | "neighbors" | "routes" | "bfd"

interface OspfServerInfo {
  id:      string   // String(serverId) in live mode, routerKey in mock
  label:   string   // serverName / routerLabel
  site:    string   // "MSK", "SPB", …
  country: string   // ISO alpha-2 or ""
}

interface OspfItem {
  key: string
  routerKey: string
  routerLabel: string
  serverId: string
  interfaceName: string
  area: string
  cost: number
  active: boolean
  type?: string
  useBfd?: boolean
  helloInterval?: number
  deadInterval?: number
}

interface OspfNeighbor {
  id: string
  localRouter: string
  localLabel: string
  localIface: string
  remoteRouter: string
  remoteLabel: string
  remoteRouterId: string
  area: string
  state: "Full" | "2-Way" | "ExStart" | "Down"
  cost: number
  uptime: string
  priority: number
}

interface OspfRoute {
  id: string
  destination: string
  type: "O" | "O IA" | "O E1" | "O E2"
  cost: number
  nextHop: string
  via: string
  serverId: string
  serverLabel: string
  area: string
}

interface BfdSession {
  id: string
  serverId: string
  serverLabel: string
  localAddr: string
  remoteAddr: string
  state: "Up" | "Down" | "Init" | "AdminDown"
  interval: number      // txInterval ms
  multiplier: number
  iface: string
  uptime: string | null
  multihop: boolean
  rxInterval: number    // ms
  holdTime: number      // ms
  packetsRx: number
  packetsTx: number
  stateChanges: number
}

// ─── backend types ────────────────────────────────────────────────────────────

interface BackendNeighbor {
  id: string; serverId: number; serverName: string; serverSite: string; serverCountry: string
  address: string; routerId: string; instance: string; area: string; areaId: string
  interface: string; state: string; uptime: string | null; stateChanges: number; priority: number
}

interface BackendInterface {
  id: string; serverId: number; serverName: string; serverSite: string; serverCountry: string
  instance: string; area: string; areaId: string; interface: string; cost: number
  type: string; disabled: boolean; inactive: boolean; priority: number
  helloInterval: number; deadInterval: number; useBfd: boolean
}

interface BackendInstance {
  id: string; serverId: number; serverName: string; serverSite: string; serverCountry: string
  name: string; routerId: string; version: number; disabled: boolean; inactive: boolean; redistribute: string
}

interface BackendBfdSession {
  id: string; serverId: number; serverName: string; serverSite: string; serverCountry: string
  localAddr: string; remoteAddr: string; interface: string; state: string; uptime: string | null
  multihop: boolean; multiplier: number; txInterval: number; rxInterval: number; holdTime: number
  packetsRx: number; packetsTx: number; stateChanges: number
}

interface BackendOspfAll {
  neighbors:   BackendNeighbor[]
  interfaces:  BackendInterface[]
  instances:   BackendInstance[]
  bfdSessions: BackendBfdSession[]
}

function isRefInterfaceName(name: string): boolean {
  return /^\(ref\s+\*.+\)$/.test(name.trim())
}

interface BackendOspfOptimizeResponse {
  serverId: number
  serverName: string
  pingWeight: number
  optimizedCount: number
  applied: Array<{ interface: string; from: number; to: number }>
  interfaces: Array<{
    id: string
    interface: string
    currentCost: number
    optimalCost: number
    score: number
    pingMs: number
    dlMbps: number
    ulMbps: number
  }>
}

// ─── backend → frontend mappers ───────────────────────────────────────────────

function backendToNeighbor(b: BackendNeighbor, ifaceMap: Map<string, number>): OspfNeighbor {
  const validStates = ["Full", "2-Way", "ExStart", "Down"]
  const state = validStates.includes(b.state) ? b.state as OspfNeighbor["state"] : "Down"
  const costKey = `${b.serverId}::${b.interface}`
  return {
    id:             `${b.serverId}-${b.id}`,
    localRouter:    String(b.serverId),
    localLabel:     b.serverName,
    localIface:     b.interface,
    remoteRouter:   b.routerId,
    remoteLabel:    b.routerId,   // no server name for remote peers
    remoteRouterId: b.routerId,
    area:           b.areaId !== b.area ? `${b.area} (${b.areaId})` : b.area,
    state,
    cost:           ifaceMap.get(costKey) ?? 10,
    uptime:         b.uptime ?? "—",
    priority:       b.priority,
  }
}

function backendToItem(b: BackendInterface): OspfItem {
  return {
    key:           `${b.serverId}-${b.id}`,
    routerKey:     String(b.serverId),
    routerLabel:   b.serverName,
    serverId:      String(b.serverId),
    interfaceName: b.interface,
    area:          b.areaId,
    cost:          b.cost,
    active:        !b.disabled && !b.inactive,
    type:          b.type,
    useBfd:        b.useBfd,
    helloInterval: b.helloInterval,
    deadInterval:  b.deadInterval,
  }
}

function backendToBfdSession(b: BackendBfdSession): BfdSession {
  const validStates = ["Up", "Down", "Init", "AdminDown"]
  const state = validStates.includes(b.state) ? b.state as BfdSession["state"] : "Down"
  return {
    id:           `${b.serverId}-${b.id}`,
    serverId:     String(b.serverId),
    serverLabel:  b.serverName,
    localAddr:    b.localAddr,
    remoteAddr:   b.remoteAddr,
    state,
    interval:     b.txInterval,
    multiplier:   b.multiplier,
    iface:        b.interface,
    uptime:       b.uptime,
    multihop:     b.multihop,
    rxInterval:   b.rxInterval,
    holdTime:     b.holdTime,
    packetsRx:    b.packetsRx,
    packetsTx:    b.packetsTx,
    stateChanges: b.stateChanges,
  }
}

// ─── mock data ────────────────────────────────────────────────────────────────

const COST_STEP = 10

const OPTIMIZER_PROB: Record<string, number> = {
  "srv1::GRE-MSK-SPB":  68,
  "srv1::GRE-MSK-FRA":  18,
  "srv1::GRE-MSK-AMS":  10,
  "srv1::GRE-MSK-SGP":   4,
  "srv1::ETHER1-WAN":   100,
  "srv1::ETHER2-LAN":   100,
  "srv7::GRE-LAB-SPB":  41,
  "srv7::GRE-LAB-CORE": 59,
  "srv7::ETHER1-UPLINK":100,
  "srv6::GRE-AMS-CORE": 68,
  "srv6::GRE-AMS-FRA":  32,
}

const MOCK_ITEMS: OspfItem[] = [
  { key: "srv1-bb-gre-msk-spb",  routerKey: "srv1", routerLabel: "mt-msk-core-01", serverId: "srv1", interfaceName: "gre-msk-spb",   area: "0.0.0.0", cost: 10, active: true  },
  { key: "srv1-bb-gre-msk-fra",  routerKey: "srv1", routerLabel: "mt-msk-core-01", serverId: "srv1", interfaceName: "gre-msk-fra",   area: "0.0.0.0", cost: 20, active: true  },
  { key: "srv1-bb-gre-msk-ams",  routerKey: "srv1", routerLabel: "mt-msk-core-01", serverId: "srv1", interfaceName: "gre-msk-ams",   area: "0.0.0.0", cost: 30, active: true  },
  { key: "srv1-bb-gre-msk-sgp",  routerKey: "srv1", routerLabel: "mt-msk-core-01", serverId: "srv1", interfaceName: "gre-msk-sgp",   area: "0.0.0.0", cost: 40, active: false },
  { key: "srv1-a1-ether1",       routerKey: "srv1", routerLabel: "mt-msk-core-01", serverId: "srv1", interfaceName: "ether1-wan",    area: "0.0.0.1", cost: 10, active: true  },
  { key: "srv1-a1-ether2",       routerKey: "srv1", routerLabel: "mt-msk-core-01", serverId: "srv1", interfaceName: "ether2-lan",    area: "0.0.0.1", cost: 20, active: true  },
  { key: "srv7-bb-gre-lab-core", routerKey: "srv7", routerLabel: "mt-msk-lab-01",  serverId: "srv7", interfaceName: "gre-lab-core",  area: "0.0.0.0", cost: 10, active: true  },
  { key: "srv7-bb-gre-lab-spb",  routerKey: "srv7", routerLabel: "mt-msk-lab-01",  serverId: "srv7", interfaceName: "gre-lab-spb",   area: "0.0.0.0", cost: 20, active: true  },
  { key: "srv7-bb-ether1",       routerKey: "srv7", routerLabel: "mt-msk-lab-01",  serverId: "srv7", interfaceName: "ether1-uplink", area: "0.0.0.0", cost: 30, active: true  },
  { key: "srv6-bb-gre-ams-core", routerKey: "srv6", routerLabel: "mt-ams-test-01", serverId: "srv6", interfaceName: "gre-ams-core",  area: "0.0.0.0", cost: 10, active: true  },
  { key: "srv6-bb-gre-ams-fra",  routerKey: "srv6", routerLabel: "mt-ams-test-01", serverId: "srv6", interfaceName: "gre-ams-fra",   area: "0.0.0.0", cost: 20, active: true  },
]

const MOCK_NEIGHBORS: OspfNeighbor[] = [
  { id: "n1", localRouter: "srv1", localLabel: "mt-msk-core-01", localIface: "gre-msk-spb",  remoteRouter: "srv2", remoteLabel: "mt-spb-edge-01", remoteRouterId: "10.0.0.2", area: "0.0.0.0", state: "Full",  cost: 10, uptime: "14d 6h 22m", priority: 1 },
  { id: "n2", localRouter: "srv1", localLabel: "mt-msk-core-01", localIface: "gre-msk-fra",  remoteRouter: "srv3", remoteLabel: "mt-fra-edge-01", remoteRouterId: "10.0.0.3", area: "0.0.0.0", state: "Full",  cost: 20, uptime: "12d 3h 11m", priority: 1 },
  { id: "n3", localRouter: "srv1", localLabel: "mt-msk-core-01", localIface: "gre-msk-ams",  remoteRouter: "srv4", remoteLabel: "mt-ams-edge-01", remoteRouterId: "10.0.0.4", area: "0.0.0.0", state: "2-Way", cost: 30, uptime: "2d 1h 4m",  priority: 0 },
  { id: "n4", localRouter: "srv1", localLabel: "mt-msk-core-01", localIface: "gre-lab-core", remoteRouter: "srv7", remoteLabel: "mt-msk-lab-01",  remoteRouterId: "10.0.0.7", area: "0.0.0.0", state: "Full",  cost: 10, uptime: "8d 14h 5m", priority: 1 },
  { id: "n5", localRouter: "srv7", localLabel: "mt-msk-lab-01",  localIface: "gre-lab-spb",  remoteRouter: "srv2", remoteLabel: "mt-spb-edge-01", remoteRouterId: "10.0.0.2", area: "0.0.0.0", state: "Full",  cost: 20, uptime: "8d 14h 4m", priority: 1 },
  { id: "n6", localRouter: "srv6", localLabel: "mt-ams-test-01", localIface: "gre-ams-core", remoteRouter: "srv1", remoteLabel: "mt-msk-core-01", remoteRouterId: "10.0.0.1", area: "0.0.0.0", state: "Full",  cost: 10, uptime: "5d 9h 17m", priority: 1 },
  { id: "n7", localRouter: "srv6", localLabel: "mt-ams-test-01", localIface: "gre-ams-fra",  remoteRouter: "srv3", remoteLabel: "mt-fra-edge-01", remoteRouterId: "10.0.0.3", area: "0.0.0.0", state: "Full",  cost: 20, uptime: "5d 9h 12m", priority: 1 },
]

const MOCK_ROUTES: OspfRoute[] = [
  { id: "r1", destination: "10.0.0.0/8",      type: "O",    cost: 10, nextHop: "10.200.0.1", via: "gre-msk-spb",  serverId: "srv1", serverLabel: "mt-msk-core-01", area: "0.0.0.0" },
  { id: "r2", destination: "172.16.0.0/12",    type: "O",    cost: 20, nextHop: "10.200.1.1", via: "gre-msk-fra",  serverId: "srv1", serverLabel: "mt-msk-core-01", area: "0.0.0.0" },
  { id: "r3", destination: "192.168.1.0/24",   type: "O IA", cost: 30, nextHop: "10.200.2.1", via: "gre-msk-ams",  serverId: "srv1", serverLabel: "mt-msk-core-01", area: "0.0.0.1" },
  { id: "r4", destination: "10.10.0.0/16",     type: "O E2", cost: 20, nextHop: "10.200.0.1", via: "gre-msk-spb",  serverId: "srv1", serverLabel: "mt-msk-core-01", area: "0.0.0.0" },
  { id: "r5", destination: "10.50.0.0/16",     type: "O IA", cost: 20, nextHop: "10.100.0.1", via: "gre-lab-spb",  serverId: "srv7", serverLabel: "mt-msk-lab-01",  area: "0.0.0.0" },
  { id: "r6", destination: "192.168.100.0/24", type: "O",    cost: 10, nextHop: "10.100.0.1", via: "gre-lab-core", serverId: "srv7", serverLabel: "mt-msk-lab-01",  area: "0.0.0.0" },
  { id: "r7", destination: "192.168.200.0/24", type: "O",    cost: 10, nextHop: "10.150.0.1", via: "gre-ams-core", serverId: "srv6", serverLabel: "mt-ams-test-01", area: "0.0.0.0" },
  { id: "r8", destination: "10.80.0.0/16",     type: "O E1", cost: 30, nextHop: "10.150.1.1", via: "gre-ams-fra",  serverId: "srv6", serverLabel: "mt-ams-test-01", area: "0.0.0.0" },
]

const MOCK_BFD: BfdSession[] = [
  { id: "b1", serverId: "srv1", serverLabel: "mt-msk-core-01", localAddr: "10.200.0.0", remoteAddr: "10.200.0.1", state: "Up",   interval: 200, multiplier: 3, iface: "gre-msk-spb",  uptime: "14d 6h 22m",  multihop: false, rxInterval: 200, holdTime: 600,  packetsRx: 14400, packetsTx: 14400, stateChanges: 1 },
  { id: "b2", serverId: "srv1", serverLabel: "mt-msk-core-01", localAddr: "10.200.1.0", remoteAddr: "10.200.1.1", state: "Up",   interval: 200, multiplier: 3, iface: "gre-msk-fra",  uptime: "12d 3h 11m",  multihop: false, rxInterval: 200, holdTime: 600,  packetsRx: 12520, packetsTx: 12519, stateChanges: 2 },
  { id: "b3", serverId: "srv1", serverLabel: "mt-msk-core-01", localAddr: "10.200.2.0", remoteAddr: "10.200.2.1", state: "Down", interval: 200, multiplier: 3, iface: "gre-msk-ams",  uptime: null,           multihop: false, rxInterval: 200, holdTime: 600,  packetsRx: 0,     packetsTx: 0,     stateChanges: 5 },
  { id: "b4", serverId: "srv7", serverLabel: "mt-msk-lab-01",  localAddr: "10.100.0.0", remoteAddr: "10.100.0.1", state: "Up",   interval: 200, multiplier: 5, iface: "gre-lab-core", uptime: "8d 14h 5m",   multihop: false, rxInterval: 200, holdTime: 1000, packetsRx: 9200,  packetsTx: 9200,  stateChanges: 1 },
  { id: "b5", serverId: "srv6", serverLabel: "mt-ams-test-01", localAddr: "10.150.0.0", remoteAddr: "10.150.0.1", state: "Up",   interval: 200, multiplier: 3, iface: "gre-ams-core", uptime: "5d 9h 17m",   multihop: false, rxInterval: 200, holdTime: 600,  packetsRx: 5700,  packetsTx: 5700,  stateChanges: 1 },
  { id: "b6", serverId: "srv6", serverLabel: "mt-ams-test-01", localAddr: "10.150.1.0", remoteAddr: "10.150.1.1", state: "Init", interval: 200, multiplier: 3, iface: "gre-ams-fra",  uptime: null,           multihop: false, rxInterval: 200, holdTime: 600,  packetsRx: 0,     packetsTx: 3,     stateChanges: 3 },
]

// ─── topology constants ────────────────────────────────────────────────────────

const TOPO_W = 900
const TOPO_H = 360
const NODE_R  = 34

const NODE_THEME = {
  Full:   { fill: "#0f2d1f", stroke: "#4ade80", glow: "rgba(74,222,128,0.18)"  },
  "2-Way":{ fill: "#2d1e06", stroke: "#fbbf24", glow: "rgba(251,191,36,0.14)"  },
  Down:   { fill: "#2d0f0f", stroke: "#f87171", glow: "transparent"             },
  none:   { fill: "#111827", stroke: "#374151", glow: "transparent"             },
}

const EDGE_COLOR: Record<string, string> = {
  Full:   "#4ade80",
  "2-Way":"#fbbf24",
  Down:   "#f87171",
}

interface GraphNode { id: string; label: string; short: string; site: string; x: number; y: number }
interface GraphEdge { from: string; to: string; state: OspfNeighbor["state"]; cost: number }

// Mock topology (static layout for mock mode)
const MOCK_GRAPH_NODES: GraphNode[] = [
  { id: "srv1", label: "mt-msk-core-01", short: "CORE-01", site: "MSK", x: 450, y: 180 },
  { id: "srv2", label: "mt-spb-edge-01", short: "EDGE-01", site: "SPB", x: 730, y:  80 },
  { id: "srv3", label: "mt-fra-edge-01", short: "EDGE-01", site: "FRA", x: 730, y: 290 },
  { id: "srv4", label: "mt-ams-edge-01", short: "EDGE-01", site: "AMS", x: 600, y: 180 },
  { id: "srv7", label: "mt-msk-lab-01",  short: "LAB-01",  site: "MSK", x: 170, y:  80 },
  { id: "srv6", label: "mt-ams-test-01", short: "TEST-01", site: "AMS", x: 170, y: 290 },
]

const MOCK_GRAPH_EDGES: GraphEdge[] = [
  { from: "srv1", to: "srv2", state: "Full",  cost: 10 },
  { from: "srv1", to: "srv3", state: "Full",  cost: 20 },
  { from: "srv1", to: "srv4", state: "2-Way", cost: 30 },
  { from: "srv1", to: "srv7", state: "Full",  cost: 10 },
  { from: "srv7", to: "srv2", state: "Full",  cost: 20 },
  { from: "srv6", to: "srv1", state: "Full",  cost: 10 },
  { from: "srv6", to: "srv3", state: "Full",  cost: 20 },
]

const MOCK_ROUTER_IDS: Record<string, string> = {
  srv1: "10.0.0.1", srv2: "10.0.0.2", srv3: "10.0.0.3",
  srv4: "10.0.0.4", srv6: "10.0.0.6", srv7: "10.0.0.7",
}

/** Build graph nodes and edges from live data */
function buildLiveGraph(
  neighbors: OspfNeighbor[],
): { nodes: GraphNode[]; edges: GraphEdge[] } {
  // Collect unique local routers
  const localMap = new Map<string, { label: string }>()
  for (const n of neighbors) {
    if (!localMap.has(n.localRouter)) localMap.set(n.localRouter, { label: n.localLabel })
  }

  // Collect unique remote peers (that are NOT already local)
  const remoteMap = new Map<string, { label: string }>()
  for (const n of neighbors) {
    const remId = `remote-${n.remoteRouterId}`
    if (!localMap.has(n.remoteRouter) && !remoteMap.has(remId)) {
      remoteMap.set(remId, { label: n.remoteRouterId })
    }
  }

  const allIds = [...localMap.keys(), ...remoteMap.keys()]
  const total  = allIds.length
  const cx = TOPO_W / 2, cy = TOPO_H / 2
  const radius = Math.min(TOPO_W, TOPO_H) * 0.38

  const nodes: GraphNode[] = allIds.map((id, i) => {
    const angle = (2 * Math.PI * i / total) - Math.PI / 2
    const x = cx + radius * Math.cos(angle)
    const y = cy + radius * Math.sin(angle)
    const label = localMap.get(id)?.label ?? remoteMap.get(id)?.label ?? id
    const short = label.replace(/^mt-/, "").toUpperCase().slice(0, 8)
    return { id, label, short, site: "—", x, y }
  })

  const edges: GraphEdge[] = neighbors.map(n => ({
    from:  n.localRouter,
    to:    localMap.has(n.remoteRouter) ? n.remoteRouter : `remote-${n.remoteRouterId}`,
    state: n.state,
    cost:  n.cost,
  }))

  return { nodes, edges }
}

// ─── helpers ──────────────────────────────────────────────────────────────────

function moveInArray<T>(arr: T[], from: number, to: number): T[] {
  if (from < 0 || to < 0 || from >= arr.length || to >= arr.length) return arr
  const copy = [...arr]
  const [moved] = copy.splice(from, 1)
  copy.splice(to, 0, moved)
  return copy
}

function stateClass(state: OspfNeighbor["state"] | BfdSession["state"]) {
  if (state === "Full" || state === "Up")
    return "bg-[var(--status-online-bg)] text-[var(--status-online-fg)] border-current/25"
  if (state === "2-Way" || state === "Init")
    return "bg-[var(--status-degraded-bg)] text-[var(--status-degraded-fg)] border-current/25"
  return "bg-[var(--status-offline-bg)] text-[var(--status-offline-fg)] border-current/25"
}

function Chip({ children, color }: { children: React.ReactNode; color?: string }) {
  return (
    <span className={cn(
      "inline-flex items-center rounded px-1.5 py-0.5 text-[11px] font-medium border",
      color ?? "bg-muted text-muted-foreground border-border",
    )}>
      {children}
    </span>
  )
}

// ─── topology SVG ─────────────────────────────────────────────────────────────

interface TopologyGraphProps {
  graphNodes: GraphNode[]
  graphEdges: GraphEdge[]
  routerIds:  Record<string, string>
  neighbors:  OspfNeighbor[]
  highlightId: string | null
  selectedId:  string | null
  showDots:    boolean
  onNodeClick: (id: string | null) => void
}

function TopologyGraph({ graphNodes, graphEdges, highlightId, selectedId, showDots, onNodeClick }: TopologyGraphProps) {
  const nodeMap = Object.fromEntries(graphNodes.map(n => [n.id, n]))

  function nodeState(id: string): keyof typeof NODE_THEME {
    const involved = graphEdges.filter(e => e.from === id || e.to === id)
    if (!involved.length) return "none"
    if (involved.some(e => e.state === "Full"))   return "Full"
    if (involved.some(e => e.state === "2-Way"))  return "2-Way"
    return "Down"
  }

  return (
    <svg
      viewBox={`0 0 ${TOPO_W} ${TOPO_H}`}
      style={{ width: "100%", height: TOPO_H, display: "block", userSelect: "none", cursor: "default" }}
      onClick={() => onNodeClick(null)}
    >
      <defs>
        <pattern id="ospf-dots" width="22" height="22" patternUnits="userSpaceOnUse">
          <circle cx="1" cy="1" r="0.8" fill="rgba(148,163,184,0.08)" />
        </pattern>
      </defs>
      <rect width={TOPO_W} height={TOPO_H} fill="#060d1a" />
      <rect width={TOPO_W} height={TOPO_H} fill="url(#ospf-dots)" />

      {graphEdges.map((e, i) => {
        const a = nodeMap[e.from]
        const b = nodeMap[e.to]
        if (!a || !b) return null
        const color    = EDGE_COLOR[e.state] ?? "#64748b"
        const isDashed = e.state !== "Full"
        const active   = highlightId === e.from || highlightId === e.to
                      || selectedId  === e.from || selectedId  === e.to
        const opacity  = active ? 1 : 0.38
        const strokeW  = active ? 2.4 : 1.4
        const mx = (a.x + b.x) / 2
        const my = (a.y + b.y) / 2
        return (
          <g key={i} style={{ transition: "opacity 0.2s" }} opacity={opacity}>
            <line x1={a.x} y1={a.y} x2={b.x} y2={b.y}
              stroke={color} strokeWidth={strokeW}
              strokeDasharray={isDashed ? "7 4" : "none"} />
            {showDots && e.state === "Full" && (
              <circle r="3.5" fill={color} opacity="0.9">
                <animateMotion dur={`${2.2 + (i % 5) * 0.38}s`} repeatCount="indefinite"
                  path={`M ${a.x} ${a.y} L ${b.x} ${b.y}`} />
              </circle>
            )}
            <g transform={`translate(${mx},${my})`}>
              <rect x="-15" y="-10" width="30" height="18" rx="4"
                fill="#060d1a" stroke={color} strokeWidth="0.8" opacity="0.95" />
              <text textAnchor="middle" y="4" fontSize="8.5" fontWeight="700"
                fill={color} fontFamily="ui-monospace,monospace">{e.cost}</text>
            </g>
          </g>
        )
      })}

      {graphNodes.map(node => {
        const state  = nodeState(node.id)
        const theme  = NODE_THEME[state]
        const isHov  = highlightId === node.id
        const isSel  = selectedId  === node.id
        const dimmed = (selectedId !== null || highlightId !== null)
                     && !isSel && !isHov
                     && !graphEdges.some(e =>
                         (e.from === selectedId  && e.to === node.id) ||
                         (e.to   === selectedId  && e.from === node.id) ||
                         (e.from === highlightId && e.to === node.id) ||
                         (e.to   === highlightId && e.from === node.id))
        return (
          <g key={node.id} transform={`translate(${node.x},${node.y})`}
            style={{ cursor: "pointer", transition: "opacity 0.2s" }}
            opacity={dimmed ? 0.25 : 1}
            onClick={e => { e.stopPropagation(); onNodeClick(isSel ? null : node.id) }}>
            {state === "Full" && !dimmed && (
              <circle r={NODE_R + 14} fill={theme.glow} opacity="0.7">
                <animate attributeName="r" values={`${NODE_R+9};${NODE_R+17};${NODE_R+9}`} dur="3s" repeatCount="indefinite" />
                <animate attributeName="opacity" values="0.8;0.18;0.8" dur="3s" repeatCount="indefinite" />
              </circle>
            )}
            {state === "2-Way" && !dimmed && <circle r={NODE_R + 10} fill={theme.glow} />}
            {isSel && <circle r={NODE_R+11} fill="none" stroke="rgba(255,255,255,0.65)" strokeWidth="2" />}
            {isHov && !isSel && <circle r={NODE_R+10} fill="none" stroke="rgba(255,255,255,0.38)" strokeWidth="1.5" strokeDasharray="4 3" />}
            <circle r={NODE_R} fill={theme.fill}
              stroke={isSel ? "#fff" : theme.stroke} strokeWidth={isSel ? 2.8 : 1.8} />
            {node.site !== "—" && (
              <g transform={`translate(${NODE_R-10},${-(NODE_R-10)})`}>
                <circle r="10" fill={
                  node.site === "MSK" ? "#7c3aed" : node.site === "SPB" ? "#0369a1" :
                  node.site === "FRA" ? "#b45309" : "#065f46"} />
                <text textAnchor="middle" y="4" fontSize="6" fontWeight="700" fill="#fff" fontFamily="system-ui,sans-serif">
                  {node.site.slice(0,3)}
                </text>
              </g>
            )}
            <text textAnchor="middle" y="-5" fontSize="11" fontWeight="700" fill="#f1f5f9" fontFamily="ui-monospace,monospace">
              {node.site !== "—" ? node.site : "?"}
            </text>
            <text textAnchor="middle" y="10" fontSize="7.5" fill={isSel ? "#fff" : theme.stroke}
              fontFamily="ui-monospace,monospace" fontWeight="500">{node.short}</text>
            <text textAnchor="middle" y={NODE_R+18} fontSize="8" fill="#94a3b8" fontFamily="ui-monospace,monospace">
              {node.label.replace(/^mt-/, "")}
            </text>
          </g>
        )
      })}

      {/* legend */}
      <g transform="translate(14, 14)">
        <rect width="152" height="118" rx="8" fill="rgba(6,13,26,0.90)" stroke="rgba(255,255,255,0.07)" strokeWidth="1" />
        <text x="10" y="22" fontSize="7.5" fontWeight="700" fill="#64748b" fontFamily="system-ui" letterSpacing="0.1em">ЛЕГЕНДА OSPF</text>
        {([
          { state: "Full",  label: "Full adjacency" },
          { state: "2-Way", label: "2-Way partial"  },
          { state: "Down",  label: "Down"            },
        ] as const).map(({ state, label }, i) => {
          const c = EDGE_COLOR[state] ?? "#64748b"
          return (
            <g key={state} transform={`translate(10, ${34 + i * 20})`}>
              <circle cx="5" cy="0" r="4.5" fill={c} opacity="0.85" />
              <line x1="16" y1="0" x2="44" y2="0" stroke={c}
                strokeWidth={state === "Full" ? 2 : 1.2}
                strokeDasharray={state !== "Full" ? "5 3" : "none"} opacity="0.8" />
              <text x="52" y="4" fontSize="8" fill="#cbd5e1" fontFamily="system-ui">{label}</text>
            </g>
          )
        })}
        <line x1="10" y1="100" x2="142" y2="100" stroke="rgba(255,255,255,0.07)" strokeWidth="1" />
        <text x="10" y="113" fontSize="7" fill="#475569" fontFamily="ui-monospace,monospace">
          клик по узлу — подробности · цифра = cost
        </text>
      </g>
    </svg>
  )
}

// ─── node detail panel ────────────────────────────────────────────────────────

function NodeDetailPanel({
  nodeId, graphNodes, routerIds, neighbors, items, onClose,
}: {
  nodeId: string; graphNodes: GraphNode[]; routerIds: Record<string, string>
  neighbors: OspfNeighbor[]; items: OspfItem[]; onClose: () => void
}) {
  const node = graphNodes.find(n => n.id === nodeId)
  if (!node) return null

  const routerId = routerIds[nodeId] ?? "—"
  const adjacencies = neighbors.filter(n => n.localRouter === nodeId || n.remoteRouter === nodeId)
    .map(n => {
      if (n.localRouter === nodeId)
        return { iface: n.localIface, peerLabel: n.remoteLabel, peerId: n.remoteRouterId, state: n.state, cost: n.cost, uptime: n.uptime }
      return { iface: n.localIface, peerLabel: n.localLabel, peerId: routerId, state: n.state, cost: n.cost, uptime: n.uptime }
    })
  const ifaces   = items.filter(i => i.routerKey === nodeId)
  const fullCnt  = adjacencies.filter(a => a.state === "Full").length
  const partCnt  = adjacencies.filter(a => a.state === "2-Way").length

  // derive state from edges
  let panelState: keyof typeof NODE_THEME = "none"
  if (adjacencies.some(a => a.state === "Full"))  panelState = "Full"
  else if (adjacencies.some(a => a.state === "2-Way")) panelState = "2-Way"
  else if (adjacencies.length) panelState = "Down"

  const theme = NODE_THEME[panelState]
  const siteFill =
    node.site === "MSK" ? "#7c3aed" : node.site === "SPB" ? "#0369a1" :
    node.site === "FRA" ? "#b45309" : "#065f46"

  return (
    <div className="w-72 shrink-0 flex flex-col overflow-y-auto border-l border-white/10"
      style={{ background: "#060d1a", maxHeight: TOPO_H }}>
      <div className="flex items-start gap-2 px-4 py-3 border-b border-white/10 shrink-0">
        <div className="size-8 rounded-full flex items-center justify-center shrink-0 mt-0.5 text-[10px] font-bold text-white"
          style={{ background: siteFill }}>
          {node.site !== "—" ? node.site.slice(0,3) : "?"}
        </div>
        <div className="flex-1 min-w-0">
          <p className="text-sm font-mono font-semibold text-[#f1f5f9] truncate leading-tight">{node.label}</p>
          <p className="text-[10px] font-mono text-[#64748b] mt-0.5">Router-ID: {routerId}</p>
        </div>
        <button onClick={onClose} className="text-white/25 hover:text-white/70 transition-colors mt-0.5 shrink-0">
          <XIcon className="size-3.5" />
        </button>
      </div>

      <div className="flex items-center gap-2 px-4 py-2.5 border-b border-white/10 shrink-0">
        <span className="size-2 rounded-full shrink-0" style={{ background: theme.stroke }} />
        <span className="text-xs font-mono font-semibold" style={{ color: theme.stroke }}>{panelState}</span>
        <div className="flex gap-1.5 ml-auto">
          {fullCnt > 0 && <span className="text-[10px] px-1.5 py-0.5 rounded-full border border-current/25 font-medium"
            style={{ background: "var(--status-online-bg)", color: "var(--status-online-fg)" }}>Full ×{fullCnt}</span>}
          {partCnt > 0 && <span className="text-[10px] px-1.5 py-0.5 rounded-full border border-current/25 font-medium"
            style={{ background: "var(--status-degraded-bg)", color: "var(--status-degraded-fg)" }}>2-Way ×{partCnt}</span>}
        </div>
      </div>

      <div className="px-4 py-3 border-b border-white/10">
        <p className="text-[10px] font-semibold text-[#475569] uppercase tracking-wider mb-2">Соседи ({adjacencies.length})</p>
        {adjacencies.length === 0 && <p className="text-[11px] text-[#475569]">Нет активных adjacency</p>}
        <div className="flex flex-col gap-0 divide-y divide-white/[0.04]">
          {adjacencies.map((adj, i) => {
            const ec = EDGE_COLOR[adj.state] ?? "#64748b"
            return (
              <div key={i} className="flex items-start gap-2.5 py-2.5">
                <span className="size-1.5 rounded-full shrink-0 mt-1.5" style={{ background: ec }} />
                <div className="flex-1 min-w-0">
                  <p className="text-xs font-mono font-medium text-[#f1f5f9] truncate leading-tight">{adj.peerLabel}</p>
                  <p className="text-[10px] font-mono text-[#64748b] mt-0.5">{adj.iface} · cost {adj.cost}</p>
                  <p className="text-[10px] text-[#475569] mt-0.5 tabular-nums">{adj.uptime}</p>
                </div>
                <span className="text-[10px] font-mono font-semibold shrink-0 mt-0.5" style={{ color: ec }}>{adj.state}</span>
              </div>
            )
          })}
        </div>
      </div>

      <div className="px-4 py-3">
        <p className="text-[10px] font-semibold text-[#475569] uppercase tracking-wider mb-2">
          Интерфейсы OSPF{ifaces.length > 0 && <span className="ml-1 text-[#374151]">({ifaces.length})</span>}
        </p>
        {ifaces.length === 0 && <p className="text-[11px] text-[#475569]">Нет данных</p>}
        <div className="flex flex-col gap-0 divide-y divide-white/[0.04]">
          {ifaces.map(iface => (
            <div key={iface.key} className="flex items-center gap-2 py-2">
              <span className={cn("size-1.5 rounded-full shrink-0", iface.active ? "bg-emerald-500" : "bg-[#374151]")} />
              <code className="text-[11px] font-mono flex-1 min-w-0 truncate" style={{ color: "#94a3b8" }}>{iface.interfaceName}</code>
              <span className="text-[10px] font-mono text-[#475569] shrink-0">
                {iface.area === "0.0.0.0" ? "backbone" : `area ${iface.area}`}
              </span>
              <span className="text-[10px] font-mono shrink-0" style={{ color: "#0ea5e9" }}>cost {iface.cost}</span>
            </div>
          ))}
        </div>
      </div>
    </div>
  )
}

// ─── interfaces tab ───────────────────────────────────────────────────────────

function InterfacesTab({
  items: initialItems,
  isLive,
  filterServerId,
  backendUrl,
  onLiveDataRefresh,
}: {
  items: OspfItem[]
  isLive: boolean
  filterServerId: string
  backendUrl: string
  onLiveDataRefresh: () => void
}) {
  const [items,    setItems]    = useState<OspfItem[]>(initialItems)
  const [dragging, setDragging] = useState<string | null>(null)
  const [dragOver, setDragOver] = useState<string | null>(null)
  const [optimizing, setOptimizing] = useState(false)
  const [liveOptimalCost, setLiveOptimalCost] = useState<Record<string, number>>({})

  // Sync with live data when it changes
  useEffect(() => {
    queueMicrotask(() => setItems(initialItems))
  }, [initialItems])

  const grouped = useMemo(() => {
    const byRouter: Record<string, { routerKey: string; routerLabel: string; areas: Record<string, OspfItem[]> }> = {}
    items.forEach(item => {
      if (!byRouter[item.routerKey])
        byRouter[item.routerKey] = { routerKey: item.routerKey, routerLabel: item.routerLabel, areas: {} }
      if (!byRouter[item.routerKey].areas[item.area])
        byRouter[item.routerKey].areas[item.area] = []
      byRouter[item.routerKey].areas[item.area].push(item)
    })
    return Object.values(byRouter).map(r => ({
      ...r,
      areas: Object.entries(r.areas)
        .map(([area, areaItems]) => ({ area, items: [...areaItems].sort((a, b) => a.cost - b.cost) }))
        .sort((a, b) => a.area.localeCompare(b.area)),
    }))
  }, [items])

  // Optimizer hints only work meaningfully for mock data
  const hints = useMemo(() => {
    if (isLive) return {}
    const out: Record<string, { optimalCost: number; prob: number }> = {}
    grouped.forEach(router => {
      const ranked = router.areas
        .flatMap(ag => ag.items)
        .map(item => ({ item, prob: OPTIMIZER_PROB[`${router.routerKey}::${item.interfaceName.toUpperCase()}`] ?? 0 }))
        .sort((a, b) => b.prob - a.prob || a.item.interfaceName.localeCompare(b.item.interfaceName))

      // В рамках одного роутера выдаём строго уникальные optimal cost.
      ranked.forEach(({ item, prob }, idx) => {
        out[item.key] = { optimalCost: (idx + 1) * COST_STEP, prob }
      })
    })
    return out
  }, [grouped, isLive])

  const needsOptimize = !isLive && items.some(item => hints[item.key] && hints[item.key].optimalCost !== item.cost)
  const canOptimizeLive = isLive && filterServerId !== ALL_SERVERS_ID
  const uniqueLiveFallbackOpt = useMemo(() => {
    const out: Record<string, number> = {}
    const byRouter: Record<string, OspfItem[]> = {}
    items.forEach((item) => {
      if (!byRouter[item.routerKey]) byRouter[item.routerKey] = []
      byRouter[item.routerKey].push(item)
    })
    Object.values(byRouter).forEach((routerItems) => {
      routerItems
        .slice()
        .sort((a, b) => a.cost - b.cost || a.interfaceName.localeCompare(b.interfaceName))
        .forEach((item, idx) => {
          out[item.key] = (idx + 1) * COST_STEP
        })
    })
    return out
  }, [items])

  async function handleLiveOptimize() {
    if (!canOptimizeLive) {
      toast.info("Выберите конкретный сервер для оптимизации OSPF")
      return
    }
    const ra = readStoredRouteOptimizerSettings()
    setOptimizing(true)
    try {
      const data = await requestJson<BackendOspfOptimizeResponse>(
        backendUrl,
        `/api/servers/${filterServerId}/ospf/optimize`,
        {
          method: "POST",
          body: JSON.stringify({ pingWeight: ra.pingWeight }),
        },
      )
      const byKey: Record<string, number> = {}
      data.interfaces.forEach((row) => {
        byKey[`${data.serverId}-${row.id}`] = row.optimalCost
      })
      setLiveOptimalCost(byKey)
      toast.success(`OSPF оптимизация применена: ${data.optimizedCount} интерфейсов`)
      onLiveDataRefresh()
    } catch (err) {
      toast.error(`Ошибка оптимизации OSPF: ${err instanceof Error ? err.message : String(err)}`)
    } finally {
      setOptimizing(false)
    }
  }

  function onDrop(routerKey: string, area: string, targetKey: string) {
    if (!dragging || dragging === targetKey) { setDragging(null); setDragOver(null); return }
    setItems(prev => {
      const areaItems = prev.filter(i => i.routerKey === routerKey && i.area === area).sort((a, b) => a.cost - b.cost)
      const fromIdx = areaItems.findIndex(i => i.key === dragging)
      const toIdx   = areaItems.findIndex(i => i.key === targetKey)
      if (fromIdx < 0 || toIdx < 0) return prev
      const reordered = moveInArray(areaItems, fromIdx, toIdx).map((item, idx) => ({ ...item, cost: (idx + 1) * COST_STEP }))
      return [...prev.filter(i => !(i.routerKey === routerKey && i.area === area)), ...reordered]
    })
    setDragging(null); setDragOver(null)
  }

  function handleOptimize() {
    setItems(prev => prev.map(item => {
      const h = hints[item.key]
      return h && item.cost !== h.optimalCost ? { ...item, cost: h.optimalCost } : item
    }))
    toast.success("Costs оптимизированы по рекомендациям оптимизатора")
  }

  return (
    <div className="flex flex-col gap-4">
      <div className="flex items-center gap-2 flex-wrap">
        <Button variant="outline" size="sm" onClick={() => setItems(initialItems)}>
          <RefreshCwIcon className="size-4" />Загрузить
        </Button>
        {needsOptimize && (
          <Button variant="outline" size="sm" onClick={handleOptimize}>
            <WandSparklesIcon className="size-4" />Оптимизировать
          </Button>
        )}
        {canOptimizeLive && (
          <Button variant="outline" size="sm" onClick={handleLiveOptimize} disabled={optimizing}>
            <WandSparklesIcon className={cn("size-4", optimizing && "animate-spin")} />
            {optimizing ? "Оптимизация OSPF…" : "Оптимизировать OSPF"}
          </Button>
        )}
        {!isLive && (
          <Button size="sm" onClick={() => toast.success("OSPF Interface Templates применены")}>
            <SaveIcon className="size-4" />Сохранить
          </Button>
        )}
      </div>

      {grouped.length === 0 && (
        <div className="rounded-md border border-border bg-muted/30 px-4 py-6 text-center text-sm text-muted-foreground">
          OSPF интерфейсы не настроены ни на одном сервере
        </div>
      )}

      <div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
        {grouped.map(router => {
          const totalIfaces = router.areas.reduce((s, a) => s + a.items.length, 0)
          return (
            <Frame key={router.routerKey} dense className="w-full overflow-hidden">
              <FramePanel className="p-0 overflow-hidden">
              <div className="flex items-center gap-3 px-4 py-3 border-b">
                <NetworkIcon className="size-4 text-muted-foreground shrink-0" />
                <div className="flex-1 min-w-0">
                  <p className="text-sm font-medium leading-none">{router.routerLabel}</p>
                  <p className="text-[11px] font-mono text-muted-foreground mt-0.5">{router.routerKey}</p>
                </div>
                <span className="text-[11px] text-muted-foreground">{totalIfaces} iface · {router.areas.length} area</span>
              </div>

              {router.areas.map((ag, aIdx) => (
                <div key={ag.area}>
                  {aIdx > 0 && <Separator />}
                  <div className="flex items-center gap-2 px-4 py-1.5 bg-muted/30">
                    <span className="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest">Area</span>
                    <code className="text-[11px] font-mono">{ag.area}</code>
                    {ag.area === "0.0.0.0" && <span className="text-[10px] text-muted-foreground/60">Backbone</span>}
                  </div>
                  <div className="divide-y divide-border/60">
                    {ag.items.map(item => {
                      const hint   = hints[item.key]
                      const matches = hint && hint.optimalCost === item.cost
                      const liveOptimal = liveOptimalCost[item.key] ?? uniqueLiveFallbackOpt[item.key] ?? item.cost
                      const costDiffers = liveOptimal !== item.cost
                      return (
                        <div key={item.key}
                          draggable={!isLive}
                          onDragStart={() => !isLive && setDragging(item.key)}
                          onDragEnd={() => { setDragging(null); setDragOver(null) }}
                          onDragOver={e => { e.preventDefault(); setDragOver(item.key) }}
                          onDrop={() => !isLive && onDrop(router.routerKey, ag.area, item.key)}
                          className={cn(
                            "flex items-center gap-3 px-4 py-2.5 select-none transition-colors",
                            !isLive && "cursor-grab",
                            dragging === item.key && "opacity-40",
                            dragOver === item.key && dragging !== item.key && "bg-primary/5",
                            dragging !== item.key && dragOver !== item.key && "hover:bg-muted/40",
                          )}>
                          <GripVerticalIcon className={cn("size-3.5 shrink-0", isLive ? "text-muted-foreground/10" : "text-muted-foreground/30")} />
                          <span className={cn("inline-block size-1.5 rounded-full shrink-0", item.active ? "bg-[var(--status-online)]" : "bg-muted-foreground/40")} />
                          <code className="text-xs font-mono flex-1 min-w-0 truncate">{item.interfaceName}</code>
                          {item.type && item.type !== "broadcast" && (
                            <Chip color="bg-violet-500/10 text-violet-600 dark:text-violet-400 border-violet-500/20">{item.type}</Chip>
                          )}
                          {item.useBfd && (
                            <Chip color="bg-sky-500/10 text-sky-600 dark:text-sky-400 border-sky-500/20">BFD</Chip>
                          )}
                          {hint && hint.prob > 0 && !isLive && (
                            <Chip color={matches
                              ? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20"
                              : "bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20"
                            }>opt {hint.optimalCost}</Chip>
                          )}
                          <Chip color="bg-sky-500/10 text-sky-600 dark:text-sky-400 border-sky-500/20">cur {item.cost}</Chip>
                          <Chip color={costDiffers
                            ? "bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20"
                            : "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20"
                          }>opt {liveOptimal}</Chip>
                        </div>
                      )
                    })}
                  </div>
                </div>
              ))}
            </FramePanel>
            </Frame>
          )
        })}
      </div>

      {!isLive && (
        <div className="flex items-center gap-4 flex-wrap px-1">
          <span className="text-xs text-muted-foreground">Легенда:</span>
          <span className="flex items-center gap-1.5 text-xs text-muted-foreground">
            <Chip color="bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20">opt 10</Chip>
            совпадает с оптимальным
          </span>
          <span className="flex items-center gap-1.5 text-xs text-muted-foreground">
            <Chip color="bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20">opt 20</Chip>
            рекомендован другой cost
          </span>
          <span className="flex items-center gap-1.5 text-xs text-muted-foreground">
            <GripVerticalIcon className="size-3.5 text-muted-foreground" />перетащите для изменения приоритета
          </span>
        </div>
      )}
    </div>
  )
}

// ─── neighbors tab ────────────────────────────────────────────────────────────

function NeighborsTab({
  neighbors, items, graphNodes, graphEdges, routerIds,
}: {
  neighbors: OspfNeighbor[]
  items: OspfItem[]
  graphNodes: GraphNode[]
  graphEdges: GraphEdge[]
  routerIds:  Record<string, string>
}) {
  const [highlightId,  setHighlightId]  = useState<string | null>(null)
  const [selectedId,   setSelectedId]   = useState<string | null>(null)
  const [showDots,     setShowDots]     = useState(true)

  const fullCount = neighbors.filter(n => n.state === "Full").length

  return (
    <div className="flex flex-col gap-5">
      <KpiStatGrid
        aria-label="Сводка соседей OSPF"
        items={[
          {
            id: "neighbors",
            label: "Всего соседей",
            value: neighbors.length,
            icon: <UsersIcon className="size-4" />,
            iconClassName: "text-muted-foreground",
          },
          {
            id: "full",
            label: "Full",
            value: fullCount,
            icon: <CheckCircleIcon className="size-4" />,
            iconClassName: "text-success",
          },
          {
            id: "not-full",
            label: "Не Full",
            value: neighbors.length - fullCount,
            icon: <AlertCircleIcon className="size-4" />,
            iconClassName: neighbors.length - fullCount > 0 ? "text-warning" : "text-muted-foreground",
            variant: neighbors.length - fullCount > 0 ? "warning" : "default",
          },
        ]}
      />

      {graphNodes.length > 0 && (
        <div className="flex rounded-xl overflow-hidden border border-white/[0.06]">
          <div className="flex-1 relative" style={{ background: "#060d1a", minWidth: 0 }}>
            <TopologyGraph
              graphNodes={graphNodes}
              graphEdges={graphEdges}
              routerIds={routerIds}
              neighbors={neighbors}
              highlightId={highlightId}
              selectedId={selectedId}
              showDots={showDots}
              onNodeClick={setSelectedId}
            />
            <button
              onClick={() => setShowDots(v => !v)}
              className={cn(
                "absolute top-3 right-3 flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg border text-[11px] font-medium transition-colors backdrop-blur-sm",
                showDots
                  ? "bg-black/60 border-white/15 text-white/70 hover:text-white"
                  : "bg-black/60 border-white/10 text-white/30 hover:text-white/60",
              )}>
              <span className={cn("size-1.5 rounded-full", showDots ? "bg-[var(--status-online)]" : "bg-white/20")} />
              Анимация
            </button>
            {!selectedId && (
              <div className="absolute bottom-3 right-3 text-[10px] font-mono text-white/20 pointer-events-none">
                кликните по узлу для деталей
              </div>
            )}
          </div>
          {selectedId && (
            <NodeDetailPanel
              nodeId={selectedId}
              graphNodes={graphNodes}
              routerIds={routerIds}
              neighbors={neighbors}
              items={items}
              onClose={() => setSelectedId(null)}
            />
          )}
        </div>
      )}

      <DataPageCard>
        <OspfNeighborsDataGrid
          neighbors={neighbors}
          selectedRouterId={selectedId}
          onSelect={setSelectedId}
          onHighlight={setHighlightId}
        />
      </DataPageCard>
    </div>
  )
}

// ─── routes tab (mock) ────────────────────────────────────────────────────────

function RoutesTab({ routes }: { routes: OspfRoute[] }) {
  const typeCount = Object.entries(
    routes.reduce<Record<string, number>>((acc, r) => { acc[r.type] = (acc[r.type] ?? 0) + 1; return acc }, {})
  )
  return (
    <div className="flex flex-col gap-4">
      <div className="flex items-center gap-3 flex-wrap">
        <span className="text-sm font-medium">{routes.length} маршрутов</span>
        {typeCount.map(([type, count]) => (
          <span key={type} className={cn("inline-flex items-center rounded border px-2 py-0.5 text-[11px] font-medium", routeTypeClass(type as OspfRoute["type"]))}>
            {type}: {count}
          </span>
        ))}
      </div>
      <DataPageCard>
        <OspfRoutesDataGrid routes={routes} />
      </DataPageCard>
      <div className="flex items-center gap-5 flex-wrap px-1">
        <span className="text-xs text-muted-foreground">Типы:</span>
        {([
          { type: "O",    desc: "Internal (intra-area)" },
          { type: "O IA", desc: "Inter-Area" },
          { type: "O E1", desc: "External Type 1" },
          { type: "O E2", desc: "External Type 2" },
        ] as const).map(({ type, desc }) => (
          <span key={type} className="flex items-center gap-1.5 text-xs text-muted-foreground">
            <span className={cn("inline-flex items-center rounded border px-1.5 py-0.5 text-[10px] font-medium", routeTypeClass(type))}>{type}</span>
            {desc}
          </span>
        ))}
      </div>
    </div>
  )
}

// ─── BFD tab ──────────────────────────────────────────────────────────────────

function BfdTab({ sessions }: { sessions: BfdSession[] }) {
  const upCount   = sessions.filter(b => b.state === "Up").length
  const downCount = sessions.filter(b => b.state === "Down" || b.state === "AdminDown").length
  const initCount = sessions.filter(b => b.state === "Init").length
  const totalPkts = sessions.reduce((s, b) => s + b.packetsRx, 0)

  function fmtMs(ms: number) {
    if (!ms) return "—"
    if (ms < 1000) return `${ms}ms`
    return `${(ms / 1000).toFixed(1)}s`
  }

  return (
    <div className="flex flex-col gap-4">
      <KpiStatGrid
        aria-label="Сводка BFD"
        items={[
          {
            id: "sessions",
            label: "Сессий BFD",
            value: sessions.length,
            icon: <ActivityIcon className="size-4" />,
            iconClassName: "text-muted-foreground",
          },
          {
            id: "up",
            label: "Up",
            value: upCount,
            icon: <CheckCircleIcon className="size-4" />,
            iconClassName: "text-success",
          },
          {
            id: "down",
            label: "Down / Admin",
            value: downCount,
            icon: <AlertCircleIcon className="size-4" />,
            iconClassName: downCount > 0 ? "text-destructive" : "text-muted-foreground",
            variant: downCount > 0 ? "destructive" : "default",
          },
          {
            id: "init",
            label: "Init / другие",
            value: initCount,
            icon: <LayersIcon className="size-4" />,
            iconClassName: initCount > 0 ? "text-warning" : "text-muted-foreground",
            variant: initCount > 0 ? "warning" : "default",
          },
        ]}
      />

      {sessions.length === 0 && (
        <div className="rounded-md border border-border bg-muted/30 px-4 py-8 text-center text-sm text-muted-foreground">
          BFD-сессий не обнаружено ни на одном сервере
        </div>
      )}

      {sessions.length > 0 && (
        <DataPageCard>
          <OspfBfdDataGrid sessions={sessions} />
        </DataPageCard>
      )}

      {sessions.length > 0 && (
        <div className="flex items-center gap-4 flex-wrap px-1">
          <p className="text-xs text-muted-foreground">
            BFD обнаруживает сбои быстрее OSPF Hello/Dead таймеров.
            Tx&nbsp;/&nbsp;Rx — интервалы отправки и приёма контрольных пакетов.
            {totalPkts > 0 && <span className="ml-2 tabular-nums">Всего получено: {totalPkts.toLocaleString()} пакетов.</span>}
          </p>
        </div>
      )}
    </div>
  )
}

// ─── page ─────────────────────────────────────────────────────────────────────

const TABS: Array<{ id: OspfTab; label: string; icon: React.ReactNode }> = [
  { id: "interfaces", label: "Интерфейсы", icon: <NetworkIcon  className="size-3.5" /> },
  { id: "neighbors",  label: "Соседи",     icon: <ActivityIcon className="size-3.5" /> },
  { id: "routes",     label: "Маршруты",   icon: <RouteIcon    className="size-3.5" /> },
  { id: "bfd",        label: "BFD",        icon: <ShieldIcon   className="size-3.5" /> },
]

export default function OspfPage() {
  const [activeTab,      setActiveTab]      = useState<OspfTab>("interfaces")
  const [filterServerId, setFilterServerId] = useState<string>(ALL_SERVERS_ID)

  const { mode, backendUrl } = useDataSource()
  const isLive = mode === "live"

  const [liveData,   setLiveData]   = useState<BackendOspfAll | null>(null)
  const [loading,    setLoading]    = useState(false)
  const [fetchedAt,  setFetchedAt]  = useState<Date | null>(null)
  const [liveError,  setLiveError]  = useState<string | null>(null)
  const [fetchTick,  setFetchTick]  = useState(0)

  // Reset server filter when switching live ↔ mock
  useEffect(() => {
    queueMicrotask(() => setFilterServerId("all"))
  }, [isLive])

  useEffect(() => {
    if (!isLive) {
      queueMicrotask(() => setLiveData(null))
      return
    }
    let cancelled = false
    queueMicrotask(() => {
      if (cancelled) return
      setLoading(true)
      setLiveError(null)
      void requestJson<BackendOspfAll>(backendUrl, "/api/ospf/all")
        .then(data => {
          if (cancelled) return
          setLiveData(data); setFetchedAt(new Date()); setLoading(false)
        })
        .catch((err: unknown) => {
          if (cancelled) return
          setLiveError(err instanceof Error ? err.message : String(err)); setLoading(false)
        })
    })
    return () => { cancelled = true }
  }, [isLive, backendUrl, fetchTick])

  // Derive frontend types from backend data or use mocks
  const { items, neighbors, graphNodes, graphEdges, routerIds, bfdSessions } = useMemo(() => {
    if (isLive && liveData) {
      // Build interface→cost map for neighbor cost lookup
      const ifaceMap = new Map<string, number>()
      for (const iface of liveData.interfaces) {
        ifaceMap.set(`${iface.serverId}::${iface.interface}`, iface.cost)
      }

      const items       = liveData.interfaces
        .map(backendToItem)
        .filter((item) => !isRefInterfaceName(item.interfaceName))
      const neighbors   = liveData.neighbors.map(b => backendToNeighbor(b, ifaceMap))
      const bfdSessions = (liveData.bfdSessions ?? []).map(backendToBfdSession)

      // Build routerIds from instances
      const routerIds: Record<string, string> = {}
      for (const inst of liveData.instances) {
        if (!routerIds[String(inst.serverId)]) {
          routerIds[String(inst.serverId)] = inst.routerId
        }
      }

      const { nodes: graphNodes, edges: graphEdges } = buildLiveGraph(neighbors)
      return { items, neighbors, graphNodes, graphEdges, routerIds, bfdSessions }
    }
    if (isLive) {
      return {
        items:       [],
        neighbors:   [],
        graphNodes:  [],
        graphEdges:  [],
        routerIds:   {},
        bfdSessions: [],
      }
    }
    return {
      items:       MOCK_ITEMS,
      neighbors:   MOCK_NEIGHBORS,
      graphNodes:  MOCK_GRAPH_NODES,
      graphEdges:  MOCK_GRAPH_EDGES,
      routerIds:   MOCK_ROUTER_IDS,
      bfdSessions: MOCK_BFD,
    }
  }, [isLive, liveData])

  // ── server list for chips bar ────────────────────────────────────────────────
  const ospfServers = useMemo((): OspfServerInfo[] => {
    if (isLive && liveData) {
      const seen = new Map<string, OspfServerInfo>()
      for (const inst of liveData.instances) {
        const id = String(inst.serverId)
        if (!seen.has(id))
          seen.set(id, { id, label: inst.serverName, site: inst.serverSite, country: inst.serverCountry })
      }
      // Fallback: pick up servers that appear in neighbors/interfaces but not in instances
      for (const n of liveData.neighbors) {
        const id = String(n.serverId)
        if (!seen.has(id))
          seen.set(id, { id, label: n.serverName, site: n.serverSite, country: n.serverCountry })
      }
      return [...seen.values()]
    }
    if (isLive) return []
    // Mock: derive from MOCK_GRAPH_NODES that actually have interfaces
    const activeIds = new Set(MOCK_ITEMS.map(i => i.routerKey))
    const siteCountry: Record<string, string> = { MSK: "RU", SPB: "RU", FRA: "DE", AMS: "NL", SGP: "SG" }
    return MOCK_GRAPH_NODES
      .filter(n => activeIds.has(n.id))
      .map(n => ({ id: n.id, label: n.label, site: n.site, country: siteCountry[n.site] ?? "" }))
  }, [isLive, liveData])

  // Per-server counts for chips (all data, before filtering)
  const serverCounts = useMemo(() => {
    const out: Record<string, { ifaces: number; neighbors: number; bfd: number }> = {}
    const ensure = (id: string) => { if (!out[id]) out[id] = { ifaces: 0, neighbors: 0, bfd: 0 } }
    for (const item of items)        { ensure(item.routerKey);   out[item.routerKey].ifaces++ }
    for (const n of neighbors)       { ensure(n.localRouter);    out[n.localRouter].neighbors++ }
    for (const b of bfdSessions)     { ensure(b.serverId);       out[b.serverId].bfd++ }
    return out
  }, [items, neighbors, bfdSessions])

  // ── filtered display data ─────────────────────────────────────────────────────
  const displayItems       = filterServerId === ALL_SERVERS_ID ? items       : items.filter(i => i.routerKey === filterServerId)
  const displayNeighbors   = filterServerId === ALL_SERVERS_ID ? neighbors   : neighbors.filter(n => n.localRouter === filterServerId)
  const displayBfdSessions = filterServerId === ALL_SERVERS_ID ? bfdSessions : bfdSessions.filter(b => b.serverId === filterServerId)

  const ospfRailItems = useMemo<ServerTileItem[]>(() => (
    ospfServers.map((s) => {
      const counts = serverCounts[s.id]
      const host = s.label.replace(/^mt-/, "")
      return {
        id: s.id,
        name: host,
        host,
        site: s.site,
        country: s.country || undefined,
        meta: counts
          ? `${counts.neighbors}n · ${counts.ifaces}i${counts.bfd > 0 ? ` · ${counts.bfd}b` : ""}`
          : undefined,
      }
    })
  ), [ospfServers, serverCounts])

  // Graph always shows full topology (highlight is handled by node click inside tab)
  // KPIs reflect the current filter
  const totalRouters    = new Set(displayItems.map(i => i.routerKey)).size
  const totalInterfaces = displayItems.length
  const totalAreas      = new Set(displayItems.map(i => i.area)).size

  return (
    <ServerRailLayout
      items={ospfRailItems}
      selectedId={filterServerId}
      onSelect={setFilterServerId}
      showAll
      allCount={ospfServers.length}
      loading={isLive && loading && ospfServers.length === 0}
      header={
        <PageHeader
          crumbs={[{ label: "Инструменты" }, { label: "OSPF" }]}
          actions={
            <>
              <ServerRailMobileButton />
              <Button variant="outline" size="sm" onClick={() => setFetchTick(t => t + 1)}>
                <RefreshCwIcon className={cn("size-4", loading && "animate-spin")} />
                Обновить
              </Button>
            </>
          }
        />
      }
      banner={
        <div className="border-b bg-background shrink-0">
          <div className="flex items-center px-6">
            {TABS.map(t => (
              <button key={t.id} onClick={() => setActiveTab(t.id)}
                className={cn(
                  "flex items-center gap-2 px-4 py-3 text-sm font-medium border-b-2 transition-colors -mb-px",
                  activeTab === t.id
                    ? "border-primary text-foreground"
                    : "border-transparent text-muted-foreground hover:text-foreground hover:border-border",
                )}>
                {t.icon}{t.label}
              </button>
            ))}
          </div>
        </div>
      }
    >
        <div className="flex flex-col gap-5">

          {/* data source banner */}
          {isLive && loading && (
            <div className="flex items-center gap-2 text-xs text-muted-foreground">
              <RefreshCwIcon className="size-3.5 animate-spin" />Загрузка OSPF данных…
            </div>
          )}
          {isLive && !loading && fetchedAt && !liveError && (
            <div className="flex items-center gap-2">
              <span className="inline-flex items-center gap-1.5 rounded-full border border-emerald-500/30 bg-emerald-500/10 px-2.5 py-0.5 text-[11px] font-medium text-emerald-600 dark:text-emerald-400">
                <span className="size-1.5 rounded-full bg-emerald-500" />
                Живые данные · обновлено {fetchedAt.toLocaleTimeString("ru")}
              </span>
              <button onClick={() => setFetchTick(t => t + 1)}
                className="flex items-center gap-1 text-[11px] text-muted-foreground hover:text-foreground transition-colors">
                <RefreshCwIcon className="size-3" />Обновить
              </button>
            </div>
          )}
          {isLive && liveError && (
            <div className="flex items-center gap-2 rounded-md border border-amber-500/30 bg-amber-500/8 px-3 py-2">
              <span className="size-1.5 rounded-full bg-amber-500 shrink-0" />
              <p className="text-xs text-amber-600 dark:text-amber-400">Ошибка загрузки: {liveError}</p>
            </div>
          )}
          {mode === "mock" && (
            <span className="inline-flex w-fit items-center gap-1.5 rounded-full border border-border bg-muted/40 px-2.5 py-0.5 text-[11px] font-medium text-muted-foreground">
              Моковые данные
            </span>
          )}

          <KpiStatGrid
            aria-label="Сводка OSPF"
            items={[
              {
                id: "routers",
                label: "Роутеров",
                value: totalRouters,
                icon: <RouterIcon className="size-4" />,
                iconClassName: "text-muted-foreground",
              },
              {
                id: "ifaces",
                label: "Интерфейсов",
                value: totalInterfaces,
                icon: <NetworkIcon className="size-4" />,
                iconClassName: "text-info",
              },
              {
                id: "areas",
                label: "Зон (Area)",
                value: totalAreas,
                icon: <LayersIcon className="size-4" />,
                iconClassName: "text-primary",
              },
            ]}
          />

          {activeTab === "interfaces" && (
            <InterfacesTab
              items={displayItems}
              isLive={isLive}
              filterServerId={filterServerId}
              backendUrl={backendUrl}
              onLiveDataRefresh={() => setFetchTick(t => t + 1)}
            />
          )}
          {activeTab === "neighbors"  && (
            <NeighborsTab
              neighbors={displayNeighbors}
              items={displayItems}
              graphNodes={graphNodes}
              graphEdges={graphEdges}
              routerIds={routerIds}
            />
          )}
          {activeTab === "routes"     && <RoutesTab routes={isLive ? [] : MOCK_ROUTES} />}
          {activeTab === "bfd"        && <BfdTab sessions={displayBfdSessions} />}

        </div>
    </ServerRailLayout>
  )
}
