"use client"

import { useMemo, useState, useCallback, useEffect } from "react"
import { PageHeader } from "@/components/page-header"
import { EmptyState } from "@/components/empty-state"
import { StatusDot } from "@/components/status-dot"
import { Flag } from "@/components/flag"
import {
  FiltersDataGrid,
  type RecursiveRouteLite,
} from "@/components/data-grids/filters-data-grid"
import { DataPageCard } from "@/components/data-page-card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Separator } from "@/components/ui/separator"
import { cn } from "@/lib/utils"
import { requestJson } from "@/shared/api/http-client"
import {
  servers, greTunnels, serverFilterRulesets, filters as mockFiltersCatalog,
  type FilterRule, type ServerFilterRuleset, type Server, type GreTunnel,
} from "@/lib/data"
import { useDataSource } from "@/lib/data-source"
import { useEvoBGP } from "@/lib/evobgp-context"
import {
  PlusIcon, SearchIcon, XIcon, CheckIcon, AlertCircleIcon,
  PencilIcon, TrashIcon, ChevronUpIcon, ChevronDownIcon,
  StarIcon, ArrowUpDownIcon, ArrowUpIcon, ArrowDownIcon,
  FileCodeIcon, CopyIcon, NetworkIcon, TagIcon,
  ArrowRightIcon, AlertTriangleIcon, LoaderCircleIcon, RouteIcon,
  CheckCircle2Icon, XCircleIcon, CircleDashedIcon, RefreshCwIcon,
} from "lucide-react"
import {
  Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetFooter,
} from "@/components/ui/sheet"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
import { toast } from "sonner"
import { CodeExportSheet } from "@/components/reui-kit/code-export-sheet"
import { ServerRailLayout, ServerRailMobileButton } from "@/components/server-rail-layout"
import { type ServerTileItem } from "@/components/server-tile-rail"

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

type FilterRouterSyncStatus = "synced" | "drift" | "missing"

function newId() { return `r${Date.now()}-${Math.random().toString(36).slice(2, 6)}` }
function innerIpToGateway(ip: string) { return ip.split("/")[0] }

/** В BGP routing filter для рекурсивного маршрута — set gateway к хопу из dst-address (напр. 10.9.9.2/32 → 10.9.9.2) */
function gatewayFromRecursiveDst(dstAddress: string): string {
  return innerIpToGateway((dstAddress ?? "").trim())
}

/** Если в правиле нет rec: (например после sync с роутера до обогащения на бэкенде), находим строку по совпадению hop с dst-prefix */
function pickRecursiveRouteByGatewayHop(
  routes: RecursiveRouteLite[],
  hopIp: string,
): RecursiveRouteLite | undefined {
  const hop = hopIp.trim()
  if (!hop) return undefined
  const candidates = routes.filter(
    r => !r.disabled && gatewayFromRecursiveDst(r.dstAddress) === hop,
  )
  if (candidates.length === 0) return undefined
  return candidates.reduce((a, b) => (a.distance <= b.distance ? a : b))
}

/** Same as backend: IP%iface из поля gateway рекурсивного маршрута */
function splitRecursiveGateway(raw: string): { ip: string; iface: string } {
  const v = (raw ?? "").trim()
  if (!v) return { ip: "", iface: "" }
  const pct = v.indexOf("%")
  const left = pct >= 0 ? v.slice(0, pct) : v
  const iface = pct >= 0 ? v.slice(pct + 1).trim() : ""
  const ip = left.split("/")[0]?.trim() ?? ""
  return { ip, iface }
}

function isRecursiveGatewayRef(ref: string): boolean {
  return ref.startsWith("rec:")
}

/** Один ключ на префикс назначения — несколько записей с тем же dst-address (разные via, резерв) = один логический маршрут */
function normalizeRecursiveDstAddress(raw: string): string {
  return (raw ?? "").trim().toLowerCase()
}

/**
 * Рекурсивный маршрут в RouterOS может быть представлен несколькими статическими записями
 * на один и тот же dst-address (разный gateway / резервирование). Для выбора в фильтре нужна
 * одна строка на уникальный dst-address; при совпадении оставляем меньший distance.
 */
function dedupeRecursiveRoutesByDstAddress(routes: RecursiveRouteLite[]): RecursiveRouteLite[] {
  const byDst = new Map<string, RecursiveRouteLite>()
  for (const r of routes) {
    if (r.disabled) continue
    const key = normalizeRecursiveDstAddress(r.dstAddress)
    if (!key) continue
    const prev = byDst.get(key)
    if (!prev) {
      byDst.set(key, r)
      continue
    }
    if (r.distance < prev.distance) byDst.set(key, r)
  }
  return Array.from(byDst.values()).sort((a, b) => {
    if (a.distance !== b.distance) return a.distance - b.distance
    return normalizeRecursiveDstAddress(a.dstAddress).localeCompare(normalizeRecursiveDstAddress(b.dstAddress))
  })
}

const COMMUNITY_NAMES: Record<string, string> = {
  "65001:100": "youtube-bypass",
  "65001:200": "streaming-eu",
  "65001:300": "cdn-bypass",
  "65002:100": "cdn-secondary",
  "65001:400": "office-direct",
  "65001:500": "gaming-ll",
  "65002:200": "gaming-ll-backup",
  "65007:999": "school-block",
  // blackhole communities
  "65535:666": "BLACKHOLE (RFC 7999)",
  "65001:666": "blackhole-local",
  "65002:666": "blackhole-peer",
  "no-export":    "no-export",
  "no-advertise": "no-advertise",
}

// Communities that imply blackhole action by default
const BLACKHOLE_COMMUNITIES = new Set(["65535:666", "65001:666", "65002:666"])
// ── RouterOS config generator (combined, all servers) ─────────────────────────
//
// Единый скрипт для всех MikroTik (RouterOS 7).
// Роутер определяет себя через [/system identity get name].
// Все community → gateway маппинги — ОДИН filter rule на сервер
// с ветвлением if/else if внутри параметра rule="...".

function generateCombinedRouterOSConfig(
  rulesets: ServerFilterRuleset[],
  serversList: Server[],
  tunnelsList: GreTunnel[],
  recRoutesByServer: Record<string, RecursiveRouteLite[]>,
): string {
  const now        = new Date().toLocaleString("ru-RU", { dateStyle: "short", timeStyle: "short" })
  const withRules  = rulesets.filter(r => r.rules.length > 0)
  const totalRules = rulesets.reduce((s, r) => s + r.rules.length, 0)
  const bhCount    = rulesets.reduce((s, r) => s + r.rules.filter(x => x.action === "blackhole").length, 0)

  const lines: string[] = [
    `# MikrotikManager — конфиг фильтрации`,
    `# Дата        : ${now}`,
    `# Серверов    : ${withRules.length}`,
    `# Communities : ${totalRules - bhCount} route + ${bhCount} blackhole`,
    ``,
  ]

  if (withRules.length === 0) {
    lines.push(`# Нет правил фильтрации.`)
  } else {
    withRules.forEach((rs, si) => {
      const server = serversList.find(s => s.id === rs.serverId)
      if (!server) return

      lines.push(`# ${server.name} · ${server.type === "jump-host" ? "JH" : "EN"} · ${server.site} · ${server.host}`)

      const recList = recRoutesByServer[rs.serverId] ?? []
      // Группировка по эффекту: communities с одинаковым `set gw` (+ интерфейс) объединяются
      // в один if-блок через `||` — компактнее и привычнее для bgp-in.
      // RouterOS не поддерживает `else if`, поэтому между группами — независимые `if`-блоки;
      // `accept;` в первом совпавшем блоке завершает обработку, дальнейшие if не выполняются.
      type Group = {
        isBlackhole: boolean
        gateway: string
        outIface: string
        items: Array<{ community: string; desc: string }>
      }
      const groups: Group[] = []
      const indexByKey = new Map<string, number>()

      for (const rule of rs.rules) {
        const isBlackhole = rule.action === "blackhole"
        const label  = rule.communityName ?? rule.community
        const desc   = rule.description ? `${label} — ${rule.description}` : label

        let gw = ""
        let iface = ""
        if (!isBlackhole) {
          if (isRecursiveGatewayRef(rule.gatewayTunnelId)) {
            const rid = rule.gatewayTunnelId.slice(4)
            const rr = recList.find(r => r.id === rid)
            gw = rr ? gatewayFromRecursiveDst(rr.dstAddress) : rule.gateway
          } else {
            const tunnel = tunnelsList.find(t => t.id === rule.gatewayTunnelId)
            gw = rule.gateway
            iface = tunnel ? tunnel.name : "unknown"
          }
        }
        const key = isBlackhole ? "bh" : `rt:${gw}:${iface}`
        let idx = indexByKey.get(key)
        if (idx === undefined) {
          idx = groups.length
          indexByKey.set(key, idx)
          groups.push({ isBlackhole, gateway: gw, outIface: iface, items: [] })
        }
        groups[idx].items.push({ community: rule.community, desc })
      }

      const branches = groups.map(g => {
        const cond = g.items
          .map(it => `bgp-communities includes ${it.community}`)
          .join(" || ")
        const commentLines = g.items.map(it => `      # ${it.community}: ${it.desc}`)

        if (g.isBlackhole) {
          return [
            `    if (${cond}) {`,
            ...commentLines,
            `      set type blackhole;`,
            `      accept;`,
            `    }`,
          ].join("\n")
        }
        const out = [
          `    if (${cond}) {`,
          ...commentLines,
          `      set gw ${g.gateway};`,
        ]
        if (g.outIface) out.push(`      set out-interface ${g.outIface};`)
        out.push(`      accept;`)
        out.push(`    }`)
        return out.join("\n")
      }).join("\n")

      lines.push(`/routing filter rule add \\`)
      lines.push(`  chain=bgp-in \\`)
      lines.push(`  comment="MikrotikManager: ${server.name}" \\`)
      lines.push(`  rule="`)
      lines.push(branches)
      lines.push(`  "`)

      if (si < withRules.length - 1) lines.push(``)
    })
  }

  return lines.join("\n")
}

// ── shared components ──────────────────────────────────────────────────────────

function TypeChip({ type }: { type: "jump-host" | "exit-node" | "home-router" }) {
  return (
    <span className={cn(
      "inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-bold border shrink-0",
      type === "home-router" ? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20" :
      type === "jump-host"
        ? "bg-violet-500/10 text-violet-600 dark:text-violet-400 border-violet-500/20"
        : "bg-sky-500/10 text-sky-600 dark:text-sky-400 border-sky-500/20",
    )}>
      {type === "jump-host" ? "JH" : type === "home-router" ? "HR" : "EN"}
    </span>
  )
}

// ── GRE gateway selector ───────────────────────────────────────────────────────

function GatewayCards({
  serverId, selected, onSelect, tunnelsList, serversList,
}: {
  serverId: string
  selected: string
  onSelect: (gw: string, tunnelId: string) => void
  tunnelsList: GreTunnel[]
  serversList: Server[]
}) {
  const tunnels = tunnelsList.filter(t => t.serverId === serverId)

  if (tunnels.length === 0) {
    return (
      <div className="rounded-md border border-dashed p-4 text-center text-sm text-muted-foreground">
        Нет GRE-туннелей на этом сервере.
        <br />
        <span className="text-xs opacity-70">Добавьте туннели на странице «GRE-туннели»</span>
      </div>
    )
  }

  return (
    <div className="flex flex-col gap-1.5">
      {tunnels.map(t => {
        const gw         = innerIpToGateway(t.remoteInnerIp)
        const isSelected = selected === gw
        const remoteSrv  = serversList.find(s => s.host === t.remoteAddress)
        return (
          <button key={t.id} type="button" onClick={() => onSelect(gw, t.id)}
            disabled={t.status === "down"}
            className={cn(
              "text-left rounded-lg border px-3 py-2.5 transition-all",
              isSelected
                ? "border-primary bg-primary/5 ring-1 ring-primary/30"
                : "border-border hover:border-muted-foreground/40 hover:bg-muted/40",
              t.status === "down" && "opacity-40 cursor-not-allowed",
            )}>
            <div className="flex items-center gap-2">
              <span className={cn(
                "size-1.5 rounded-full shrink-0",
                t.status === "up" ? "bg-[var(--status-online)]" : t.status === "degraded" ? "bg-[var(--status-degraded)]" : "bg-[var(--status-offline)]",
              )} />
              {remoteSrv && <Flag code={remoteSrv.country} className="shrink-0" />}
              <span className="font-mono text-xs font-semibold flex-1 truncate">{t.name}</span>
              {isSelected && <CheckIcon className="size-3.5 text-primary shrink-0" />}
            </div>
            <div className="mt-1.5 flex items-center gap-2 text-[11px] text-muted-foreground font-mono">
              <span className="text-muted-foreground/50">local</span>
              <span>{innerIpToGateway(t.localInnerIp)}</span>
              <span className="text-muted-foreground/30">→</span>
              <span className={isSelected ? "text-primary font-medium" : ""}>{gw}</span>
            </div>
          </button>
        )
      })}
    </div>
  )
}

function RecursiveGatewayCards({
  selectedTunnelId,
  routes,
  onSelect,
}: {
  selectedTunnelId: string
  routes: RecursiveRouteLite[]
  onSelect: (route: RecursiveRouteLite) => void
}) {
  const list = useMemo(() => dedupeRecursiveRoutesByDstAddress(routes), [routes])

  /** Подсветка, если в правиле сохранён rec:id «дубликата», убранного из списка — совпадение по dst-address */
  const selectedDstNorm = useMemo(() => {
    if (!isRecursiveGatewayRef(selectedTunnelId)) return null
    const id = selectedTunnelId.slice(4)
    const raw = routes.find(r => r.id === id)
    return raw ? normalizeRecursiveDstAddress(raw.dstAddress) : null
  }, [selectedTunnelId, routes])

  if (list.length === 0) {
    return (
      <div className="rounded-md border border-dashed p-4 text-center text-sm text-muted-foreground">
        Нет рекурсивных маршрутов для этого сервера.
        <br />
        <span className="text-xs opacity-70">
          Добавьте их в «Рекурсивные маршруты» и синхронизируйте с роутером
        </span>
      </div>
    )
  }

  return (
    <div className="flex flex-col gap-1.5 max-h-64 overflow-y-auto pr-0.5">
      {list.map(r => {
        const ref = `rec:${r.id}`
        const filterGw = gatewayFromRecursiveDst(r.dstAddress)
        const { ip: viaIp } = splitRecursiveGateway(r.gateway)
        const sel =
          selectedTunnelId === ref ||
          (!!selectedDstNorm && normalizeRecursiveDstAddress(r.dstAddress) === selectedDstNorm)
        return (
          <button
            key={r.id}
            type="button"
            onClick={() => onSelect(r)}
            className={cn(
              "text-left rounded-lg border px-3 py-2.5 transition-all",
              sel
                ? "border-primary bg-primary/5 ring-1 ring-primary/30"
                : "border-border hover:border-muted-foreground/40 hover:bg-muted/40",
            )}
          >
            <div className="flex items-center gap-2">
              <RouteIcon className="size-3.5 text-muted-foreground shrink-0" />
              <span className="font-mono text-xs font-semibold flex-1 truncate">{r.dstAddress}</span>
              {sel && <CheckIcon className="size-3.5 text-primary shrink-0" />}
            </div>
            <div className="mt-1.5 space-y-0.5 pl-5 text-[11px] text-muted-foreground font-mono">
              <div>
                <span className="text-muted-foreground/50">set gateway </span>
                <span className={sel ? "text-primary font-medium" : ""}>{filterGw}</span>
              </div>
              <div>
                <span className="text-muted-foreground/50">via </span>
                <span>{viaIp || r.gateway}</span>
              </div>
              {r.routingTable !== "main" && (
                <div className="text-[10px]">table {r.routingTable}</div>
              )}
              {r.comment ? (
                <div className="text-[10px] opacity-80 truncate">{r.comment}</div>
              ) : null}
            </div>
          </button>
        )
      })}
    </div>
  )
}

// ── community autocomplete ─────────────────────────────────────────────────────

function CommunityInput({
  value,
  onChange,
  suggestions,
  nameMap,
}: {
  value: string
  onChange: (v: string) => void
  suggestions: string[]
  nameMap: Record<string, string>
}) {
  const [open, setOpen] = useState(false)
  const filtered = suggestions.filter(c =>
    c.toLowerCase().includes(value.toLowerCase()) ||
    (nameMap[c] ?? "").toLowerCase().includes(value.toLowerCase())
  )
  return (
    <div className="relative">
      <Input value={value} onChange={e => { onChange(e.target.value); setOpen(true) }}
        onFocus={() => setOpen(true)} onBlur={() => setTimeout(() => setOpen(false), 150)}
        placeholder="65001:100" className="font-mono h-9" />
      {open && filtered.length > 0 && (
        <div className="absolute z-50 w-full mt-1 rounded-md border bg-popover shadow-md overflow-hidden max-h-56 overflow-y-auto">
          {filtered.map(c => (
            <button key={c} type="button" onMouseDown={() => { onChange(c); setOpen(false) }}
              className="w-full flex items-center gap-2 px-3 py-2 hover:bg-accent text-left">
              <span className="font-mono text-xs font-medium">{c}</span>
              <span className="text-xs text-muted-foreground truncate">{nameMap[c] ?? ""}</span>
            </button>
          ))}
        </div>
      )}
    </div>
  )
}

// ── rule sheet ─────────────────────────────────────────────────────────────────

interface RuleForm {
  action: "route" | "blackhole"
  community: string
  gateway: string
  gatewayTunnelId: string
  /** Только UI: GRE-туннель или рекурсивный маршрут (хранится как rec:id в gatewayTunnelId) */
  gatewayKind: "gre" | "recursive"
  description: string
}
const emptyForm = (): RuleForm => ({
  action: "route",
  community: "",
  gateway: "",
  gatewayTunnelId: "",
  gatewayKind: "gre",
  description: "",
})

function RuleSheet({
  open, mode, initial, serverId, onSave, onClose, serversList, tunnelsList,
  communityNameMap,
  communitySuggestions,
  evoHint,
  recursiveRoutes,
}: {
  open: boolean; mode: "create" | "edit"
  initial: RuleForm; serverId: string
  onSave: (f: RuleForm) => void; onClose: () => void
  serversList: Server[]
  tunnelsList: GreTunnel[]
  communityNameMap: Record<string, string>
  communitySuggestions: string[]
  evoHint?: string | null
  recursiveRoutes: RecursiveRouteLite[]
}) {
  const [form,    setForm]    = useState<RuleForm>(initial)
  const [error,   setError]   = useState<string | null>(null)
  const [touched, setTouched] = useState(false)

  const set = <K extends keyof RuleForm>(k: K, v: RuleForm[K]) => {
    setForm(f => ({ ...f, [k]: v }))
    if (touched) validate_({ ...form, [k]: v })
  }

  // When community changes, auto-detect blackhole communities
  const handleCommunityChange = (v: string) => {
    const next: RuleForm = {
      ...form,
      community: v,
      action: BLACKHOLE_COMMUNITIES.has(v) ? "blackhole" : form.action,
    }
    setForm(next)
    if (touched) validate_(next)
  }

  const validate_ = (f: RuleForm) => {
    if (!f.community.trim()) {
      setError("BGP Community обязательно"); return false
    }
    if (f.action === "route" && !f.gatewayTunnelId.trim()) {
      setError(f.gatewayKind === "recursive" ? "Выберите рекурсивный маршрут" : "Выберите GRE-шлюз")
      return false
    }
    setError(null); return true
  }
  const handleSave = () => {
    setTouched(true)
    if (!validate_(form)) return
    onSave(form.action === "blackhole"
      ? { ...form, gateway: "", gatewayTunnelId: "", gatewayKind: "gre" }
      : form
    )
  }

  const isBlackhole = form.action === "blackhole"

  return (
    <Sheet open={open} onOpenChange={v => { if (!v) onClose() }}>
      <SheetContent side="right" className="sm:max-w-[440px] p-0 flex flex-col" showCloseButton={false}>

        <SheetHeader className="px-5 pt-5 pb-4 border-b shrink-0">
          <div className="flex items-start justify-between gap-2">
            <div>
              <SheetTitle className="text-base">
                {mode === "create" ? "Новое правило" : "Редактировать правило"}
              </SheetTitle>
              <SheetDescription className="text-xs mt-0.5">
                Community →{" "}
                {isBlackhole
                  ? "Blackhole"
                  : form.gatewayKind === "recursive"
                    ? "рекурсивный маршрут"
                    : "GRE-шлюз"}{" "}
                · {serversList.find(s => s.id === serverId)?.name}
              </SheetDescription>
            </div>
            <Button variant="ghost" size="icon-sm" onClick={onClose} className="shrink-0 mt-0.5">
              <XIcon className="size-4" />
            </Button>
          </div>
        </SheetHeader>

        <div className="flex-1 overflow-y-auto px-5 py-5 flex flex-col gap-5">

          {/* ── BGP Community ── */}
          <div className="flex flex-col gap-1.5">
            <label className="text-xs font-medium flex items-center gap-1">
              <TagIcon className="size-3 text-muted-foreground" />
              BGP Community <span className="text-destructive ml-0.5">*</span>
            </label>
            <CommunityInput
              value={form.community}
              onChange={handleCommunityChange}
              suggestions={communitySuggestions}
              nameMap={communityNameMap}
            />
            {form.community && communityNameMap[form.community] && (
              <p className={cn(
                "text-[11px] flex items-center gap-1",
                BLACKHOLE_COMMUNITIES.has(form.community)
                  ? "text-red-600 dark:text-red-400"
                  : "text-emerald-600 dark:text-emerald-400",
              )}>
                <CheckIcon className="size-3" />{communityNameMap[form.community]}
              </p>
            )}
            {evoHint && (
              <p className="text-[11px] text-amber-600 dark:text-amber-400 flex items-center gap-1">
                <AlertCircleIcon className="size-3 shrink-0" />{evoHint}
              </p>
            )}
            <p className="text-[11px] text-muted-foreground">Формат: AS:NNN · RFC 1997 / RFC 7999 · подсказки из EvoBGP при включённой интеграции</p>
          </div>

          <Separator />

          {/* ── Action toggle ── */}
          <div className="flex flex-col gap-2">
            <label className="text-xs font-medium">Действие</label>
            <div className="grid grid-cols-2 gap-2">
              {(["route", "blackhole"] as const).map(a => (
                <button
                  key={a}
                  type="button"
                  onClick={() => set("action", a)}
                  className={cn(
                    "flex flex-col items-start gap-1.5 rounded-lg border px-3 py-3 text-left transition-all",
                    form.action === a
                      ? a === "blackhole"
                        ? "border-red-500/50 bg-red-500/8 ring-1 ring-red-500/30"
                        : "border-primary bg-primary/5 ring-1 ring-primary/30"
                      : "border-border hover:border-muted-foreground/40 hover:bg-muted/30",
                  )}
                >
                  <div className="flex items-center gap-1.5 w-full">
                    <span className={cn(
                      "text-xs font-semibold",
                      form.action === a
                        ? a === "blackhole" ? "text-red-600 dark:text-red-400" : "text-primary"
                        : "text-foreground",
                    )}>
                      {a === "route" ? "Маршрутизировать" : "⊘ Blackhole"}
                    </span>
                    {form.action === a && <CheckIcon className={cn(
                      "size-3 ml-auto shrink-0",
                      a === "blackhole" ? "text-red-500" : "text-primary",
                    )} />}
                  </div>
                  <span className="text-[10px] text-muted-foreground leading-tight">
                    {a === "route"
                      ? "Next-hop через GRE или рекурсивный маршрут"
                      : "set type blackhole в таблице маршрутов"}
                  </span>
                </button>
              ))}
            </div>
          </div>

          {/* ── GRE / recursive gateway (only for route action) ── */}
          {!isBlackhole && (
            <>
              <Separator />
              <div className="flex flex-col gap-2">
                <label className="text-xs font-medium">
                  Выход маршрута <span className="text-destructive">*</span>
                </label>
                <Tabs
                  value={form.gatewayKind}
                  onValueChange={(v) => {
                    const k = v as "gre" | "recursive"
                    setForm(f => {
                      const next = { ...f, gatewayKind: k, gateway: "", gatewayTunnelId: "" }
                      if (touched) validate_(next)
                      return next
                    })
                  }}
                  className="flex flex-col gap-3"
                >
                  <TabsList className="w-full grid grid-cols-2 h-9 p-1">
                    <TabsTrigger value="gre" className="text-xs gap-1.5">
                      <NetworkIcon className="size-3.5" />
                      GRE-туннель
                    </TabsTrigger>
                    <TabsTrigger value="recursive" className="text-xs gap-1.5">
                      <RouteIcon className="size-3.5" />
                      Рекурсивный
                    </TabsTrigger>
                  </TabsList>
                  <TabsContent value="gre" className="flex flex-col gap-2 mt-0 outline-none">
                    <GatewayCards
                      serverId={serverId}
                      selected={form.gateway}
                      tunnelsList={tunnelsList}
                      serversList={serversList}
                      onSelect={(gw, tid) => {
                        setForm(f => {
                          const next = { ...f, gatewayKind: "gre" as const, gateway: gw, gatewayTunnelId: tid }
                          if (touched) validate_(next)
                          return next
                        })
                      }}
                    />
                  </TabsContent>
                  <TabsContent value="recursive" className="flex flex-col gap-2 mt-0 outline-none">
                    <RecursiveGatewayCards
                      selectedTunnelId={form.gatewayTunnelId}
                      routes={recursiveRoutes}
                      onSelect={(r) => {
                        setForm(f => {
                          const next = {
                            ...f,
                            gatewayKind: "recursive" as const,
                            gateway: gatewayFromRecursiveDst(r.dstAddress),
                            gatewayTunnelId: `rec:${r.id}`,
                          }
                          if (touched) validate_(next)
                          return next
                        })
                      }}
                    />
                  </TabsContent>
                </Tabs>
                {touched && form.action === "route" && !form.gatewayTunnelId.trim() && (
                  <p className="text-xs text-destructive flex items-center gap-1">
                    <AlertCircleIcon className="size-3" />
                    {form.gatewayKind === "recursive"
                      ? "Выберите рекурсивный маршрут"
                      : "Выберите GRE-шлюз"}
                  </p>
                )}
              </div>
            </>
          )}

          {/* ── Blackhole info panel ── */}
          {isBlackhole && (
            <div className="rounded-lg border border-red-500/20 bg-red-500/5 px-4 py-3 flex flex-col gap-1.5">
              <p className="text-xs font-semibold text-red-600 dark:text-red-400">RTBH — Remote Triggered Black Hole</p>
              <p className="text-[11px] text-muted-foreground leading-relaxed">
                Маршруты с этим community будут установлены с типом <code className="font-mono">blackhole</code> в таблице маршрутизации RouterOS.
                Трафик на совпадающие префиксы будет молча сброшен.
              </p>
              <div className="mt-1 rounded bg-[#0d1117] px-3 py-2 font-mono text-[10px] leading-relaxed text-[#8b949e] overflow-x-auto">
                <span className="text-[#ff7b72]">if</span>
                {" (bgp-communities includes "}
                <span className="text-[#79c0ff]">{form.community || "AS:NNN"}</span>
                {") {\n  "}
                <span className="text-[#ff7b72]">set type blackhole</span>
                {";\n  accept;\n}"}
              </div>
            </div>
          )}

          <Separator />

          <div className="flex flex-col gap-1.5">
            <label className="text-xs font-medium text-muted-foreground">Описание (опционально)</label>
            <Input value={form.description} onChange={e => set("description", e.target.value)}
              placeholder={isBlackhole ? "RTBH — нулевая маршрутизация DDoS-трафика" : "YouTube bypass через Франкфурт"}
              className="h-9 text-sm" />
          </div>

          {error && (
            <div className="flex items-center gap-2 text-sm text-destructive bg-destructive/10 border border-destructive/20 px-3 py-2 rounded-md">
              <AlertCircleIcon className="size-4 shrink-0" />{error}
            </div>
          )}
        </div>

        <SheetFooter className="px-5 py-4 border-t shrink-0 gap-2">
          <Button variant="outline" onClick={onClose} className="flex-1">Отмена</Button>
          <Button
            onClick={handleSave}
            className={cn("flex-1", isBlackhole && "bg-red-600 hover:bg-red-700 text-white border-0")}
          >
            <CheckIcon className="size-4" />
            {mode === "create" ? "Добавить" : "Сохранить"}
          </Button>
        </SheetFooter>
      </SheetContent>
    </Sheet>
  )
}

// ── config preview modal ───────────────────────────────────────────────────────

function PreviewModal({ open, serverId, rulesets, onClose, serversList, tunnelsList, recRoutesByServer }: {
  open: boolean
  serverId: string
  rulesets: ServerFilterRuleset[]
  onClose: () => void
  serversList: Server[]
  tunnelsList: GreTunnel[]
  recRoutesByServer: Record<string, RecursiveRouteLite[]>
}) {
  const singleRuleset = useMemo(
    () => rulesets.filter(r => r.serverId === serverId),
    [rulesets, serverId],
  )
  const server = serversList.find(s => s.id === serverId)
  const totalRules = singleRuleset.reduce((s, r) => s + r.rules.length, 0)

  const config = useMemo(
    () =>
      open
        ? generateCombinedRouterOSConfig(singleRuleset, serversList, tunnelsList, recRoutesByServer)
        : "",
    [open, singleRuleset, serversList, tunnelsList, recRoutesByServer],
  )

  return (
    <CodeExportSheet
      open={open}
      onClose={onClose}
      title="RouterOS config"
      description={
        server
          ? `${server.name} · ${totalRules} правил`
          : "Экспорт правил фильтрации"
      }
      formats={[
        {
          id: "rsc",
          label: "RouterOS",
          filename: `filters-${server?.name ?? "server"}.rsc`,
          code: config,
        },
      ]}
    />
  )
}

// ── Copy Rules Sheet ──────────────────────────────────────────────────────────

type CopyMode = "append" | "replace"

/** Try to find an equivalent tunnel on the target server (same remoteAddress). */
function mapTunnel(
  sourceTunnelId: string,
  targetServerId: string,
  tunnelsList: GreTunnel[],
): { tunnelId: string; gateway: string } | null {
  const src = tunnelsList.find(t => t.id === sourceTunnelId)
  if (!src) return null
  const dst = tunnelsList.find(
    t => t.serverId === targetServerId && t.remoteAddress === src.remoteAddress,
  )
  if (!dst) return null
  return { tunnelId: dst.id, gateway: innerIpToGateway(dst.remoteInnerIp) }
}

/** Лучший кандидат на целевом сервере: тот же dst-address и routing-table (несколько via — меньший distance) */
function pickRecursiveRouteForCopy(
  src: RecursiveRouteLite,
  targetRoutes: RecursiveRouteLite[],
): RecursiveRouteLite | undefined {
  const candidates = targetRoutes.filter(
    r =>
      r.dstAddress === src.dstAddress &&
      r.routingTable === src.routingTable &&
      !r.disabled,
  )
  if (candidates.length === 0) return undefined
  return candidates.reduce((a, b) => (a.distance <= b.distance ? a : b))
}

/** Сопоставить рекурсивный маршрут на целевом сервере по dst + table; gateway для фильтра — из dst-address */
function mapRecursiveGatewayForCopy(
  gatewayTunnelId: string,
  sourceRoutes: RecursiveRouteLite[],
  targetRoutes: RecursiveRouteLite[],
): { gateway: string; gatewayTunnelId: string } {
  const sid = gatewayTunnelId.slice(4)
  const src = sourceRoutes.find(r => r.id === sid)
  if (!src) return { gateway: "", gatewayTunnelId: "" }
  const tgt = pickRecursiveRouteForCopy(src, targetRoutes)
  if (!tgt) {
    return { gateway: gatewayFromRecursiveDst(src.dstAddress), gatewayTunnelId: "" }
  }
  return {
    gateway: gatewayFromRecursiveDst(tgt.dstAddress),
    gatewayTunnelId: `rec:${tgt.id}`,
  }
}

function CopyRulesSheet({
  open, onClose,
  sourceServerId, sourceRules,
  rulesets, onCopy, serversList,
  greByServer,
  ensureGreFor,
  recRoutesByServer,
  ensureRecursiveFor,
}: {
  open: boolean; onClose: () => void
  sourceServerId: string; sourceRules: FilterRule[]
  rulesets: ServerFilterRuleset[]
  onCopy: (targetServerId: string, rules: FilterRule[], mode: CopyMode) => void
  serversList: Server[]
  greByServer: Record<string, GreTunnel[]>
  ensureGreFor?: (serverId: string) => void
  recRoutesByServer: Record<string, RecursiveRouteLite[]>
  ensureRecursiveFor?: (serverId: string) => void
}) {
  const [selected,  setSelected]  = useState<Set<string>>(new Set())
  const [targetId,  setTargetId]  = useState<string | null>(null)
  const [mode,      setMode]      = useState<CopyMode>("append")
  const [done,      setDone]      = useState(false)

  const sourceRecursiveRoutes = recRoutesByServer[sourceServerId] ?? []
  const targetRecursiveRoutes = targetId ? (recRoutesByServer[targetId] ?? []) : []

  // Reset each time the sheet opens
  useEffect(() => {
    queueMicrotask(() => {
      if (!open) { setDone(false); return }
      setSelected(new Set(sourceRules.map(r => r.id)))
      setTargetId(null); setMode("append"); setDone(false)
    })
  }, [open, sourceRules])

  useEffect(() => {
    if (!open || !ensureGreFor) return
    ensureGreFor(sourceServerId)
  }, [open, sourceServerId, ensureGreFor])

  useEffect(() => {
    if (!open || !targetId || !ensureGreFor) return
    ensureGreFor(targetId)
  }, [open, targetId, ensureGreFor])

  useEffect(() => {
    if (!open || !ensureRecursiveFor) return
    ensureRecursiveFor(sourceServerId)
  }, [open, sourceServerId, ensureRecursiveFor])

  useEffect(() => {
    if (!open || !targetId || !ensureRecursiveFor) return
    ensureRecursiveFor(targetId)
  }, [open, targetId, ensureRecursiveFor])

  const tunnelsList = useMemo(
    () => [
      ...(greByServer[sourceServerId] ?? []),
      ...(targetId ? (greByServer[targetId] ?? []) : []),
    ],
    [greByServer, sourceServerId, targetId],
  )

  const sourceServer  = serversList.find(s => s.id === sourceServerId)!
  const targetServers = serversList.filter(s => s.id !== sourceServerId)

  const selectedRules = sourceRules.filter(r => selected.has(r.id))
  const targetRules   = targetId
    ? (rulesets.find(r => r.serverId === targetId)?.rules ?? [])
    : []

  // Compute gateway mapping for each selected route rule
  const mapping = useMemo(() => {
    if (!targetId) return []
    return selectedRules
      .filter(r =>
        (r.action ?? "route") === "route" &&
        r.gatewayTunnelId &&
        !isRecursiveGatewayRef(r.gatewayTunnelId),
      )
      .map(r => {
        const srcTunnel  = tunnelsList.find(t => t.id === r.gatewayTunnelId)
        const dstMapping = mapTunnel(r.gatewayTunnelId, targetId, tunnelsList)
        const dstTunnel  = dstMapping ? tunnelsList.find(t => t.id === dstMapping.tunnelId) : undefined
        return { rule: r, srcTunnel, dstTunnel, mapped: !!dstMapping }
      })
      // deduplicate by source tunnel
      .filter((m, i, arr) =>
        arr.findIndex(x => x.srcTunnel?.id === m.srcTunnel?.id) === i
      )
  }, [selectedRules, targetId, tunnelsList])

  const unmappedCount = mapping.filter(m => !m.mapped).length

  function toggleRule(id: string) {
    setSelected(prev => {
      const next = new Set(prev)
      if (next.has(id)) next.delete(id); else next.add(id)
      return next
    })
  }
  function toggleAll() {
    setSelected(prev =>
      prev.size === sourceRules.length
        ? new Set()
        : new Set(sourceRules.map(r => r.id))
    )
  }

  function handleCopy() {
    if (!targetId || selectedRules.length === 0) return
    const copied: FilterRule[] = selectedRules.map(r => {
      if ((r.action ?? "route") === "blackhole") {
        return { ...r, id: newId() }
      }
      if (isRecursiveGatewayRef(r.gatewayTunnelId)) {
        const m = mapRecursiveGatewayForCopy(r.gatewayTunnelId, sourceRecursiveRoutes, targetRecursiveRoutes)
        return {
          ...r,
          id: newId(),
          gateway: m.gateway,
          gatewayTunnelId: m.gatewayTunnelId,
        }
      }
      const m = mapTunnel(r.gatewayTunnelId, targetId, tunnelsList)
      return {
        ...r,
        id:              newId(),
        gateway:         m?.gateway         ?? "",
        gatewayTunnelId: m?.tunnelId        ?? "",
      }
    })
    onCopy(targetId, copied, mode)
    setDone(true)
    setTimeout(() => onClose(), 900)
  }

  const canCopy = targetId !== null && selectedRules.length > 0 && !done

  return (
    <Sheet open={open} onOpenChange={v => { if (!v) onClose() }}>
      <SheetContent side="right" className="sm:max-w-[500px] p-0 flex flex-col" showCloseButton={false}>

        {/* header */}
        <SheetHeader className="px-5 pt-5 pb-4 border-b shrink-0">
          <div className="flex items-start justify-between gap-2">
            <div>
              <SheetTitle className="text-base flex items-center gap-2">
                <CopyIcon className="size-4 text-muted-foreground" />
                Копировать правила
              </SheetTitle>
              <SheetDescription className="text-xs mt-1 flex items-center gap-1.5">
                <Flag code={sourceServer.country} size={12} />
                <span className="font-mono font-medium text-foreground/70">{sourceServer.name}</span>
                <ArrowRightIcon className="size-3 text-muted-foreground/40" />
                <span className="text-muted-foreground">целевой сервер</span>
              </SheetDescription>
            </div>
            <Button variant="ghost" size="icon-sm" onClick={onClose} className="shrink-0 mt-0.5">
              <XIcon className="size-4" />
            </Button>
          </div>
        </SheetHeader>

        <div className="flex-1 overflow-y-auto flex flex-col divide-y divide-border">

          {/* ── rule selection ── */}
          <div className="px-5 py-4 flex flex-col gap-2">
            <div className="flex items-center justify-between">
              <p className="text-xs font-medium">Правила для копирования</p>
              <button onClick={toggleAll}
                className="text-[11px] text-muted-foreground hover:text-foreground transition-colors">
                {selected.size === sourceRules.length ? "Снять все" : "Выбрать все"}
              </button>
            </div>

            <div className="flex flex-col gap-1">
              {sourceRules.map(r => {
                const isBlackhole = r.action === "blackhole"
                const tunnel      = !isBlackhole && !isRecursiveGatewayRef(r.gatewayTunnelId)
                  ? tunnelsList.find(t => t.id === r.gatewayTunnelId)
                  : undefined
                const rec         = !isBlackhole && isRecursiveGatewayRef(r.gatewayTunnelId)
                  ? sourceRecursiveRoutes.find(x => x.id === r.gatewayTunnelId.slice(4))
                  : undefined
                const isSelected  = selected.has(r.id)
                return (
                  <button key={r.id} type="button" onClick={() => toggleRule(r.id)}
                    className={cn(
                      "flex items-center gap-2.5 px-3 py-2 rounded-lg border text-left transition-all",
                      isSelected
                        ? "bg-muted/50 border-border"
                        : "border-transparent opacity-40 hover:opacity-70",
                    )}>
                    {/* checkbox */}
                    <span className={cn(
                      "size-4 rounded border shrink-0 flex items-center justify-center transition-colors",
                      isSelected ? "bg-primary border-primary" : "border-border",
                    )}>
                      {isSelected && <CheckIcon className="size-2.5 text-primary-foreground" />}
                    </span>
                    {/* community */}
                    <span className={cn(
                      "font-mono text-[11px] px-1.5 py-0.5 rounded border shrink-0",
                      isBlackhole
                        ? "bg-red-500/10 text-red-600 dark:text-red-400 border-red-500/25"
                        : "bg-sky-500/10 text-sky-600 dark:text-sky-400 border-sky-500/20",
                    )}>
                      {r.community}
                    </span>
                    {/* action / tunnel */}
                    {isBlackhole ? (
                      <span className="text-[10px] font-semibold text-red-500 uppercase tracking-wide shrink-0">⊘ blackhole</span>
                    ) : rec ? (
                      <span className="text-[11px] font-mono text-muted-foreground truncate">
                        {rec.dstAddress} → {gatewayFromRecursiveDst(rec.dstAddress)}
                      </span>
                    ) : tunnel ? (
                      <span className="text-[11px] font-mono text-muted-foreground truncate">{tunnel.name}</span>
                    ) : null}
                    {/* description */}
                    <span className="text-[11px] text-muted-foreground truncate ml-auto">{r.description}</span>
                  </button>
                )
              })}
            </div>

            <p className="text-[11px] text-muted-foreground">
              Выбрано: <span className="font-semibold text-foreground">{selected.size}</span> из {sourceRules.length}
            </p>
          </div>

          {/* ── target server ── */}
          <div className="px-5 py-4 flex flex-col gap-2">
            <p className="text-xs font-medium">Целевой сервер</p>
            <div className="flex flex-col gap-1.5">
              {targetServers.map(s => {
                const ruleCount = rulesets.find(r => r.serverId === s.id)?.rules.length ?? 0
                const isTarget  = targetId === s.id
                return (
                  <button key={s.id} type="button" onClick={() => setTargetId(s.id)}
                    className={cn(
                      "flex items-center gap-2.5 px-3 py-2.5 rounded-lg border text-left transition-all",
                      isTarget
                        ? "border-primary bg-primary/5 ring-1 ring-primary/30"
                        : "border-border hover:border-muted-foreground/40 hover:bg-muted/30",
                    )}>
                    <StatusDot status={s.status} />
                    <Flag code={s.country} size={14} />
                    <span className="font-mono text-sm font-medium flex-1">{s.name}</span>
                    <TypeChip type={s.type} />
                    <span className="text-[11px] text-muted-foreground tabular-nums shrink-0">
                      {ruleCount} правил
                    </span>
                    {isTarget && <CheckIcon className="size-3.5 text-primary shrink-0" />}
                  </button>
                )
              })}
            </div>
          </div>

          {/* ── copy mode ── */}
          {targetId && (
            <div className="px-5 py-4 flex flex-col gap-2">
              <p className="text-xs font-medium">Режим копирования</p>
              <div className="grid grid-cols-2 gap-2">
                {([
                  { v: "append"  as CopyMode, label: "Добавить", sub: `+${selectedRules.length} к ${targetRules.length} существующим` },
                  { v: "replace" as CopyMode, label: "Заменить", sub: `${targetRules.length} правил → ${selectedRules.length} новых` },
                ] as const).map(opt => (
                  <button key={opt.v} type="button" onClick={() => setMode(opt.v)}
                    className={cn(
                      "flex flex-col gap-1 text-left px-3 py-2.5 rounded-lg border transition-all",
                      mode === opt.v
                        ? opt.v === "replace"
                          ? "border-amber-500/50 bg-amber-500/5 ring-1 ring-amber-500/20"
                          : "border-primary bg-primary/5 ring-1 ring-primary/30"
                        : "border-border hover:border-muted-foreground/40 hover:bg-muted/30",
                    )}>
                    <div className="flex items-center gap-1.5">
                      <span className={cn(
                        "text-xs font-semibold",
                        mode === opt.v
                          ? opt.v === "replace" ? "text-amber-600 dark:text-amber-400" : "text-primary"
                          : "text-foreground",
                      )}>{opt.label}</span>
                      {mode === opt.v && <CheckIcon className={cn(
                        "size-3 ml-auto",
                        opt.v === "replace" ? "text-amber-500" : "text-primary",
                      )} />}
                    </div>
                    <span className="text-[10px] text-muted-foreground leading-tight">{opt.sub}</span>
                  </button>
                ))}
              </div>

              {mode === "replace" && targetRules.length > 0 && (
                <div className="flex items-center gap-2 text-[11px] text-amber-600 dark:text-amber-400 bg-amber-500/8 border border-amber-500/20 px-3 py-2 rounded-md">
                  <AlertTriangleIcon className="size-3.5 shrink-0" />
                  {targetRules.length} существующих правил на {serversList.find(s => s.id === targetId)?.name} будут удалены
                </div>
              )}
            </div>
          )}

          {/* ── gateway mapping preview ── */}
          {targetId && mapping.length > 0 && (
            <div className="px-5 py-4 flex flex-col gap-2">
              <p className="text-xs font-medium flex items-center gap-1.5">
                Маппинг GRE-туннелей
                {unmappedCount > 0 && (
                  <span className="text-[10px] font-normal text-amber-600 dark:text-amber-400">
                    · {unmappedCount} не найдено
                  </span>
                )}
              </p>
              <div className="flex flex-col gap-1.5">
                {mapping.map((m, i) => (
                  <div key={i} className={cn(
                    "flex items-center gap-2 px-3 py-2 rounded-lg border text-xs",
                    m.mapped
                      ? "border-emerald-500/20 bg-emerald-500/5"
                      : "border-amber-500/20 bg-amber-500/5",
                  )}>
                    {/* source tunnel */}
                    <span className="font-mono font-medium shrink-0">{m.srcTunnel?.name ?? "—"}</span>
                    <ArrowRightIcon className="size-3 text-muted-foreground/50 shrink-0" />
                    {/* target tunnel */}
                    {m.mapped ? (
                      <span className="font-mono font-medium text-emerald-600 dark:text-emerald-400 flex-1 truncate">
                        {m.dstTunnel?.name}
                      </span>
                    ) : (
                      <span className="text-amber-600 dark:text-amber-400 flex-1 truncate">
                        туннель не найден — шлюз будет пустым
                      </span>
                    )}
                    {m.mapped
                      ? <CheckIcon className="size-3.5 text-emerald-500 shrink-0" />
                      : <AlertTriangleIcon className="size-3.5 text-amber-500 shrink-0" />
                    }
                  </div>
                ))}
                {mapping.some(m => !m.mapped) && (
                  <p className="text-[10px] text-muted-foreground/60">
                    Правила с пустым шлюзом нужно отредактировать после копирования
                  </p>
                )}
              </div>
            </div>
          )}

        </div>

        {/* footer */}
        <SheetFooter className="px-5 py-4 border-t shrink-0 gap-2">
          <Button variant="outline" onClick={onClose} className="flex-1">Отмена</Button>
          <Button
            onClick={handleCopy}
            disabled={!canCopy}
            className="flex-1"
          >
            {done
              ? <><CheckIcon className="size-4" />Скопировано!</>
              : <><CopyIcon className="size-4" />Копировать {selectedRules.length} правил</>
            }
          </Button>
        </SheetFooter>
      </SheetContent>
    </Sheet>
  )
}

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

interface BackendServer {
  id: number
  name: string
  host: string
  type: "jump-host" | "exit-node" | "home-router"
  site: string
  country: string
  asn: string
  enabled: boolean
  status: "online" | "offline" | null
  latency: number | null
}

interface LiveFiltersResponse {
  rulesets: ServerFilterRuleset[]
  greTunnels: GreTunnel[]
}

function buildRulesets(serverList: Server[], sourceRulesets: ServerFilterRuleset[]): ServerFilterRuleset[] {
  return serverList.map(s => ({
    serverId: s.id,
    rules: sourceRulesets.find(r => r.serverId === s.id)?.rules ?? [],
  }))
}

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

export default function FiltersPage() {
  const { mode, backendUrl } = useDataSource()
  const evo = useEvoBGP()
  /** Как на `/servers`: живые данные при режиме «Живые», без ожидания отдельного health-check */
  const isLive = mode === "live"
  const apiFetch = useMemo(() => makeApiFetch(backendUrl), [backendUrl])

  const communityNameMap = useMemo(() => {
    if (mode === "live" && evo.enabled) {
      const m: Record<string, string> = {}
      for (const c of evo.snapshot?.communities ?? []) {
        const v = (c.value ?? "").trim()
        if (!v) continue
        m[v] = c.name?.trim() || v
      }
      return m
    }
    return { ...COMMUNITY_NAMES }
  }, [mode, evo.enabled, evo.snapshot?.communities, evo.snapshot?.fetchedAt])

  const communitySuggestions = useMemo(
    () => Object.keys(communityNameMap).sort((a, b) => a.localeCompare(b)),
    [communityNameMap],
  )

  const evoCommunityHint = useMemo(() => {
    if (mode !== "live" || !evo.enabled) return null
    if (evo.loading) return "Загрузка справочника communities из EvoBGP…"
    return null
  }, [mode, evo.enabled, evo.loading])

  const [liveServers, setLiveServers] = useState<Server[]>([])
  /** GRE с роутера только по запрошенным serverId (текущий + цели копирования) */
  const [greByServer, setGreByServer] = useState<Record<string, GreTunnel[]>>({})
  const [recRoutesByServer, setRecRoutesByServer] = useState<Record<string, RecursiveRouteLite[]>>({})
  const [rulesets, setRulesets] = useState<ServerFilterRuleset[]>(
    () => buildRulesets(servers, serverFilterRulesets),
  )
  const [liveLoadState, setLiveLoadState] = useState<"idle" | "loading" | "error">(
    () => (mode === "live" ? "loading" : "idle"),
  )

  const ensureGreTunnels = 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])

  const ensureRecursiveRoutes = useCallback(async (serverId: string) => {
    if (!isLive) return
    try {
      const d = await apiFetch<{ routes: RecursiveRouteLite[] }>(
        `/api/recursive-routes?serverId=${encodeURIComponent(serverId)}`,
      )
      setRecRoutesByServer(prev => ({ ...prev, [serverId]: d.routes }))
    } catch {
      setRecRoutesByServer(prev => ({ ...prev, [serverId]: [] }))
    }
  }, [isLive, apiFetch])
  const [selectedServerId, setSelectedServerId] = useState("srv1")
  const [search,       setSearch]      = useState("")
  const [sheetOpen,    setSheetOpen]   = useState(false)
  const [sheetMode,    setSheetMode]   = useState<"create" | "edit">("create")
  const [sheetInitial, setSheetInitial]= useState<RuleForm>(emptyForm())
  const [editingId,    setEditingId]   = useState<string | null>(null)
  const [previewOpen,  setPreviewOpen] = useState(false)
  const [copyOpen,     setCopyOpen]    = useState(false)
  const [syncBusy,     setSyncBusy]    = useState<"from" | "to" | null>(null)
  const [routerCompare, setRouterCompare] = useState<{
    serverId: string
    byCommunity: Record<string, FilterRouterSyncStatus>
  } | null>(null)
  const [routerCompareLoading, setRouterCompareLoading] = useState(false)

  useEffect(() => {
    setRouterCompare(null)
  }, [selectedServerId])

  useEffect(() => {
    if (!isLive) {
      queueMicrotask(() => {
        setLiveServers([])
        setGreByServer({})
        setRecRoutesByServer({})
        setRulesets(buildRulesets(servers, serverFilterRulesets))
        setSelectedServerId(servers[0]?.id ?? "")
        setLiveLoadState("idle")
      })
      return
    }

    setLiveLoadState("loading")
    Promise.all([
      apiFetch<BackendServer[]>("/api/servers"),
      apiFetch<LiveFiltersResponse>("/api/filters/rules"),
    ])
      .then(([backendServers, liveFilters]) => {
        const mappedServers: Server[] = backendServers.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,
        }))

        const normalizedRulesets = buildRulesets(mappedServers, liveFilters.rulesets)
        setLiveServers(mappedServers)
        setRulesets(normalizedRulesets)
        setSelectedServerId(prev => (
          mappedServers.some(s => s.id === prev) ? prev : (mappedServers[0]?.id ?? "")
        ))
        setLiveLoadState("idle")
      })
      .catch(() => {
        setLiveServers([])
        setGreByServer({})
        setRulesets(buildRulesets(servers, serverFilterRulesets))
        setSelectedServerId(servers[0]?.id ?? "")
        setLiveLoadState("error")
      })
  }, [isLive, apiFetch])

  useEffect(() => {
    if (!isLive || !selectedServerId) return
    void Promise.all([
      ensureGreTunnels(selectedServerId),
      ensureRecursiveRoutes(selectedServerId),
    ])
  }, [isLive, selectedServerId, ensureGreTunnels, ensureRecursiveRoutes])

  const allServers = isLive ? liveServers : servers
  const allTunnels = isLive ? (greByServer[selectedServerId] ?? []) : greTunnels
  const selectedServer = allServers.find(s => s.id === selectedServerId) ?? allServers[0]
  const totalRules     = rulesets.reduce((s, r) => s + r.rules.length, 0)

  const filterRailItems = 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,
      meta: String(rulesets.find((r) => r.serverId === s.id)?.rules.length ?? 0),
    }))
  ), [allServers, rulesets])

  const handleSelectServer = useCallback((id: string) => {
    setSelectedServerId(id)
    setSearch("")
  }, [])

  const currentRules = useMemo(
    () => rulesets.find(r => r.serverId === selectedServerId)?.rules ?? [],
    [rulesets, selectedServerId],
  )

  const fetchRouterCompare = useCallback(async () => {
    if (!isLive || !selectedServerId) return
    setRouterCompareLoading(true)
    try {
      const d = await apiFetch<{ byCommunity: Record<string, FilterRouterSyncStatus> }>(
        `/api/filters/router-compare?serverId=${encodeURIComponent(selectedServerId)}`,
      )
      setRouterCompare({ serverId: selectedServerId, byCommunity: d.byCommunity })
    } catch {
      setRouterCompare(null)
    } finally {
      setRouterCompareLoading(false)
    }
  }, [isLive, selectedServerId, apiFetch])

  const filteredRules = useMemo(() => {
    const q = search.toLowerCase()
    if (!q) return currentRules
    return currentRules.filter((r) =>
      r.community.includes(q) ||
      (communityNameMap[r.community] ?? "").toLowerCase().includes(q) ||
      r.gateway.includes(q) ||
      r.description.toLowerCase().includes(q),
    )
  }, [currentRules, search, communityNameMap])

  const updateRules = useCallback((serverId: string, updater: (rules: FilterRule[]) => FilterRule[]) => {
    setRouterCompare(rc => (rc && rc.serverId === serverId ? null : rc))
    setRulesets(prev => {
      const next = prev.map(rs =>
        rs.serverId === serverId ? { ...rs, rules: updater(rs.rules) } : rs
      )
      if (isLive) {
        void apiFetch<{ ok: boolean }>("/api/filters/rules", {
          method: "PUT",
          body: JSON.stringify({ rulesets: next }),
        }).catch(() => {})
      }
      return next
    })
  }, [isLive, apiFetch])

  const syncFromRouter = useCallback(async () => {
    if (!isLive || syncBusy) return
    setSyncBusy("from")
    try {
      await apiFetch<{ ok: boolean }>("/api/filters/sync/from-router", {
        method: "POST",
        body: JSON.stringify({ serverId: selectedServerId }),
      })
      const fresh = await apiFetch<LiveFiltersResponse>("/api/filters/rules")
      setRulesets(buildRulesets(allServers, fresh.rulesets))
      await Promise.all([
        ensureGreTunnels(selectedServerId),
        ensureRecursiveRoutes(selectedServerId),
      ])
      await fetchRouterCompare()
    } finally {
      setSyncBusy(null)
    }
  }, [isLive, syncBusy, apiFetch, allServers, selectedServerId, ensureGreTunnels, ensureRecursiveRoutes, fetchRouterCompare])

  const syncToRouter = useCallback(async () => {
    if (!isLive || syncBusy || !selectedServerId) return
    setSyncBusy("to")
    try {
      const res = await apiFetch<{
        ok: boolean
        updatedServers: number
        pushedRules: number
        errors?: Array<{ serverId: number; error: string }>
      }>("/api/filters/sync/to-router", {
        method: "POST",
        body: JSON.stringify({ serverId: selectedServerId }),
      })
      if (res.ok) {
        toast.success(`Загружено правил на роутер: ${res.pushedRules}`)
      } else {
        const detail = res.errors?.[0]?.error ?? "неизвестная ошибка"
        toast.error("Не удалось загрузить правила на роутер", { description: detail })
      }
      await fetchRouterCompare()
    } catch (err) {
      toast.error("Не удалось загрузить правила на роутер", { description: String(err) })
    } finally {
      setSyncBusy(null)
    }
  }, [isLive, syncBusy, selectedServerId, apiFetch, fetchRouterCompare])

  const openCreate = () => {
    setSheetInitial(emptyForm()); setSheetMode("create"); setEditingId(null); setSheetOpen(true)
  }
  const openEdit = (rule: FilterRule) => {
    setSheetInitial({
      action:          rule.action ?? "route",
      community:       rule.community,
      gateway:         rule.gateway,
      gatewayTunnelId: rule.gatewayTunnelId,
      gatewayKind:     isRecursiveGatewayRef(rule.gatewayTunnelId) ? "recursive" : "gre",
      description:     rule.description,
    })
    setSheetMode("edit"); setEditingId(rule.id); setSheetOpen(true)
  }

  const handleSave = (form: RuleForm) => {
    const { gatewayKind: _gk, ...payload } = form
    if (sheetMode === "create") {
      updateRules(selectedServerId, rules => [
        ...rules,
        { id: newId(), ...payload, communityName: communityNameMap[payload.community] ?? "" },
      ])
    } else if (editingId) {
      updateRules(selectedServerId, rules =>
        rules.map(r => r.id === editingId
          ? { ...r, ...payload, communityName: communityNameMap[payload.community] ?? "" }
          : r
        )
      )
    }
    setSheetOpen(false)
  }

  const handleDelete = (id: string) => updateRules(selectedServerId, rules => rules.filter(r => r.id !== id))

  const handleCopyRules = useCallback((targetServerId: string, rules: FilterRule[], mode: CopyMode) => {
    updateRules(targetServerId, existing =>
      mode === "replace" ? rules : [...existing, ...rules]
    )
  }, [updateRules])
  const handleMoveUp   = (index: number) => {
    if (index === 0) return
    updateRules(selectedServerId, rules => {
      const n = [...rules]; [n[index - 1], n[index]] = [n[index], n[index - 1]]; return n
    })
  }
  const handleMoveDown = (index: number) => {
    updateRules(selectedServerId, rules => {
      if (index >= rules.length - 1) return rules
      const n = [...rules]; [n[index], n[index + 1]] = [n[index + 1], n[index]]; return n
    })
  }

  if (!selectedServer) {
    return (
      <div className="flex h-full items-center justify-center text-sm text-muted-foreground">
        Нет доступных серверов
      </div>
    )
  }

  if (isLive && liveLoadState === "loading") {
    return (
      <div className="flex h-full flex-col items-center justify-center gap-3 text-sm text-muted-foreground">
        <LoaderCircleIcon className="size-8 animate-spin text-muted-foreground/50" />
        <p>Загрузка правил и GRE из бекенда…</p>
      </div>
    )
  }

  return (
    <>
    <ServerRailLayout
      items={filterRailItems}
      selectedId={selectedServerId}
      onSelect={handleSelectServer}
      showAll={false}
      header={
        <PageHeader
          crumbs={[{ label: "Управление" }, { label: "Фильтры" }]}
          actions={
            <>
              <ServerRailMobileButton />
              {isLive && (
                <>
                  <Button
                    variant="outline"
                    size="sm"
                    onClick={syncFromRouter}
                    disabled={syncBusy !== null}
                    title="Синхронизация Router → БД"
                  >
                    {syncBusy === "from" ? "Синк Router → DB…" : "Router → DB"}
                  </Button>
                  <Button
                    variant="outline"
                    size="sm"
                    onClick={syncToRouter}
                    disabled={syncBusy !== null}
                    title="Синхронизация БД → Router"
                  >
                    {syncBusy === "to" ? "Синк DB → Router…" : "DB → Router"}
                  </Button>
                  <Button
                    variant="outline"
                    size="sm"
                    onClick={() => void fetchRouterCompare()}
                    disabled={syncBusy !== null || routerCompareLoading}
                    title="Сравнить правила в БД с цепочкой bgp-in на MikroTik"
                    className="gap-1.5"
                  >
                    {routerCompareLoading ? (
                      <LoaderCircleIcon className="size-4 animate-spin" />
                    ) : (
                      <RefreshCwIcon className="size-4" />
                    )}
                    Сверить
                  </Button>
                </>
              )}
              <Button variant="outline" size="sm" onClick={() => setPreviewOpen(true)}>
                <FileCodeIcon className="size-4" />RouterOS
              </Button>
              <Button
                variant="outline" size="sm"
                onClick={() => setCopyOpen(true)}
                disabled={currentRules.length === 0}
                title="Копировать правила на другой сервер"
              >
                <CopyIcon className="size-4" />Копировать
              </Button>
              <Button size="sm" onClick={openCreate}>
                <PlusIcon className="size-4" />Новое правило
              </Button>
            </>
          }
        />
      }
      banner={
        <>
          {isLive && liveLoadState === "error" && (
            <div className="shrink-0 border-b border-destructive/30 bg-destructive/10 px-6 py-2.5 text-xs text-destructive flex items-center gap-2">
              <AlertCircleIcon className="size-3.5 shrink-0" />
              Бекенд недоступен — показаны демо-данные из lib/data. Проверьте URL бекенда в настройках.
            </div>
          )}
          <div className="border-b px-4 py-3 flex items-center gap-3 flex-wrap shrink-0 md:px-6">
            <div className="relative min-w-[200px] max-w-xs flex-1">
              <SearchIcon className="absolute left-2.5 top-1/2 -translate-y-1/2 size-3.5 text-muted-foreground pointer-events-none" />
              <Input className="pl-8 h-8 text-sm" placeholder="Community, gateway, описание…"
                value={search} onChange={e => setSearch(e.target.value)} />
              {search && (
                <button onClick={() => setSearch("")}
                  className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground">
                  <XIcon className="size-3.5" />
                </button>
              )}
            </div>
            <div className="flex items-center gap-1.5 text-xs">
              <span className="text-muted-foreground">Всего правил</span>
              <span className="font-semibold tabular-nums">{totalRules}</span>
            </div>
            {(() => {
              const bhTotal = rulesets.reduce((s, r) => s + r.rules.filter(x => x.action === "blackhole").length, 0)
              if (bhTotal === 0) return null
              return (
                <span className="inline-flex items-center gap-1 text-[11px] font-medium px-2 py-0.5 rounded border
                  bg-red-500/10 text-red-600 dark:text-red-400 border-red-500/20">
                  ⊘ {bhTotal} blackhole
                </span>
              )
            })()}
            <p className="text-xs text-muted-foreground ml-auto">
              {filteredRules.length !== currentRules.length
                ? `${filteredRules.length} из ${currentRules.length} правил`
                : `${currentRules.length} правил`
              }
            </p>
          </div>
        </>
      }
    >
        <div className="flex flex-col gap-4">

          {/* RouterOS 7.x BGP extensions — только демо из lib/data (моки) */}
          {mode === "mock" && (() => {
            const filter = mockFiltersCatalog.find(f => f.servers.includes(selectedServerId))
            const hasLarge = filter?.largeCommunities && filter.largeCommunities.length > 0
            const hasRpki  = filter?.rpkiValid
            const hasFlow  = filter?.flowspecRules && filter.flowspecRules.length > 0
            if (!hasLarge && !hasRpki && !hasFlow) return null
            return (
              <div className="flex flex-wrap gap-2 text-xs">
                {hasRpki && (
                  <span className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md bg-emerald-500/10 border border-emerald-500/20 text-emerald-600 dark:text-emerald-400">
                    <span className="font-semibold">RPKI</span> · Только валидные маршруты
                  </span>
                )}
                {hasLarge && (
                  <span className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md bg-sky-500/10 border border-sky-500/20 text-sky-600 dark:text-sky-400 font-mono">
                    Large Communities RFC 8092 · {filter?.largeCommunities?.join(", ")}
                  </span>
                )}
                {hasFlow && (
                  <span className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md bg-violet-500/10 border border-violet-500/20 text-violet-600 dark:text-violet-400">
                    Flowspec · {filter?.flowspecRules?.length} правил
                  </span>
                )}
              </div>
            )
          })()}

          <DataPageCard>

            {/* selected server header */}
            <div className="flex items-center gap-2.5 px-5 py-3 border-b bg-muted/10">
              <StatusDot status={selectedServer.status} pulse={selectedServer.status === "online"} />
              <Flag code={selectedServer.country} size={16} />
              <span className="font-mono text-sm font-semibold">{selectedServer.name}</span>
              <TypeChip type={selectedServer.type} />
              <code className="text-[11px] font-mono text-muted-foreground">{selectedServer.host}</code>
              <span className="text-xs text-muted-foreground">{selectedServer.asn}</span>
              {selectedServer.latency !== null && (
                <span className={cn(
                  "text-xs font-mono",
                  selectedServer.latency > 60 ? "text-amber-600 dark:text-amber-400" : "text-muted-foreground",
                )}>{selectedServer.latency}мс</span>
              )}
              <div className="ml-auto flex items-center gap-2 text-xs text-muted-foreground flex-wrap justify-end">
                {isLive && routerCompare?.serverId === selectedServerId && currentRules.length > 0 && (
                  <span className="font-mono tabular-nums">
                    роутер:{" "}
                    <span className="text-emerald-600 dark:text-emerald-500">
                      {Object.values(routerCompare.byCommunity).filter(s => s === "synced").length}
                    </span>
                    /{currentRules.length} совпало
                  </span>
                )}
                <span>{currentRules.length} правил</span>
              </div>
            </div>

            {currentRules.length === 0 ? (
              /* empty state */
              <EmptyState
                icon={<NetworkIcon className="size-4" />}
                title="Нет правил фильтрации"
                description={
                  (isLive ? allTunnels.length > 0 : allTunnels.some(t => t.serverId === selectedServerId))
                    ? "Добавьте правило: BGP community → GRE-шлюз"
                    : "Сначала добавьте GRE-туннели для этого сервера"
                }
                action={
                  (isLive ? allTunnels.length > 0 : allTunnels.some(t => t.serverId === selectedServerId)) ? (
                    <Button size="sm" onClick={openCreate}>
                      <PlusIcon className="size-4" />Добавить правило
                    </Button>
                  ) : undefined
                }
                className="py-20"
              />
            ) : filteredRules.length === 0 ? (
              /* no search results */
              <div className="flex flex-col items-center justify-center py-16 gap-2 text-muted-foreground">
                <SearchIcon className="size-6 opacity-20" />
                <p className="text-sm">Ничего не найдено</p>
              </div>
            ) : (
              <FiltersDataGrid
                rules={search ? filteredRules : currentRules}
                tunnelsList={allTunnels}
                serversList={allServers}
                communityNameMap={communityNameMap}
                recursiveRoutes={recRoutesByServer[selectedServerId] ?? []}
                routerSyncByCommunity={
                  !isLive || !routerCompare || routerCompare.serverId !== selectedServerId
                    ? null
                    : routerCompare.byCommunity
                }
                isLive={isLive}
                enableSorting={!!search}
                onEdit={openEdit}
                onDelete={handleDelete}
                onMoveUp={(id) => handleMoveUp(currentRules.findIndex((r) => r.id === id))}
                onMoveDown={(id) => handleMoveDown(currentRules.findIndex((r) => r.id === id))}
              />
            )}

            {/* add rule shortcut */}
            <button onClick={openCreate}
              className="w-full flex items-center gap-2 px-5 py-2 text-xs text-muted-foreground hover:text-foreground hover:bg-muted/20 transition-colors border-t">
              <PlusIcon className="size-3.5" />
              Добавить правило для {selectedServer.name}
            </button>
          </DataPageCard>

        </div>
    </ServerRailLayout>

      <RuleSheet
        key={`${sheetMode}-${editingId ?? "new"}-${selectedServerId}`}
        open={sheetOpen}
        mode={sheetMode}
        initial={sheetInitial}
        serverId={selectedServerId}
        onSave={handleSave}
        onClose={() => setSheetOpen(false)}
        serversList={allServers}
        tunnelsList={allTunnels}
        communityNameMap={communityNameMap}
        communitySuggestions={communitySuggestions}
        evoHint={evoCommunityHint}
        recursiveRoutes={recRoutesByServer[selectedServerId] ?? []}
      />

      <PreviewModal
        open={previewOpen}
        serverId={selectedServerId}
        rulesets={rulesets}
        onClose={() => setPreviewOpen(false)}
        serversList={allServers}
        tunnelsList={allTunnels}
        recRoutesByServer={recRoutesByServer}
      />

      <CopyRulesSheet
        open={copyOpen}
        onClose={() => setCopyOpen(false)}
        sourceServerId={selectedServerId}
        sourceRules={currentRules}
        rulesets={rulesets}
        onCopy={handleCopyRules}
        serversList={allServers}
        greByServer={greByServer}
        ensureGreFor={ensureGreTunnels}
        recRoutesByServer={recRoutesByServer}
        ensureRecursiveFor={ensureRecursiveRoutes}
      />
    </>
  )
}
