import { and, asc, eq, inArray } from "drizzle-orm"
import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
import { db } from "../db/index.js"
import { filterRules, recursiveRoutes, servers } from "../db/schema.js"
import { MikrotikClient } from "../services/mikrotik.js"
import { parseDbServerId } from "../utils/server-id.js"
import { appendEvent } from "../modules/events/service/events-service.js"
import {
  hasManagedCommentPrefix,
  managedComment,
} from "../managed-markers.js"

type ServerRow = typeof servers.$inferSelect

interface RosGre {
  ".id"?: string
  name?: string
  "local-address"?: string
  "remote-address"?: string
  "allow-fast-path"?: string
  "clamp-tcp-mss"?: string
  mtu?: string
  "keepalive"?: string
  dscp?: string
  running?: string
  disabled?: string
  comment?: string
}

interface RosFilterRule {
  ".id"?: string
  chain?: string
  rule?: string
  comment?: string
  disabled?: string
}

interface LiveGreTunnel {
  id: string
  name: string
  serverId: string
  localAddress: string
  remoteAddress: string
  localInnerIp: string
  remoteInnerIp: string
  poolId: string
  ipsec: null
  mtu: number
  keepaliveInterval: number
  keepaliveRetries: number
  dscp: "inherit" | number
  clampTcpMss: boolean
  allowFastPath: boolean
  comment: string
  enabled: boolean
  status: "up" | "down" | "degraded"
}

interface ApiFilterRule {
  id: string
  community: string
  communityName?: string
  action?: "route" | "blackhole"
  gateway: string
  gatewayTunnelId: string
  description: string
}

function parseKeepalive(value: string | undefined): { interval: number; retries: number } {
  if (!value || value.toLowerCase() === "none") return { interval: 0, retries: 0 }
  const [intervalRaw, retriesRaw] = value.split(",")
  const interval = Number.parseInt((intervalRaw ?? "").trim(), 10)
  const retries = Number.parseInt((retriesRaw ?? "").trim(), 10)
  return {
    interval: Number.isFinite(interval) ? interval : 0,
    retries: Number.isFinite(retries) ? retries : 0,
  }
}

function parseDscp(value: string | undefined): "inherit" | number {
  if (!value || value === "inherit") return "inherit"
  const n = Number.parseInt(value, 10)
  return Number.isFinite(n) ? n : "inherit"
}

function parseInnerIps(rule: string | undefined): { localInnerIp: string; remoteInnerIp: string } {
  if (!rule) return { localInnerIp: "", remoteInnerIp: "" }
  const local = rule.match(/address\s*=\s*([0-9.]+\/\d+)/)?.[1] ?? ""
  const remote = rule.match(/(?:network|gateway)\s*=\s*([0-9.]+\/\d+)/)?.[1] ?? ""
  return { localInnerIp: local, remoteInnerIp: remote }
}

function parseFilterRule(raw: RosFilterRule): ApiFilterRule[] {
  const text = raw.rule ?? ""
  const out: ApiFilterRule[] = []

  const extractCommunities = (src: string): string[] => {
    const communities = [
      ...src.matchAll(/bgp-communities\.has\("([^"]+)"\)/gi),
      ...src.matchAll(/bgp-communities\s+includes\s+([0-9]+:[0-9]+)/gi),
    ].map(m => m[1]).filter(Boolean)
    return [...new Set(communities)]
  }

  /**
   * Per-community описания: `# 65001:130: Torrents` → { "65001:130": "Torrents" }.
   * Используется при round-trip Router → DB, чтобы не схлопывать описания всех
   * членов сгруппированного блока в одну общую подпись.
   */
  const extractDescMap = (src: string): Map<string, string> => {
    const m = new Map<string, string>()
    for (const cm of src.matchAll(/#\s*(\d+:\d+)\s*:\s*([^\n]+)/g)) {
      m.set(cm[1], cm[2].trim())
    }
    return m
  }

  /** Первый произвольный `#` комментарий — fallback, если нет per-community. */
  const firstInlineComment = (src: string): string =>
    src.match(/#\s*([^\n]+)/)?.[1]?.trim() ?? ""

  const rawComment = raw.comment?.trim() ?? ""

  // Парсим по веткам if/else if, чтобы action применялся к "своим" communities.
  const branches = [...text.matchAll(/(?:if|else\s+if)\s*\(([\s\S]*?)\)\s*\{([\s\S]*?)\}/gi)]
  if (branches.length > 0) {
    for (const b of branches) {
      const condition = b[1] ?? ""
      const body = b[2] ?? ""
      const comms = extractCommunities(condition)
      if (comms.length === 0) continue

      const isBlackhole =
        /set\s+type\s+blackhole/i.test(body) ||
        /set\s+blackhole\s+(?:yes|true)/i.test(body)
      const gwToken = body.match(/set\s+gw(?:ateway)?\s+([^\s;]+)/i)?.[1] ?? ""
      const outIface = body.match(/set\s+out-interface\s+([^\s;]+)/i)?.[1] ?? ""

      const descMap = extractDescMap(body)
      const fallbackInline = firstInlineComment(body)

      for (const community of comms) {
        const perComm = descMap.get(community)?.trim() ?? ""
        out.push({
          id: `${raw[".id"] ?? "live"}-${out.length}`,
          community,
          action: isBlackhole ? "blackhole" : "route",
          gateway: isBlackhole ? "" : gwToken,
          gatewayTunnelId: isBlackhole ? "" : outIface,
          description: perComm || fallbackInline || rawComment,
        })
      }
    }
    if (out.length > 0) return out
  }

  // Fallback для старого/плоского синтаксиса без явных блоков.
  const communities = extractCommunities(text)
  if (communities.length === 0) return []
  const isBlackhole =
    /set\s+type\s+blackhole/i.test(text) ||
    /set\s+blackhole\s+(?:yes|true)/i.test(text)
  const gwToken = text.match(/set\s+gw(?:ateway)?\s+([^\s;]+)/i)?.[1] ?? ""
  const outIface = text.match(/set\s+out-interface\s+([^\s;]+)/i)?.[1] ?? ""
  const flatDescMap = extractDescMap(text)
  const flatFallback = firstInlineComment(text)
  return communities.map((community, idx) => {
    const perComm = flatDescMap.get(community)?.trim() ?? ""
    return {
      id: `${raw[".id"] ?? "live"}-${idx}`,
      community,
      action: isBlackhole ? "blackhole" : "route",
      gateway: isBlackhole ? "" : gwToken,
      gatewayTunnelId: isBlackhole ? "" : outIface,
      description: perComm || flatFallback || rawComment,
    }
  })
}

/** Хоп для BGP filter из префикса рекурсивного статического маршрута (напр. 10.9.9.2/32 → 10.9.9.2) */
function gatewayFromRecursiveDst(dstAddress: string): string {
  return (dstAddress ?? "").trim().split("/")[0]?.trim() ?? ""
}

/**
 * Импорт с роутера даёт только `set gateway` без `set out-interface` — в таком виде не отличить от «поломанного» GRE.
 * Сопоставляем hop с локальной таблицей recursive_routes и восстанавливаем gatewayTunnelId = rec:<id>.
 */
function enrichRulesWithRecursiveGateway(serverId: number, rules: ApiFilterRule[]): ApiFilterRule[] {
  const rows = db
    .select()
    .from(recursiveRoutes)
    .where(and(eq(recursiveRoutes.serverId, serverId), eq(recursiveRoutes.disabled, false)))
    .all()

  return rules.map(rule => {
    if (rule.action === "blackhole") return rule
    const tid = (rule.gatewayTunnelId ?? "").trim()
    if (tid) return rule
    const gw = (rule.gateway ?? "").trim()
    if (!gw) return rule
    const candidates = rows.filter(r => gatewayFromRecursiveDst(r.dstAddress) === gw)
    if (candidates.length === 0) return rule
    const best = candidates.reduce((a, b) => (a.distance <= b.distance ? a : b))
    return {
      ...rule,
      gatewayTunnelId: `rec:${best.id}`,
      gateway: gatewayFromRecursiveDst(best.dstAddress) || gw,
    }
  })
}

async function fetchServerFilters(server: ServerRow) {
  const client = MikrotikClient.fromServer(server)
  const [greRaw, filterRaw] = await Promise.all([
    client.get<RosGre[]>("/interface/gre"),
    client.get<RosFilterRule[]>("/routing/filter/rule"),
  ])

  const tunnels: LiveGreTunnel[] = greRaw.map((g, idx) => {
    const keepalive = parseKeepalive(g["keepalive"])
    const inner = parseInnerIps(g.comment)
    const running = g.running === "true"
    const disabled = g.disabled === "true"

    return {
      id: g.name || g[".id"] || `gre-${server.id}-${idx}`,
      name: g.name || `gre-${idx + 1}`,
      serverId: String(server.id),
      localAddress: g["local-address"] ?? "",
      remoteAddress: g["remote-address"] ?? "",
      localInnerIp: inner.localInnerIp,
      remoteInnerIp: inner.remoteInnerIp,
      poolId: "live",
      ipsec: null,
      mtu: Number.parseInt(g.mtu ?? "1476", 10) || 1476,
      keepaliveInterval: keepalive.interval,
      keepaliveRetries: keepalive.retries,
      dscp: parseDscp(g.dscp),
      clampTcpMss: g["clamp-tcp-mss"] !== "false",
      allowFastPath: g["allow-fast-path"] !== "false",
      comment: g.comment ?? "",
      enabled: !disabled,
      status: disabled ? "down" : (running ? "up" : "degraded"),
    }
  })

  const rulesRaw = filterRaw
    .filter(r => (r.chain ?? "").trim().toLowerCase() === "bgp-in")
    .flatMap(parseFilterRule)

  const rules = enrichRulesWithRecursiveGateway(server.id, rulesRaw)

  return {
    serverId: String(server.id),
    rules,
    tunnels,
  }
}

function toApiRulesets(serverRows: ServerRow[]) {
  const dbRules = db.select().from(filterRules).orderBy(asc(filterRules.serverId), asc(filterRules.sortOrder)).all()
  return serverRows.map(s => ({
    serverId: String(s.id),
    rules: dbRules
      .filter(r => r.serverId === s.id)
      .map((r): ApiFilterRule => ({
        id: String(r.id),
        community: r.community,
        communityName: r.communityName ?? undefined,
        action: r.action,
        gateway: r.gateway,
        gatewayTunnelId: r.gatewayTunnelId,
        description: r.description,
      })),
  }))
}

/** GRE: gatewayTunnelId = имя интерфейса; рекурсивный: rec:<id строки recursive_routes */
function resolveRouteTargets(serverId: number, rule: ApiFilterRule): { gateway: string; outIface: string } {
  if (rule.action === "blackhole") return { gateway: "", outIface: "" }
  const tid = (rule.gatewayTunnelId ?? "").trim()
  if (tid.startsWith("rec:")) {
    const rid = Number.parseInt(tid.slice(4), 10)
    if (!Number.isFinite(rid)) {
      return { gateway: rule.gateway, outIface: "" }
    }
    const row = db
      .select()
      .from(recursiveRoutes)
      .where(and(eq(recursiveRoutes.serverId, serverId), eq(recursiveRoutes.id, rid)))
      .limit(1)
      .all()[0]
    if (!row) return { gateway: rule.gateway, outIface: "" }
    const gw = gatewayFromRecursiveDst(row.dstAddress)
    return { gateway: gw || rule.gateway, outIface: "" }
  }
  return { gateway: rule.gateway, outIface: tid }
}

function normalizeCommunity(c: string): string {
  return (c ?? "").trim()
}

/** Одинаковый эффект на роутере при одинаковой community (blackhole vs gateway + out-interface) */
function ruleEffectSignature(serverId: number, r: ApiFilterRule): string {
  if (r.action === "blackhole") return `bh:${normalizeCommunity(r.community)}`
  const { gateway, outIface } = resolveRouteTargets(serverId, r)
  return `rt:${normalizeCommunity(r.community)}:${gateway}:${outIface}`
}

export type FilterRouterCompareStatus = "synced" | "drift" | "missing"

function compareDbRulesWithRouter(
  serverId: number,
  dbRules: ApiFilterRule[],
  remoteRules: ApiFilterRule[],
): Record<string, FilterRouterCompareStatus> {
  const remoteSigByComm = new Map<string, string>()
  for (const rr of remoteRules) {
    const c = normalizeCommunity(rr.community)
    if (!remoteSigByComm.has(c)) {
      remoteSigByComm.set(c, ruleEffectSignature(serverId, rr))
    }
  }
  const out: Record<string, FilterRouterCompareStatus> = {}
  for (const dr of dbRules) {
    const c = normalizeCommunity(dr.community)
    const sigD = ruleEffectSignature(serverId, dr)
    const sigR = remoteSigByComm.get(c)
    if (sigR === undefined) {
      out[c] = "missing"
    } else if (sigR !== sigD) {
      out[c] = "drift"
    } else {
      out[c] = "synced"
    }
  }
  return out
}

function toRouterRuleBody(serverId: number, rules: ApiFilterRule[]): string {
  if (rules.length === 0) return ""
  // Группируем по эффекту (action + gateway + out-interface). Communities с одним и тем же
  // `set gw` объединяются через `||` в один if-блок — компактнее и ближе к привычному
  // синтаксису bgp-in на MikroTik.
  // RouterOS routing filter parser НЕ поддерживает `else if` (см. error
  // «expected '{' instead of 'if'»), поэтому между группами — независимые `if`-блоки;
  // после `accept;` обработка правила завершается, следующие `if`-ы не запускаются.
  // Эталон тела: `bgp-communities includes <v>`, `set gw <ip>`.
  type Group = {
    isBlackhole: boolean
    gateway: string
    outIface: string
    items: ApiFilterRule[]
  }
  const groups: Group[] = []
  const indexByKey = new Map<string, number>()

  for (const rule of rules) {
    const isBlackhole = rule.action === "blackhole"
    const { gateway, outIface } = isBlackhole
      ? { gateway: "", outIface: "" }
      : resolveRouteTargets(serverId, rule)
    const key = isBlackhole ? "bh" : `rt:${gateway}:${outIface}`
    let idx = indexByKey.get(key)
    if (idx === undefined) {
      idx = groups.length
      indexByKey.set(key, idx)
      groups.push({ isBlackhole, gateway, outIface, items: [] })
    }
    groups[idx].items.push(rule)
  }

  return groups.map(g => {
    const cond = g.items
      .map(r => `bgp-communities includes ${r.community}`)
      .join(" || ")
    // Per-community подпись `# 65001:130: Torrents` — на роутере человеко-читаемо,
    // и parseFilterRule умеет извлечь её обратно в description конкретного правила.
    const commentLines = g.items
      .filter(r => r.description?.trim())
      .map(r => `  # ${r.community}: ${r.description.trim()}`)

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

async function replaceDbRules(serverId: number, rules: ApiFilterRule[]) {
  db.delete(filterRules).where(eq(filterRules.serverId, serverId)).run()
  if (rules.length === 0) return
  const now = new Date().toISOString()
  db.insert(filterRules).values(
    rules.map((r, i) => ({
      serverId,
      sortOrder: i,
      community: r.community,
      communityName: r.communityName ?? null,
      action: r.action ?? "route",
      gateway: r.gateway,
      gatewayTunnelId: r.gatewayTunnelId,
      description: r.description,
      createdAt: now,
      updatedAt: now,
    })),
  ).run()
}

const filtersRoutes: FastifyPluginAsyncZod = async (app) => {
  /** Сравнение правил в БД с живым bgp-in на MikroTik (один запрос API к роутеру) */
  app.get("/filters/router-compare", async (req, reply) => {
    const q = req.query as { serverId?: string | number }
    const serverId = parseDbServerId(q.serverId)
    if (serverId === null) {
      return reply.status(400).send({ error: "serverId is required" })
    }

    const server = db.select().from(servers).where(eq(servers.id, serverId)).limit(1).all()[0]
    if (!server) return reply.status(404).send({ error: "Server not found" })

    try {
      const remote = await fetchServerFilters(server)
      const rows = db
        .select()
        .from(filterRules)
        .where(eq(filterRules.serverId, serverId))
        .orderBy(asc(filterRules.sortOrder))
        .all()

      const dbRules: ApiFilterRule[] = rows.map(r => ({
        id: String(r.id),
        community: r.community,
        communityName: r.communityName ?? undefined,
        action: r.action,
        gateway: r.gateway,
        gatewayTunnelId: r.gatewayTunnelId,
        description: r.description,
      }))

      const byCommunity = compareDbRulesWithRouter(serverId, dbRules, remote.rules)
      return reply.send({ byCommunity })
    } catch (err) {
      app.log.error({ serverId, err: String(err) }, "filters router-compare failed")
      return reply.status(500).send({ error: String(err) })
    }
  })

  /** GRE с роутеров: один сервер (?serverId) или все включённые (без query) — для /gre, карты сети */
  app.get("/filters/gre-tunnels", async (req, reply) => {
    const q = req.query as { serverId?: string | number }
    const sid = parseDbServerId(q.serverId)
    const enabledServers = db.select().from(servers).where(eq(servers.enabled, true)).all()

    if (sid !== null) {
      const server = enabledServers.find(s => s.id === sid)
      if (!server) return reply.status(404).send({ error: "Server not found" })
      try {
        const remote = await fetchServerFilters(server)
        return reply.send({ tunnels: remote.tunnels })
      } catch {
        return reply.send({ tunnels: [] as LiveGreTunnel[] })
      }
    }

    const results = await Promise.all(enabledServers.map(async (server) => {
      try {
        const remote = await fetchServerFilters(server)
        return remote.tunnels
      } catch {
        return [] as LiveGreTunnel[]
      }
    }))
    return reply.send({ tunnels: results.flat() })
  })

  /** Только правила фильтров из БД (без опроса MikroTik за GRE) */
  app.get("/filters/rules", async (_req, reply) => {
    const allServers = db.select().from(servers).where(eq(servers.enabled, true)).all()
    const dbRulesets = toApiRulesets(allServers)

    return reply.send({
      rulesets: dbRulesets,
      greTunnels: [] as LiveGreTunnel[],
    })
  })

  app.put("/filters/rules", async (req, reply) => {
    const body = req.body as { rulesets?: Array<{ serverId: string; rules: ApiFilterRule[] }> }
    const payload = body.rulesets ?? []
    const serverIds = payload.map(r => Number.parseInt(r.serverId, 10)).filter(Number.isFinite)
    if (serverIds.length > 0) {
      db.delete(filterRules).where(inArray(filterRules.serverId, serverIds)).run()
    }
    for (const rs of payload) {
      const sid = Number.parseInt(rs.serverId, 10)
      if (!Number.isFinite(sid)) continue
      await replaceDbRules(sid, rs.rules ?? [])
    }
    return reply.send({ ok: true })
  })

  app.post("/filters/sync/from-router", async (_req, reply) => {
    const body = _req.body as { serverId?: string | number } | undefined
    const rawServerId = body?.serverId
    const serverId = parseDbServerId(rawServerId)
    if (serverId === null) {
      return reply.status(400).send({ error: "serverId is required" })
    }

    const server = db.select().from(servers).where(eq(servers.id, serverId)).limit(1).all()[0]
    if (!server) return reply.status(404).send({ error: "Server not found" })

    try {
      app.log.info({ serverId, host: server.host }, "Filters sync from router started")
      appendEvent({
        level: "info",
        eventType: "filters.sync.from_router.started",
        sourceModule: "filters",
        title: "Синхронизация фильтров запущена",
        message: `${server.name || server.host} → БД`,
        entityType: "server",
        entityId: String(serverId),
      })
      const remote = await fetchServerFilters(server)
      await replaceDbRules(server.id, remote.rules)
      app.log.info({ serverId, totalRules: remote.rules.length }, "Filters sync from router completed")
      appendEvent({
        level: "info",
        eventType: "filters.sync.from_router.done",
        sourceModule: "filters",
        title: "Синхронизация фильтров завершена",
        message: `${server.name || server.host}: ${remote.rules.length} правил`,
        entityType: "server",
        entityId: String(serverId),
      })
      return reply.send({ ok: true, updatedServers: 1, totalRules: remote.rules.length, serverId })
    } catch (err) {
      app.log.error({ serverId, err: String(err) }, "Filters sync from router failed")
      appendEvent({
        level: "critical",
        eventType: "filters.sync.from_router.failed",
        sourceModule: "filters",
        title: "Ошибка синхронизации фильтров",
        message: `${server.name || server.host}: ${String(err)}`,
        entityType: "server",
        entityId: String(serverId),
      })
      return reply.status(500).send({ error: String(err) })
    }
  })

  app.post("/filters/sync/to-router", async (req, reply) => {
    const body = req.body as { serverId?: string | number } | undefined
    const requestedServerId = parseDbServerId(body?.serverId)

    const allServers = db.select().from(servers).where(eq(servers.enabled, true)).all()
    const targetServers = requestedServerId !== null
      ? allServers.filter(s => s.id === requestedServerId)
      : allServers

    if (requestedServerId !== null && targetServers.length === 0) {
      return reply.status(404).send({ error: "Server not found" })
    }

    let updatedServers = 0
    let pushedRules = 0
    const errors: Array<{ serverId: number; error: string }> = []
    appendEvent({
      level: "info",
      eventType: "filters.sync.to_router.started",
      sourceModule: "filters",
      title: "Отправка фильтров на роутеры запущена",
      message: `Целевых серверов: ${targetServers.length}`,
      payload: { requestedServerId },
    })

    for (const server of targetServers) {
      try {
        app.log.info({ serverId: server.id, host: server.host }, "Filters sync to router started")
        const client = MikrotikClient.fromServer(server)
        const existing = await client.get<RosFilterRule[]>("/routing/filter/rule")

        const isInBgpIn = (r: RosFilterRule) =>
          (r.chain ?? "").trim().toLowerCase() === "bgp-in"
        const managedCommentValue = managedComment(server.name || server.host)

        // Уже созданное нами правило — будем PATCH'ить, чтобы сохранить ID/позицию в цепочке.
        const managedRule = existing.find(
          r => isInBgpIn(r) && hasManagedCommentPrefix(r.comment ?? ""),
        )

        // Конфликтующие легаси-правила в bgp-in (без нашего comment, но с bgp-communities) —
        // удаляем после успешного upsert: иначе старое правило с `else { reject; }`
        // отрабатывает первым и перебивает наш upsert.
        const conflictIds = existing
          .filter(r =>
            isInBgpIn(r) &&
            !hasManagedCommentPrefix(r.comment ?? "") &&
            /bgp-communities/i.test(r.rule ?? ""),
          )
          .map(r => r[".id"])
          .filter((id): id is string => Boolean(id))

        const rows = db.select().from(filterRules)
          .where(and(eq(filterRules.serverId, server.id)))
          .orderBy(asc(filterRules.sortOrder))
          .all()

        const rules: ApiFilterRule[] = rows.map(r => ({
          id: String(r.id),
          community: r.community,
          communityName: r.communityName ?? undefined,
          action: r.action,
          gateway: r.gateway,
          gatewayTunnelId: r.gatewayTunnelId,
          description: r.description,
        }))

        // Upsert: PATCH существующего managed-правила или POST /add нового.
        // Если ошибка — конфликтные правила НЕ удаляем (роутер не остаётся с пустым bgp-in).
        // Путь `/routing/filter/rule/add` обязателен: голый POST на коллекцию RouterOS REST
        // трактует как «вызов команды» и отдаёт 400 «no such command».
        // См. https://help.mikrotik.com/docs/spaces/ROS/pages/47579162/REST+API
        if (rules.length > 0) {
          const ruleBody = toRouterRuleBody(server.id, rules)
          if (managedRule && managedRule[".id"]) {
            await client.patch(
              `/routing/filter/rule/${encodeURIComponent(managedRule[".id"])}`,
              {
                chain: "bgp-in",
                comment: managedCommentValue,
                rule: ruleBody,
                disabled: "no",
              },
            )
            app.log.info({ serverId: server.id, id: managedRule[".id"] }, "bgp-in rule updated")
          } else {
            await client.post("/routing/filter/rule/add", {
              chain: "bgp-in",
              comment: managedCommentValue,
              rule: ruleBody,
            })
            app.log.info({ serverId: server.id }, "bgp-in rule created")
          }
          pushedRules += rules.length
        } else if (managedRule && managedRule[".id"]) {
          // В БД нет правил → удаляем наш managed-rule на роутере.
          await client.delete(
            `/routing/filter/rule/${encodeURIComponent(managedRule[".id"])}`,
          )
          app.log.info({ serverId: server.id }, "bgp-in rule removed (no rules in DB)")
        }

        for (const id of conflictIds) {
          await client.delete(`/routing/filter/rule/${encodeURIComponent(id)}`)
        }

        updatedServers += 1
        app.log.info(
          {
            serverId: server.id,
            mode: managedRule ? "patch" : "create",
            conflictsRemoved: conflictIds.length,
            pushed: rules.length,
          },
          "Filters sync to router completed",
        )
      } catch (err) {
        app.log.error({ serverId: server.id, err: String(err) }, "filters sync to-router failed")
        errors.push({ serverId: server.id, error: String(err) })
      }
    }

    appendEvent({
      level: errors.length === 0 ? "info" : "warning",
      eventType: errors.length === 0 ? "filters.sync.to_router.done" : "filters.sync.to_router.partial",
      sourceModule: "filters",
      title: errors.length === 0 ? "Отправка фильтров завершена" : "Отправка фильтров завершена с ошибками",
      message: `Успешно: ${updatedServers}, ошибок: ${errors.length}, правил: ${pushedRules}`,
      payload: {
        updatedServers,
        pushedRules,
        errors,
      },
    })
    return reply.send({
      ok: errors.length === 0,
      updatedServers,
      pushedRules,
      errors,
    })
  })
}

export default filtersRoutes
