"use client"

import { useEffect, useRef, useState, useMemo, useCallback } from "react"
import { PageHeader } from "@/components/page-header"
import { FormToggle } from "@/components/form-kit"
import { Frame, FramePanel } from "@/components/reui/frame"
import { OpsPanel } from "@/components/ops-panel"
import { DataPageCard } from "@/components/data-page-card"
import {
  ProbesScheduleDataGrid,
  type SchedRule,
  type SchedType,
} from "@/components/data-grids/probes-schedule-data-grid"
import {
  ProbesSpeedProbesDataGrid,
  type SpeedProbeApiRow,
} from "@/components/data-grids/probes-speed-probes-data-grid"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { ServerRailLayout, ServerRailMobileButton } from "@/components/server-rail-layout"
import { type ServerTileItem } from "@/components/server-tile-rail"
import { servers, greTunnels, type GreTunnel, type Server } from "@/lib/data"
import { useDataSource } from "@/lib/data-source"
import {
  PlayIcon, SquareIcon, CopyIcon, Trash2Icon, PlusIcon,
  ActivityIcon, RouteIcon, SearchIcon, NetworkIcon, RulerIcon,
  ZapIcon, ClockIcon, CheckIcon, TerminalIcon,
  LoaderCircleIcon, AlertCircleIcon,
} from "lucide-react"
import { cn } from "@/lib/utils"
import { requestJson } from "@/shared/api/http-client"

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

type DiagTool    = "ping" | "traceroute" | "bandwidth" | "dns" | "route" | "mtu"
type RunStatus   = "running" | "done" | "error"
type TraceProto  = "icmp" | "udp" | "tcp"
type DnsType     = "A" | "AAAA" | "MX" | "NS" | "TXT" | "CNAME" | "PTR"

interface OutputLine { text: string; kind: "normal" | "ok" | "err" | "dim" | "header" | "cmd" }

interface DiagTest {
  id: string
  tool: DiagTool
  status: RunStatus | "error"
  srcServerId: string
  srcServerName: string
  target: string
  command: string
  startedAt: number
  lines: OutputLine[]
  totalLines: number   // final line count — reveals progressively
  /** demo: симуляция в браузере; live: ответ MikroTik через бекенд */
  source?: "demo" | "live"
}

// SchedRule imported from probes-schedule-data-grid


// ─── tool metadata ────────────────────────────────────────────────────────────

const TOOL_META: Record<DiagTool, {
  label: string
  ros: string              // RouterOS tool path
  Icon: React.FC<{ className?: string }>
  color: string
  description: string
}> = {
  ping:       { label: "Ping",        ros: "/tool ping",           Icon: ActivityIcon, color: "text-sky-500",     description: "Проверка связи и RTT" },
  traceroute: { label: "Traceroute",  ros: "/tool traceroute",     Icon: RouteIcon,    color: "text-violet-500",  description: "Трассировка маршрута" },
  bandwidth:  { label: "BW-тест",    ros: "/tool bandwidth-test", Icon: ZapIcon,      color: "text-emerald-500", description: "Пропускная способность" },
  dns:        { label: "DNS",         ros: "/resolve",             Icon: SearchIcon,   color: "text-amber-500",   description: "Разрешение DNS-имён" },
  route:      { label: "Маршрут",    ros: "/ip route lookup",     Icon: NetworkIcon,  color: "text-blue-500",    description: "Поиск активного маршрута" },
  mtu:        { label: "MTU-тест",   ros: "/tool ping",           Icon: RulerIcon,    color: "text-orange-500",  description: "Определение MTU пути" },
}

function makeApiFetch(backendUrl: string) {
  return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
    return requestJson<T>(backendUrl, path, init)
  }
}

function inferProbeLineKind(line: string): OutputLine["kind"] {
  const l = line.toLowerCase()
  if (l.includes("error:") || (l.includes("timeout") && l.includes("seq="))) return "err"
  if (l.includes("sent=") || l.includes("received=") || l.includes("packet-loss") || l.includes("✓")) return "ok"
  if (l.startsWith(";;") || l.trim() === "") return "dim"
  if (/ADDRESS|QUESTION|bandwidth-test|^ping |lookup |MTU discovery/i.test(line)) return "header"
  return "normal"
}

function parseProbeOutput(text: string): OutputLine[] {
  return text.split("\n").map(t => ({ text: t || " ", kind: inferProbeLineKind(t) }))
}

interface BackendServerRow {
  id: number
  name: string
  host: string
  site: string
  country: string
  asn: string
  type: Server["type"]
  enabled: boolean
  status?: Server["status"]
  latency?: number | null
}

// SpeedProbeApiRow imported from probes-speed-probes-data-grid

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

function jitter(base: number, pct = 0.15) {
  return Math.max(1, Math.round(base + (Math.random() - 0.5) * base * pct * 2))
}

function seededRng(seed: string) {
  let h = 0
  for (let i = 0; i < seed.length; i++) h = (Math.imul(31, h) + seed.charCodeAt(i)) | 0
  h = Math.abs(h)
  return (n = 233280) => { h = (h * 9301 + 49297) % n; return h / n }
}

function baseRtt(srcId: string, _target: string): number {
  const map: Record<string, number> = { srv1: 13, srv2: 11, srv3: 5, srv4: 8, srv5: 80, srv6: 8, srv7: 14 }
  return map[srcId] ?? 20
}

function randomIp(rng: () => number, base = "10.") {
  return `${base}${Math.floor(rng() * 255)}.${Math.floor(rng() * 255)}.${Math.floor(rng() * 255)}`
}

// ─── output generators ────────────────────────────────────────────────────────

function genPingOutput(target: string, srcId: string, count: number, size: number, _ttl: number): OutputLine[] {
  const rng  = seededRng(target + srcId)
  const base = baseRtt(srcId, target)
  const rtts: (number | null)[] = Array.from({ length: count }, () =>
    rng() < 0.04 ? null : jitter(base, 0.2),
  )
  const lines: OutputLine[] = [
    { text: `  SEQ HOST                                    SIZE TTL TIME   STATUS`, kind: "header" },
  ]
  rtts.forEach((rtt, i) => {
    lines.push({
      text: `  ${String(i).padStart(3)} ${target.padEnd(40)} ${String(size).padStart(4)} ${String(rtt ? 118 : 0).padStart(3)} ${rtt ? `${rtt}ms`.padStart(6) : "      "} ${rtt ? "" : "timeout"}`,
      kind: rtt ? "normal" : "err",
    })
  })
  const recv = rtts.filter(r => r !== null) as number[]
  const loss = Math.round((count - recv.length) / count * 100)
  lines.push({ text: "", kind: "dim" })
  lines.push({
    text: `  sent=${count} received=${recv.length} packet-loss=${loss}% min-rtt=${Math.min(...recv)}ms avg-rtt=${Math.round(recv.reduce((a, b) => a + b, 0) / recv.length)}ms max-rtt=${Math.max(...recv)}ms`,
    kind: loss === 0 ? "ok" : "err",
  })
  return lines
}

function genTraceOutput(target: string, srcId: string, proto: TraceProto, maxHops: number): OutputLine[] {
  const rng  = seededRng(target + srcId + proto)
  const base = baseRtt(srcId, target)
  const hopCount = Math.min(maxHops, 4 + Math.floor(rng() * 3))
  const lines: OutputLine[] = [
    { text: `  # ADDRESS                                    LOSS  SENT   LAST     AVG      BEST     WORST`, kind: "header" },
  ]
  for (let i = 1; i <= hopCount; i++) {
    const addr = i === 1 ? "10.200.0.2" : i === hopCount ? target : randomIp(rng, "95.213.")
    const frac  = i / hopCount
    const rtt1  = Math.round(base * frac * jitter(1, 0.1))
    const rtt2  = Math.round(base * frac * jitter(1, 0.08))
    const rtt3  = Math.round(base * frac * jitter(1, 0.12))
    const avg   = Math.round((rtt1 + rtt2 + rtt3) / 3)
    lines.push({
      text: `  ${String(i).padStart(2)} ${addr.padEnd(40)}   0%     3    ${`${rtt1}ms`.padStart(6)}  ${`${avg}ms`.padStart(6)}  ${`${Math.min(rtt1,rtt2,rtt3)}ms`.padStart(6)}  ${`${Math.max(rtt1,rtt2,rtt3)}ms`.padStart(6)}`,
      kind: i === hopCount ? "ok" : "normal",
    })
  }
  return lines
}

function genDnsOutput(target: string, srcId: string, type: DnsType): OutputLine[] {
  const rng = seededRng(target + srcId + type)
  const ttl = 300 - Math.floor(rng() * 200)
  const time = 5 + Math.floor(rng() * 20)
  const lines: OutputLine[] = [
    { text: `;; QUESTION SECTION:`, kind: "dim" },
    { text: `;; ${target}.     IN  ${type}`, kind: "dim" },
    { text: "", kind: "dim" },
    { text: `;; ANSWER SECTION:`, kind: "header" },
  ]
  const count = type === "A" ? 2 + Math.floor(rng() * 2) : 1
  for (let i = 0; i < count; i++) {
    let value = ""
    if (type === "A")     value = `142.250.${74 + Math.floor(rng() * 10)}.${Math.floor(rng() * 200)}`
    if (type === "AAAA")  value = `2001:db8::${Math.floor(rng() * 0xffff).toString(16)}`
    if (type === "MX")    value = `10 mail.${target}.`
    if (type === "NS")    value = `ns${i + 1}.${target}.`
    if (type === "TXT")   value = `"v=spf1 include:${target} ~all"`
    if (type === "CNAME") value = `${target}.cdn.example.com.`
    if (type === "PTR")   value = `${target}.in-addr.arpa.`
    lines.push({ text: `${target.padEnd(24)} ${String(ttl).padStart(5)}  IN  ${type.padEnd(5)} ${value}`, kind: "ok" })
  }
  lines.push({ text: "", kind: "dim" })
  lines.push({ text: `;; Query time: ${time} msec`, kind: "dim" })
  lines.push({ text: `;; SERVER: 1.1.1.1`, kind: "dim" })
  return lines
}

function genRouteOutput(target: string, srcId: string): OutputLine[] {
  const rng     = seededRng(target + srcId)
  const gw      = `10.200.0.${2 + Math.floor(rng() * 14)}`
  const iface   = ["gre-msk-fra", "gre-msk-ams", "gre-msk-spb"][Math.floor(rng() * 3)]
  const prefix  = target.split(".").slice(0, 3).join(".") + ".0/24"
  const asn     = [15169, 32934, 20940][Math.floor(rng() * 3)]
  const comm    = `65001:${100 + Math.floor(rng() * 4) * 100}`
  return [
    { text: `Flags: X - disabled, A - active, B - blackhole, U - unreachable`, kind: "dim" },
    { text: "", kind: "dim" },
    { text: ` #   DST-ADDRESS        PREF-SRC  GATEWAY         DISTANCE SCOPE TARGET-SCOPE`, kind: "header" },
    { text: ` 0 A ${prefix.padEnd(18)}           ${gw.padEnd(15)}      200    30           10`, kind: "ok" },
    { text: `     routing-table: main`, kind: "dim" },
    { text: `     bgp-as-path: ${asn}`, kind: "normal" },
    { text: `     bgp-communities: ${comm} ${asn}:5003`, kind: "normal" },
    { text: `     bgp-local-pref: 100`, kind: "dim" },
    { text: `     bgp-med: 0`, kind: "dim" },
    { text: `     bgp-origin: igp`, kind: "dim" },
    { text: `     bgp-nexthop: ${gw}`, kind: "dim" },
    { text: `     bgp-ext-communities: (nothing)`, kind: "dim" },
    { text: `     interface: ${iface}`, kind: "normal" },
    { text: `     gateway: ${gw}`, kind: "normal" },
    { text: "", kind: "dim" },
    { text: `1 route found`, kind: "ok" },
  ]
}

function genMtuOutput(target: string, srcId: string): OutputLine[] {
  const base     = baseRtt(srcId, target)
  const mtu      = [1476, 1472, 1468, 1400][Math.floor(Math.random() * 2)]  // common GRE MTUs
  const sizes    = [1500, 1492, mtu + 8, mtu + 4, mtu, mtu - 4]
  const lines: OutputLine[] = [
    { text: `MTU path discovery → ${target} (do-not-fragment ping)`, kind: "header" },
    { text: "", kind: "dim" },
    { text: `  SIZE   RESULT               RTT`, kind: "dim" },
  ]
  sizes.forEach(sz => {
    const pass = sz <= mtu
    const rtt  = pass ? jitter(base, 0.1) : null
    lines.push({
      text: `  ${String(sz).padStart(4)}   ${pass ? "✓ PASS" : "✗ FAIL (frag needed)"}  ${pass ? `${rtt}ms` : "—"}`,
      kind: pass ? "ok" : "err",
    })
  })
  lines.push({ text: "", kind: "dim" })
  lines.push({ text: `MTU discovered: ${mtu} bytes`, kind: "ok" })
  const overhead = 1500 - mtu
  lines.push({ text: `Overhead: ${overhead} bytes (GRE ${overhead >= 24 ? "+IPsec" : "no IPsec"})`, kind: "dim" })
  return lines
}

function genBwOutput(target: string, srcId: string, proto: string, duration: number): OutputLine[] {
  const base = [410, 580, 220, 680][Math.floor(Math.random() * 4)]
  seededRng(target + srcId)
  const lines: OutputLine[] = [
    { text: `         status: running`, kind: "dim" },
    { text: `      direction: both`, kind: "dim" },
    { text: `       protocol: ${proto.toUpperCase()}`, kind: "dim" },
    { text: `       duration: ${duration}s`, kind: "dim" },
    { text: "", kind: "dim" },
  ]
  for (let t = 2; t <= duration; t += 2) {
    const tx = jitter(base * 0.98, 0.08)
    const rx = jitter(base * 0.95, 0.09)
    lines.push({
      text: `  [${String(t).padStart(2)}s]  tx-current: ${tx}Mbps   rx-current: ${rx}Mbps`,
      kind: "normal",
    })
  }
  const txAvg = jitter(base * 0.97, 0.04)
  const rxAvg = jitter(base * 0.94, 0.04)
  lines.push({ text: "", kind: "dim" })
  lines.push({ text: `         status: done`, kind: "ok" })
  lines.push({ text: `  tx-total-average: ${txAvg}Mbps`, kind: "ok" })
  lines.push({ text: `  rx-total-average: ${rxAvg}Mbps`, kind: "ok" })
  return lines
}

// ─── command builder ──────────────────────────────────────────────────────────

/** RouterOS принимает в src-address только IPv4; FQDN подставлять нельзя. */
function isIpv4Literal(s: string): boolean {
  const t = s.trim()
  if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(t)) return false
  return t.split(".").every(p => {
    const n = Number.parseInt(p, 10)
    return Number.isFinite(n) && n >= 0 && n <= 255
  })
}

/** Превью: IPv4 из API, иначе литерал host если уже IP, иначе плейсхолдер пока DNS не готов. */
function pickSrcAddressRos(host: string, resolvedLiveIpv4: string | null | undefined): string {
  if (isIpv4Literal(host)) return host.trim()
  if (resolvedLiveIpv4 === undefined) return "…"
  if (resolvedLiveIpv4 === null) return "?"
  return resolvedLiveIpv4
}

function buildCommand(
  serversList: Server[],
  tool: DiagTool, src: string, target: string,
  opts: { pingCount?: number; pingSize?: number; pingTtl?: number; traceProto?: TraceProto; traceMaxHops?: number; traceUseDns?: boolean; dnsType?: DnsType; mtuStart?: number; bwProto?: string; bwDuration?: number; bwTarget?: string },
  resolvedSrcIpv4?: string | null,
): string {
  const host = serversList.find(s => s.id === src)?.host ?? "?"
  const srcRos = pickSrcAddressRos(host, resolvedSrcIpv4)
  switch (tool) {
    case "ping":
      return `/tool ping address=${target} count=${opts.pingCount ?? 5} size=${opts.pingSize ?? 64} ttl=${opts.pingTtl ?? 64} src-address=${srcRos}`
    case "traceroute":
      return `/tool traceroute address=${target} max-hops=${opts.traceMaxHops ?? 30} protocol=${opts.traceProto ?? "icmp"} timeout=00:00:01 count=1 use-dns=${opts.traceUseDns ? "yes" : "no"} src-address=${srcRos}`
    case "bandwidth":
      return `/tool bandwidth-test address=${opts.bwTarget ?? target} duration=${opts.bwDuration ?? 10}s protocol=${opts.bwProto ?? "tcp"} direction=both`
    case "dns":
      return `/resolve ${target} type=${opts.dnsType ?? "A"} server=1.1.1.1`
    case "route":
      return `/ip route lookup ip=${target}`
    case "mtu":
      return `/tool ping address=${target} do-not-fragment count=1 size=${opts.mtuStart ?? 1500} src-address=${srcRos}`
  }
}

// ─── small UI components ──────────────────────────────────────────────────────

function NativeSelect({ value, onChange, children, className }: {
  value: string; onChange: (v: string) => void; children: React.ReactNode; className?: string
}) {
  return (
    <select value={value} onChange={e => onChange(e.target.value)}
      className={cn(
        "h-8 min-w-0 rounded-lg border border-input bg-background px-2.5 py-1 text-sm text-foreground",
        "transition-colors outline-none",
        "focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50",
        className,
      )}>
      {children}
    </select>
  )
}

function OptionLabel({ children }: { children: React.ReactNode }) {
  return <p className="text-[11px] font-medium text-muted-foreground mb-1">{children}</p>
}

function SegBtn<T extends string | number>({ value, current, onClick, children }: {
  value: T; current: T; onClick: (v: T) => void; children: React.ReactNode
}) {
  return (
    <button onClick={() => onClick(value)}
      className={cn(
        "px-2.5 py-1 text-xs font-mono border-r last:border-r-0 border-input transition-colors",
        value === current ? "bg-muted text-foreground font-semibold" : "text-muted-foreground hover:bg-muted/50",
      )}>
      {children}
    </button>
  )
}

// ─── terminal output ──────────────────────────────────────────────────────────

function TerminalOutput({ test }: { test: DiagTest }) {
  const ref = useRef<HTMLDivElement>(null)
  const visible = test.lines.slice(0, test.totalLines)

  useEffect(() => {
    if (ref.current) ref.current.scrollTop = ref.current.scrollHeight
  }, [visible.length])

  return (
    <div ref={ref}
      className="h-72 overflow-y-auto rounded-lg bg-zinc-950 dark:bg-zinc-900 border border-zinc-800 px-4 py-3 font-mono text-xs leading-relaxed">
      {/* command line */}
      <div className="mb-2 flex items-center gap-2">
        <span className="text-zinc-500">[{test.srcServerName}]</span>
        <span className="text-emerald-400">$</span>
        <span className="text-zinc-300">{test.command}</span>
        {test.status === "running" && (
          <span className="inline-block size-1.5 rounded-full bg-emerald-400 animate-pulse ml-1" />
        )}
      </div>
      {/* output */}
      {visible.map((line, i) => (
        <div key={i} className={cn(
          "whitespace-pre leading-5",
          line.kind === "ok"     && "text-emerald-400",
          line.kind === "err"    && "text-red-400",
          line.kind === "dim"    && "text-zinc-500",
          line.kind === "header" && "text-zinc-400 font-semibold",
          line.kind === "cmd"    && "text-amber-400",
          line.kind === "normal" && "text-zinc-300",
        )}>
          {line.text || " "}
        </div>
      ))}
      {test.status === "running" && visible.length < test.lines.length && (
        <div className="text-zinc-600 animate-pulse">…</div>
      )}
    </div>
  )
}

// ─── uptime speed probes (live) ───────────────────────────────────────────────

function ScheduleSpeedProbesLive({
  apiFetch,
  serversForName,
}: {
  apiFetch: ReturnType<typeof makeApiFetch>
  serversForName: Server[]
}) {
  const [rows, setRows] = useState<SpeedProbeApiRow[]>([])
  const [loading, setLoading] = useState(true)
  const [err, setErr]       = useState<string | null>(null)

  useEffect(() => {
    let cancelled = false
    setLoading(true)
    setErr(null)
    void apiFetch<{ probes: SpeedProbeApiRow[] }>("/api/uptime/speed-probes")
      .then(d => {
        if (!cancelled) setRows(d.probes ?? [])
      })
      .catch(e => {
        if (!cancelled) setErr(e instanceof Error ? e.message : String(e))
      })
      .finally(() => {
        if (!cancelled) setLoading(false)
      })
    return () => { cancelled = true }
  }, [apiFetch])

  const name = (id: string) => serversForName.find(s => s.id === id)?.name ?? id

  if (loading) {
    return (
      <div className="flex items-center justify-center gap-2 py-12 text-sm text-muted-foreground">
        <LoaderCircleIcon className="size-5 animate-spin" />
        Загрузка расписания из БД…
      </div>
    )
  }
  if (err) {
    return (
      <div className="flex items-center gap-2 rounded-lg border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive">
        <AlertCircleIcon className="size-4 shrink-0" />
        {err}
      </div>
    )
  }

  return (
    <div className="flex flex-col gap-3">
      <DataPageCard>
        <ProbesSpeedProbesDataGrid rows={rows} serverName={name} />
      </DataPageCard>
      <p className="text-[11px] text-muted-foreground">
        Данные из коллектора uptime (та же БД, что и дашборд). Редактирование — через настройки мониторинга / API.
      </p>
    </div>
  )
}

// ─── schedule tab ─────────────────────────────────────────────────────────────

const INIT_RULES: SchedRule[] = [
  { id: "r1", srcId: "srv1", tunnelId: "gre1", type: "both",      intervalMin: 15, enabled: true,  lastRun: "3 мин назад", nextRunMin: 12 },
  { id: "r2", srcId: "srv1", tunnelId: "gre2", type: "ping",      intervalMin:  5, enabled: true,  lastRun: "1 мин назад", nextRunMin:  4 },
  { id: "r3", srcId: "srv7", tunnelId: "gre5", type: "bandwidth", intervalMin: 60, enabled: false, lastRun: "2ч назад",    nextRunMin: null },
]

function ScheduleTab({
  rules,
  setRules,
  serverOptions,
  tunnelsForServer,
  defaultSrc,
}: {
  rules: SchedRule[]
  setRules: React.Dispatch<React.SetStateAction<SchedRule[]>>
  serverOptions: Server[]
  tunnelsForServer: (serverId: string) => GreTunnel[]
  defaultSrc?: string
}) {
  const [showAdd, setShowAdd] = useState(false)
  const [addSrc,  setAddSrc]  = useState(defaultSrc ?? serverOptions[0]?.id ?? "srv1")
  const [addTun,  setAddTun]  = useState("")
  const [addType, setAddType] = useState<SchedType>("ping")
  const [addMin,  setAddMin]  = useState(10)
  const addTunnels = useMemo(() => tunnelsForServer(addSrc), [addSrc, tunnelsForServer])

  useEffect(() => {
    if (defaultSrc) setAddSrc(defaultSrc)
  }, [defaultSrc])

  useEffect(() => {
    const list = tunnelsForServer(addSrc)
    if (list.length && !list.some(t => t.id === addTun)) {
      setAddTun(list[0]?.id ?? "")
    }
  }, [addSrc, addTun, tunnelsForServer])

  const tunnelName = (srcId: string, tunnelId: string) =>
    tunnelsForServer(srcId).find((t) => t.id === tunnelId)?.name

  return (
    <div className="flex flex-col gap-3">
      <DataPageCard>
        <ProbesScheduleDataGrid
          rules={rules}
          serverOptions={serverOptions}
          tunnelName={tunnelName}
          onToggleEnabled={(id, enabled) =>
            setRules((p) => p.map((r) => (r.id === id ? { ...r, enabled } : r)))
          }
          onDelete={(id) => setRules((p) => p.filter((r) => r.id !== id))}
        />
      </DataPageCard>
      {showAdd ? (
        <Frame dense className="w-full overflow-hidden">
          <FramePanel className="p-0 overflow-hidden">
          <div className="px-4 py-3 border-b flex items-center gap-2 text-sm font-medium">
            <PlusIcon className="size-4 text-muted-foreground" />Новое правило
          </div>
          <div className="px-4 py-4 flex flex-wrap items-end gap-3">
            <div><OptionLabel>Сервер</OptionLabel>
              <NativeSelect value={addSrc} onChange={setAddSrc} className="min-w-[160px]">
                {serverOptions.filter(s => s.enabled).map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
              </NativeSelect>
            </div>
            <div><OptionLabel>GRE-туннель</OptionLabel>
              <NativeSelect value={addTun} onChange={setAddTun} className="min-w-[150px]">
                {addTunnels.map(t => <option key={t.id} value={t.id}>{t.name}</option>)}
              </NativeSelect>
            </div>
            <div><OptionLabel>Тип</OptionLabel>
              <NativeSelect value={addType} onChange={v => setAddType(v as SchedType)}>
                <option value="ping">Ping</option>
                <option value="bandwidth">BW-тест</option>
                <option value="both">Ping + BW</option>
              </NativeSelect>
            </div>
            <div><OptionLabel>Интервал (мин)</OptionLabel>
              <Input type="number" min={1} max={1440} value={addMin}
                onChange={e => setAddMin(Math.max(1, parseInt(e.target.value) || 1))}
                className="w-20 font-mono text-sm" />
            </div>
            <Button size="sm" disabled={!addTunnels.length}
              onClick={() => {
                setRules(p => [...p, { id: `r${Date.now()}`, srcId: addSrc, tunnelId: addTun, type: addType, intervalMin: addMin, enabled: true, lastRun: null, nextRunMin: addMin }])
                setShowAdd(false)
              }}>
              <CheckIcon className="size-4" />Добавить
            </Button>
            <Button variant="outline" size="sm" onClick={() => setShowAdd(false)}>Отмена</Button>
          </div>
          </FramePanel>
        </Frame>
      ) : (
        <Button variant="outline" size="sm" className="w-fit" onClick={() => setShowAdd(true)}>
          <PlusIcon className="size-4" />Добавить правило
        </Button>
      )}
    </div>
  )
}

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

export default function ProbesPage() {
  const { mode, backendUrl } = useDataSource()
  const isLive = mode === "live"
  const apiFetch = useMemo(() => makeApiFetch(backendUrl), [backendUrl])

  const [liveServers, setLiveServers] = useState<Server[]>([])
  const [liveLoad, setLiveLoad]       = useState<"idle" | "loading" | "error">(() => (isLive ? "loading" : "idle"))
  const [greByServer, setGreByServer] = useState<Record<string, GreTunnel[]>>({})

  const [tool,       setTool]       = useState<DiagTool>("ping")
  const [srcId,      setSrcId]      = useState(servers[0]?.id ?? "srv1")
  const [target,     setTarget]     = useState("8.8.8.8")
  const [pingCount,  setPingCount]  = useState(5)
  const [pingSize,   setPingSize]   = useState(64)
  const [pingTtl]                    = useState(64)
  const [traceProto, setTraceProto]  = useState<TraceProto>("icmp")
  const [traceUseDns, setTraceUseDns] = useState(false)
  const [traceHops]                  = useState(30)
  const [dnsType,    setDnsType]     = useState<DnsType>("A")
  const [bwTunId,    setBwTunId]     = useState("")
  const [bwProto,    setBwProto]     = useState<"tcp" | "udp">("tcp")
  const [bwDuration, setBwDuration]  = useState(10)
  const [runBusy, setRunBusy]        = useState(false)
  /** В live: IPv4 для src-address (DNS A-запись к host API); undefined = грузим, null = не удалось */
  const [rosSrcV4, setRosSrcV4] = useState<string | null | undefined>(undefined)

  useEffect(() => {
    if (!isLive) {
      queueMicrotask(() => setLiveLoad("idle"))
      return
    }
    setLiveLoad("loading")
    void apiFetch<BackendServerRow[]>("/api/servers")
      .then(rows => {
        const mapped: Server[] = rows.map(s => ({
          id: String(s.id),
          name: s.name || s.host,
          host: s.host,
          model: "—",
          os: "—",
          site: s.site,
          country: s.country || "UN",
          asn: s.asn,
          type: s.type,
          enabled: s.enabled,
          status: (s.status ?? "offline") as Server["status"],
          latency: s.latency != null ? Math.round(s.latency) : null,
          sessions: 0,
        }))
        setLiveServers(mapped)
        setLiveLoad("idle")
      })
      .catch(() => {
        setLiveServers([])
        setLiveLoad("error")
      })
  }, [isLive, apiFetch])

  const allServers = useMemo(() => {
    if (!isLive) return servers
    return liveServers
  }, [isLive, liveServers])

  const probeRailItems = useMemo<ServerTileItem[]>(() => (
    allServers.map((s) => ({
      id: s.id,
      name: s.name,
      host: s.host,
      site: s.site,
      country: s.country,
      status: s.status,
      type: s.type,
      enabled: s.enabled,
      selectable: s.enabled,
    }))
  ), [allServers])

  useEffect(() => {
    if (!isLive) {
      setRosSrcV4(undefined)
      return
    }
    const n = Number.parseInt(srcId, 10)
    if (!Number.isFinite(n)) {
      setRosSrcV4(undefined)
      return
    }
    let cancelled = false
    setRosSrcV4(undefined)
    void apiFetch<{ ipv4: string | null }>(`/api/servers/${n}/ros-src-address`)
      .then((r) => {
        if (!cancelled) setRosSrcV4(r.ipv4)
      })
      .catch(() => {
        if (!cancelled) setRosSrcV4(null)
      })
    return () => { cancelled = true }
  }, [isLive, srcId, apiFetch])

  const ensureGre = useCallback(async (serverId: string) => {
    if (!isLive) return
    try {
      const d = await apiFetch<{ tunnels: GreTunnel[] }>(
        `/api/filters/gre-tunnels?serverId=${encodeURIComponent(serverId)}`,
      )
      setGreByServer(prev => ({ ...prev, [serverId]: d.tunnels }))
    } catch {
      setGreByServer(prev => ({ ...prev, [serverId]: [] }))
    }
  }, [isLive, apiFetch])

  useEffect(() => {
    if (!isLive || !srcId) return
    void ensureGre(srcId)
  }, [isLive, srcId, ensureGre])

  const tunnelsForSrc = useMemo(() => {
    if (isLive) return greByServer[srcId] ?? []
    return greTunnels.filter(t => t.serverId === srcId)
  }, [isLive, greByServer, srcId])

  useEffect(() => {
    const first = allServers[0]?.id
    if (!first) return
    if (!allServers.some(s => s.id === srcId)) setSrcId(first)
  }, [allServers, srcId])

  useEffect(() => {
    const list = isLive ? (greByServer[srcId] ?? []) : greTunnels.filter(t => t.serverId === srcId)
    if (list.length && !list.some(t => t.id === bwTunId)) setBwTunId(list[0]!.id)
  }, [isLive, greByServer, srcId, bwTunId])

  // ── run state ──
  const [tests,   setTests]   = useState<DiagTest[]>([])
  const [tab,     setTab]     = useState<"history" | "schedule">("history")
  const [rules,   setRules]   = useState<SchedRule[]>(INIT_RULES)
  const nextId                = useRef(1)
  const tickRef               = useRef<ReturnType<typeof setInterval> | null>(null)
  /** Живой POST /probes/run — отмена через fetch AbortSignal */
  const liveProbeRunRef       = useRef<{ testId: string; ctrl: AbortController } | null>(null)

  const bwTunnels = tunnelsForSrc
  const srcServer = useMemo(() => allServers.find(s => s.id === srcId), [allServers, srcId])
  const bwTun     = useMemo(() => bwTunnels.find(t => t.id === bwTunId), [bwTunnels, bwTunId])

  // current command preview
  const cmdPreview = useMemo(() => buildCommand(allServers, tool, srcId, target, {
    pingCount, pingSize, pingTtl, traceProto, traceMaxHops: traceHops, traceUseDns, dnsType,
    bwTarget: bwTun?.remoteAddress, bwProto, bwDuration,
  }, isLive ? rosSrcV4 : undefined), [allServers, tool, srcId, target, pingCount, pingSize, pingTtl, traceProto, traceHops, traceUseDns, dnsType, bwTun, bwProto, bwDuration, isLive, rosSrcV4])

  // ── progressive reveal tick ──
  useEffect(() => {
    tickRef.current = setInterval(() => {
      setTests(prev => {
        const hasRunning = prev.some(t => t.status === "running")
        if (!hasRunning) return prev
        return prev.map(t => {
          if (t.source === "live") return t
          if (t.status !== "running") return t
          const speed = t.tool === "dns" || t.tool === "route" ? t.lines.length : 1
          const nextTotal = Math.min(t.totalLines + speed, t.lines.length)
          const done = nextTotal >= t.lines.length
          return { ...t, totalLines: nextTotal, status: done ? "done" : "running" }
        })
      })
    }, 400)
    return () => { if (tickRef.current) clearInterval(tickRef.current) }
  }, [])

  // ── run test ──
  const runTest = useCallback(async () => {
    const srv = allServers.find(s => s.id === srcId)
    if (!srv) return
    const id = String(nextId.current++)
    const numericServerId = Number.parseInt(srcId, 10)

    if (isLive) {
      if (!Number.isFinite(numericServerId)) return
      setRunBusy(true)
      const abortCtrl = new AbortController()
      liveProbeRunRef.current = { testId: id, ctrl: abortCtrl }
      setTests(p => [{
        id,
        tool,
        status: "running",
        srcServerId: srcId,
        srcServerName: srv.name,
        target: tool === "bandwidth" ? (bwTun?.name ?? target) : target,
        command: cmdPreview,
        startedAt: Date.now(),
        lines: [{ text: "Выполняется запрос к MikroTik через API…", kind: "dim" }],
        totalLines: 1,
        source: "live",
      }, ...p.slice(0, 9)])
      setTab("history")
      try {
        const peer =
          bwTun?.remoteAddress
            ? allServers.find(s => s.host.trim().toLowerCase() === bwTun.remoteAddress.trim().toLowerCase())
            : undefined
        const body = {
          tool,
          target: tool === "bandwidth" ? undefined : target.trim(),
          pingCount,
          pingSize,
          pingTtl,
          traceProto,
          traceMaxHops: traceHops,
          traceHopTimeout: tool === "traceroute" ? "1s" : undefined,
          traceProbeCount: tool === "traceroute" ? 1 : undefined,
          traceUseDns: tool === "traceroute" ? traceUseDns : undefined,
          dnsType,
          bwRemoteAddress: tool === "bandwidth" ? bwTun?.remoteAddress : undefined,
          dstServerId: peer ? Number.parseInt(peer.id, 10) : undefined,
          bwProto,
          bwDuration,
        }
        const res = await apiFetch<{ output: string }>(`/api/servers/${numericServerId}/probes/run`, {
          method: "POST",
          body: JSON.stringify(body),
          signal: abortCtrl.signal,
        })
        const lines = parseProbeOutput(res.output ?? "")
        const looksErr = (res.output ?? "").trim().toLowerCase().startsWith("error:")
        setTests(p => p.map(t => (t.id === id
          ? {
              ...t,
              lines,
              totalLines: lines.length,
              status: looksErr ? "error" : "done",
            }
          : t)))
      } catch (e) {
        const aborted =
          (typeof DOMException !== "undefined" && e instanceof DOMException && e.name === "AbortError")
          || (e instanceof Error && e.name === "AbortError")
        if (aborted) {
          setTests(p => p.map(t => (t.id === id
            ? {
                ...t,
                lines: [{ text: "Запрос отменён (стоп или закрытие запроса).", kind: "dim" }],
                totalLines: 1,
                status: "done",
              }
            : t)))
        } else {
          const msg = e instanceof Error ? e.message : String(e)
          setTests(p => p.map(t => (t.id === id
            ? {
                ...t,
                lines: [{ text: msg, kind: "err" }],
                totalLines: 1,
                status: "error",
              }
            : t)))
        }
      } finally {
        if (liveProbeRunRef.current?.testId === id) liveProbeRunRef.current = null
        setRunBusy(false)
      }
      return
    }

    let lines: OutputLine[] = []

    switch (tool) {
      case "ping":       lines = genPingOutput(target, srcId, pingCount, pingSize, pingTtl); break
      case "traceroute": lines = genTraceOutput(target, srcId, traceProto, traceHops); break
      case "dns":        lines = genDnsOutput(target, srcId, dnsType); break
      case "route":      lines = genRouteOutput(target, srcId); break
      case "mtu":        lines = genMtuOutput(target, srcId); break
      case "bandwidth":  lines = genBwOutput(bwTun?.remoteAddress ?? target, srcId, bwProto, bwDuration); break
    }

    const test: DiagTest = {
      id,
      tool,
      status: "done",
      srcServerId: srcId,
      srcServerName: srv.name,
      target: tool === "bandwidth" ? (bwTun?.name ?? target) : target,
      command: cmdPreview,
      startedAt: Date.now(),
      lines,
      totalLines: lines.length,
      source: "demo",
    }
    setTests(p => [test, ...p.slice(0, 9)])
    setTab("history")
  }, [
    isLive,
    allServers,
    srcId,
    tool,
    target,
    pingCount,
    pingSize,
    pingTtl,
    traceProto,
    traceUseDns,
    traceHops,
    dnsType,
    bwTun,
    bwTunId,
    bwProto,
    bwDuration,
    cmdPreview,
    apiFetch,
  ])

  const stopTest = (id: string) => {
    if (liveProbeRunRef.current?.testId === id) liveProbeRunRef.current.ctrl.abort()
    setTests(p => p.map(t => (t.id === id && t.status === "running"
      ? { ...t, status: "done" as const, totalLines: t.lines.length }
      : t)))
  }
  const clearTest = (id: string) => setTests(p => p.filter(t => t.id !== id))

  const running = tests.filter(t => t.status === "running")

  if (isLive && liveLoad === "loading") {
    return (
      <div className="flex flex-col h-full">
        <PageHeader crumbs={[{ label: "Инструменты" }, { label: "Диагностика" }]} />
        <div className="flex-1 flex flex-col items-center justify-center gap-3 text-muted-foreground">
          <LoaderCircleIcon className="size-8 animate-spin opacity-50" />
          <p className="text-sm">Загрузка серверов из бекенда…</p>
        </div>
      </div>
    )
  }

  return (
    <ServerRailLayout
      items={probeRailItems}
      selectedId={srcId}
      onSelect={setSrcId}
      showAll={false}
      loading={isLive && liveLoad === "loading" && allServers.length === 0}
      header={
        <PageHeader
          crumbs={[{ label: "Инструменты" }, { label: "Диагностика" }]}
          actions={
            <>
              <ServerRailMobileButton />
              {running.length > 0
                ? <Button variant="outline" size="sm" onClick={() => {
                    liveProbeRunRef.current?.ctrl.abort()
                    setTests(p => p.map(t => (t.status === "running"
                      ? { ...t, status: "done" as const, totalLines: t.lines.length }
                      : t)))
                  }}>
                    <SquareIcon className="size-4" />Остановить все
                  </Button>
                : null}
            </>
          }
        />
      }
    >
        <div className="flex flex-col gap-4">

          {isLive && liveLoad === "error" && (
            <div className="rounded-lg border border-destructive/30 bg-destructive/10 px-4 py-2.5 text-xs text-destructive flex items-center gap-2">
              <AlertCircleIcon className="size-3.5 shrink-0" />
              Бекенд недоступен — переключитесь в режим «Демо» в настройках источника данных или проверьте URL API.
            </div>
          )}

          {isLive && (
            <p className="text-xs text-muted-foreground">
              Режим «Живые»: ping/traceroute/route/mtu/bandwidth выполняются на выбранном MikroTik; для источника поле src-address — только IPv4 (FQDN резолвится на бекенде). Traceroute через REST: у MikroTik лимит сессии ~60 с (параметры команды это не продлевают); у нас timeout в формате HH:MM:SS, count=1, max-hops при необходимости уменьшается автоматически. «Стоп» прерывает HTTP к бекенду. DNS — резолвер приложения, не MikroTik.
            </p>
          )}

          {/* ── tool selector + config ── */}
          <OpsPanel contentClassName="pt-4 pb-4 px-4 flex flex-col gap-4">

              {/* tool chips */}
              <div className="flex gap-2 flex-wrap">
                {(Object.entries(TOOL_META) as [DiagTool, typeof TOOL_META[DiagTool]][]).map(([t, m]) => {
                  const active = tool === t
                  return (
                    <button key={t} onClick={() => setTool(t)}
                      className={cn(
                        "flex items-center gap-1.5 px-3 py-1.5 rounded-full border text-xs font-medium transition-colors",
                        active
                          ? "border-foreground bg-foreground text-background"
                          : "border-border text-muted-foreground hover:text-foreground hover:border-foreground/40",
                      )}>
                      <m.Icon className={cn("size-3.5", active ? "" : m.color)} />
                      {m.label}
                    </button>
                  )
                })}
              </div>

              {/* main config row */}
              <div className="flex flex-wrap items-end gap-3">

                {/* target — all tools except bandwidth */}
                {tool !== "bandwidth" && (
                  <div className="flex-1 min-w-[140px]">
                    <OptionLabel>
                      {tool === "dns" ? "Домен" : tool === "route" ? "Destination IP" : "Цель (IP или домен)"}
                    </OptionLabel>
                    <Input value={target} onChange={e => setTarget(e.target.value)}
                      placeholder={tool === "dns" ? "google.com" : tool === "route" ? "8.8.8.8" : "8.8.8.8"}
                      className="font-mono text-sm"
                      onKeyDown={e => e.key === "Enter" && runTest()}
                    />
                  </div>
                )}

                {/* bandwidth: select GRE tunnel */}
                {tool === "bandwidth" && (
                  <div>
                    <OptionLabel>GRE-туннель (цель)</OptionLabel>
                    <NativeSelect value={bwTunId} onChange={setBwTunId} className="min-w-[180px]">
                      {bwTunnels.map(t => <option key={t.id} value={t.id}>{t.name}</option>)}
                    </NativeSelect>
                  </div>
                )}

                {/* inline quick options */}
                {tool === "ping" && (
                  <>
                    <div>
                      <OptionLabel>Кол-во</OptionLabel>
                      <div className="flex rounded-lg border border-input overflow-hidden h-8">
                        {[5, 10, 25, 100].map(n => <SegBtn key={n} value={n} current={pingCount} onClick={setPingCount}>{n}</SegBtn>)}
                      </div>
                    </div>
                    <div>
                      <OptionLabel>Размер (байт)</OptionLabel>
                      <div className="flex rounded-lg border border-input overflow-hidden h-8">
                        {[64, 128, 512, 1472].map(n => <SegBtn key={n} value={n} current={pingSize} onClick={setPingSize}>{n}</SegBtn>)}
                      </div>
                    </div>
                  </>
                )}

                {tool === "traceroute" && (
                  <>
                    <div>
                      <OptionLabel>Протокол</OptionLabel>
                      <div className="flex rounded-lg border border-input overflow-hidden h-8">
                        {(["icmp", "udp", "tcp"] as TraceProto[]).map(p => <SegBtn key={p} value={p} current={traceProto} onClick={setTraceProto}>{p.toUpperCase()}</SegBtn>)}
                      </div>
                    </div>
                    <div className="flex items-end gap-3 pb-0.5">
                      <div className="flex flex-col gap-1 min-w-0">
                        <OptionLabel>Имена хопов (use-dns)</OptionLabel>
                        <p className="text-[10px] text-muted-foreground leading-snug max-w-[220px]">
                          Как в RouterOS: резолвить IP промежуточных узлов в DNS-имена на самом MikroTik.
                        </p>
                      </div>
                      <FormToggle checked={traceUseDns} onChange={setTraceUseDns} />
                    </div>
                  </>
                )}

                {tool === "dns" && (
                  <div>
                    <OptionLabel>Тип записи</OptionLabel>
                    <div className="flex rounded-lg border border-input overflow-hidden h-8">
                      {(["A", "AAAA", "MX", "NS", "TXT", "PTR"] as DnsType[]).map(t => <SegBtn key={t} value={t} current={dnsType} onClick={setDnsType}>{t}</SegBtn>)}
                    </div>
                  </div>
                )}

                {tool === "bandwidth" && (
                  <>
                    <div>
                      <OptionLabel>Протокол</OptionLabel>
                      <div className="flex rounded-lg border border-input overflow-hidden h-8">
                        {(["tcp", "udp"] as const).map(p => <SegBtn key={p} value={p} current={bwProto} onClick={setBwProto}>{p.toUpperCase()}</SegBtn>)}
                      </div>
                    </div>
                    <div>
                      <OptionLabel>Длительность</OptionLabel>
                      <div className="flex rounded-lg border border-input overflow-hidden h-8">
                        {[5, 10, 30].map(n => <SegBtn key={n} value={n} current={bwDuration} onClick={setBwDuration}>{n}с</SegBtn>)}
                      </div>
                    </div>
                  </>
                )}

                <Button
                  onClick={() => void runTest()}
                  disabled={runBusy || !srcServer || (tool === "bandwidth" && !bwTunnels.length)}
                  className="h-8 shrink-0 gap-1.5 self-end"
                >
                  {runBusy ? <LoaderCircleIcon className="size-3.5 animate-spin" /> : <PlayIcon className="size-3.5" />}
                  Запустить
                </Button>
              </div>

              {/* RouterOS command preview */}
              <div className="flex items-center gap-2 rounded-lg bg-zinc-950 dark:bg-zinc-900 px-3 py-2 border border-zinc-800">
                <TerminalIcon className="size-3 text-zinc-500 shrink-0" />
                <span className="text-zinc-500 text-xs font-mono shrink-0">[{srcServer?.name}]</span>
                <span className="text-emerald-400 text-xs font-mono shrink-0">$</span>
                <span className="text-zinc-300 text-xs font-mono truncate">{cmdPreview}</span>
                <button onClick={() => navigator.clipboard.writeText(cmdPreview)}
                  className="shrink-0 text-zinc-600 hover:text-zinc-300 transition-colors ml-auto">
                  <CopyIcon className="size-3" />
                </button>
              </div>
          </OpsPanel>

          {/* ── tabs ── */}
          <div>
            <div className="flex items-center gap-0 border-b">
              {([
                ["history",  "История тестов"],
                ["schedule", "Расписание"],
              ] as const).map(([t, label]) => (
                <button key={t} onClick={() => setTab(t)}
                  className={cn(
                    "px-4 py-2 text-sm font-medium -mb-px border-b-2 transition-colors",
                    tab === t
                      ? "border-foreground text-foreground"
                      : "border-transparent text-muted-foreground hover:text-foreground",
                  )}>
                  {label}
                  {t === "history" && tests.length > 0 && (
                    <span className="ml-1.5 text-[10px] font-mono px-1.5 py-0.5 rounded-full bg-muted text-muted-foreground">
                      {tests.length}
                    </span>
                  )}
                </button>
              ))}
            </div>

            <div className="mt-4">
              {/* history */}
              {tab === "history" && (
                tests.length === 0 ? (
                  <div className="flex flex-col items-center justify-center py-16 text-muted-foreground gap-2">
                    <TerminalIcon className="size-8 opacity-20" />
                    <p className="text-sm">Запустите тест — результаты появятся здесь</p>
                  </div>
                ) : (
                  <div className="flex flex-col gap-4">
                    {tests.map(test => {
                      const { Icon, color, label } = TOOL_META[test.tool]
                      return (
                        <Frame key={test.id} dense className="w-full overflow-hidden">
                          <FramePanel className="p-0 overflow-hidden">
                          {/* header */}
                          <div className="flex items-center justify-between gap-3 px-4 py-2.5 border-b bg-muted/20">
                            <div className="flex items-center gap-2 min-w-0">
                              <Icon className={cn("size-4 shrink-0", color)} />
                              <span className="text-sm font-medium">{label}</span>
                              <span className="text-xs font-mono text-muted-foreground truncate">{test.target}</span>
                              <span className="text-[10px] text-muted-foreground">← {test.srcServerName}</span>
                            </div>
                            <div className="flex items-center gap-2 shrink-0">
                              {test.status === "running" && (
                                <>
                                  <span className="flex items-center gap-1 text-[11px] text-muted-foreground">
                                    <span className="size-1.5 rounded-full bg-emerald-400 animate-pulse" />
                                    запущен
                                  </span>
                                  <Button size="sm" variant="outline" className="h-6 px-2 text-xs"
                                    onClick={() => stopTest(test.id)}>
                                    <SquareIcon className="size-3" />Стоп
                                  </Button>
                                </>
                              )}
                              {test.status === "done" && test.source !== "live" && (
                                <span className="text-[11px] text-muted-foreground">
                                  {((test.lines.length * 0.4)).toFixed(1)}с
                                </span>
                              )}
                              {test.status === "done" && test.source === "live" && (
                                <span className="text-[10px] font-mono text-emerald-600 dark:text-emerald-400">live</span>
                              )}
                              {test.status === "error" && (
                                <span className="text-[11px] text-destructive">ошибка</span>
                              )}
                              <button onClick={() => navigator.clipboard.writeText(test.lines.map(l => l.text).join("\n"))}
                                className="size-6 flex items-center justify-center rounded text-muted-foreground/40 hover:text-foreground hover:bg-muted transition-colors">
                                <CopyIcon className="size-3.5" />
                              </button>
                              <button onClick={() => clearTest(test.id)}
                                className="size-6 flex items-center justify-center rounded text-muted-foreground/40 hover:text-red-500 hover:bg-red-500/10 transition-colors">
                                <Trash2Icon className="size-3.5" />
                              </button>
                            </div>
                          </div>
                          {/* terminal output */}
                          <div className="p-3">
                            <TerminalOutput test={test} />
                          </div>
                          </FramePanel>
                        </Frame>
                      )
                    })}
                  </div>
                )
              )}

              {/* schedule */}
              {tab === "schedule" && (
                isLive
                  ? <ScheduleSpeedProbesLive apiFetch={apiFetch} serversForName={allServers} />
                  : (
                    <ScheduleTab
                      rules={rules}
                      setRules={setRules}
                      serverOptions={allServers}
                      tunnelsForServer={sid => greTunnels.filter(t => t.serverId === sid)}
                      defaultSrc={srcId}
                    />
                  )
              )}
            </div>
          </div>
        </div>
    </ServerRailLayout>
  )
}
