import http  from "node:http"
import https from "node:https"
import type { Server } from "../db/schema.js"
import { parseRosDataSizeBytes } from "./ros-metric-parse.js"
import type {
  RosIdentity, RosInterface, RosIpAddress, RosResource,
  RosBgpSession,
  RosOspfNeighbor, RosOspfArea, RosOspfInterfaceTemplate, RosOspfInstance,
  RosBfdSession,
  RosIpRoute, RosFirewallFilter, RosFirewallAddressList, RosLogEntry, RosPingResult,
  FirewallFamily, FirewallTable,
} from "../types/server.js"

const MAX_ROS_BODY_BYTES = 8 * 1024 * 1024

function appendRosBody(body: string, chunk: string, req?: http.ClientRequest): string {
  if (body.length + chunk.length > MAX_ROS_BODY_BYTES) {
    req?.destroy(new Error("RouterOS: ответ больше 8 МиБ"))
    return body
  }
  return body + chunk
}

// ── connection params ─────────────────────────────────────────────────────────

export interface MikrotikConnectParams {
  host:      string
  port:      number
  useSsl:    boolean
  verifySsl: boolean
  username:  string
  password:  string
  apiPath?:  string   // defaults to "/rest"
}

// ── low-level HTTP helpers ────────────────────────────────────────────────────

function rosRequest(
  params: MikrotikConnectParams,
  path: string,
  timeoutMs: number,
): Promise<unknown> {
  return new Promise((resolve, reject) => {
    const basePath  = params.apiPath ?? "/rest"
    const authHeader = "Basic " + Buffer.from(`${params.username}:${params.password}`).toString("base64")

    const options: https.RequestOptions = {
      hostname:           params.host,
      port:               params.port,
      path:               basePath + path,
      method:             "GET",
      headers:            { Authorization: authHeader, "Content-Type": "application/json" },
      rejectUnauthorized: params.useSsl ? params.verifySsl : undefined,
    }

    const lib = params.useSsl ? https : http

    const timer = setTimeout(() => {
      req.destroy(new Error(`Connection to ${params.host}:${params.port} timed out after ${timeoutMs / 1000}s`))
    }, timeoutMs)

    const req = lib.request(options, (res) => {
      let body = ""
      res.setEncoding("utf8")
      res.on("data", (chunk: string) => { body = appendRosBody(body, chunk, req) })
      res.on("end", () => {
        clearTimeout(timer)
        if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
          reject(new MikrotikError(res.statusCode ?? 0, path, body))
          return
        }
        try {
          resolve(JSON.parse(body))
        } catch {
          reject(new Error(`Invalid JSON from RouterOS: ${body.slice(0, 200)}`))
        }
      })
    })

    req.on("error", (err) => {
      clearTimeout(timer)
      reject(err)
    })

    req.end()
  })
}

function rosPost(
  params: MikrotikConnectParams,
  path: string,
  body: Record<string, string>,
  timeoutMs: number,
  externalSignal?: AbortSignal,
): Promise<unknown> {
  return new Promise((resolve, reject) => {
    const basePath   = params.apiPath ?? "/rest"
    const authHeader = "Basic " + Buffer.from(`${params.username}:${params.password}`).toString("base64")
    const payload    = JSON.stringify(body)

    const options: https.RequestOptions = {
      hostname:           params.host,
      port:               params.port,
      path:               basePath + path,
      method:             "POST",
      headers:            {
        Authorization:    authHeader,
        "Content-Type":   "application/json",
        "Content-Length": Buffer.byteLength(payload),
      },
      rejectUnauthorized: params.useSsl ? params.verifySsl : undefined,
    }

    const lib = params.useSsl ? https : http

    let req: ReturnType<typeof lib.request> | undefined

    let settled = false
    const settle = (fn: () => void) => {
      if (settled) return
      settled = true
      if (req) {
        req.setTimeout(0)
        req.removeListener("timeout", onSocketTimeout)
      }
      externalSignal?.removeEventListener("abort", onExternalAbort)
      fn()
    }

    const onExternalAbort = () => {
      req?.destroy(new DOMException("Aborted", "AbortError"))
    }

    const onSocketTimeout = () => {
      req?.destroy()
      settle(() => reject(new Error(`Connection to ${params.host}:${params.port} timed out after ${timeoutMs / 1000}s`)))
    }

    if (externalSignal?.aborted) {
      settle(() => reject(new DOMException("Aborted", "AbortError")))
      return
    }
    externalSignal?.addEventListener("abort", onExternalAbort)

    req = lib.request(options, (res) => {
      let buf = ""
      res.setEncoding("utf8")
      res.on("data", (chunk: string) => { buf = appendRosBody(buf, chunk, req) })
      res.on("end", () => {
        settle(() => {
          if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
            reject(new MikrotikError(res.statusCode ?? 0, path, buf)); return
          }
          try { resolve(JSON.parse(buf)) } catch {
            reject(new Error(`Invalid JSON from RouterOS: ${buf.slice(0, 200)}`))
          }
        })
      })
    })
    req.setTimeout(timeoutMs)
    req.on("timeout", onSocketTimeout)
    req.on("error", (err) => {
      settle(() => reject(err))
    })
    req.write(payload)
    req.end()
  })
}

function rosPut(
  params: MikrotikConnectParams,
  path: string,
  body: Record<string, string>,
  timeoutMs: number,
): Promise<unknown> {
  return new Promise((resolve, reject) => {
    const basePath = params.apiPath ?? "/rest"
    const authHeader = "Basic " + Buffer.from(`${params.username}:${params.password}`).toString("base64")
    const payload = JSON.stringify(body)

    const options: https.RequestOptions = {
      hostname: params.host,
      port: params.port,
      path: basePath + path,
      method: "PUT",
      headers: {
        Authorization: authHeader,
        "Content-Type": "application/json",
        "Content-Length": Buffer.byteLength(payload),
      },
      rejectUnauthorized: params.useSsl ? params.verifySsl : undefined,
    }

    const lib = params.useSsl ? https : http
    const timer = setTimeout(() => {
      req.destroy(new Error(`Connection to ${params.host}:${params.port} timed out after ${timeoutMs / 1000}s`))
    }, timeoutMs)

    const req = lib.request(options, (res) => {
      let buf = ""
      res.setEncoding("utf8")
      res.on("data", (chunk: string) => { buf = appendRosBody(buf, chunk, req) })
      res.on("end", () => {
        clearTimeout(timer)
        if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
          reject(new MikrotikError(res.statusCode ?? 0, path, buf))
          return
        }
        try {
          resolve(buf.trim() ? JSON.parse(buf) : {})
        } catch {
          reject(new Error(`Invalid JSON from RouterOS: ${buf.slice(0, 200)}`))
        }
      })
    })

    req.on("error", (err) => {
      clearTimeout(timer)
      reject(err)
    })
    req.write(payload)
    req.end()
  })
}

function rosDelete(
  params: MikrotikConnectParams,
  path: string,
  timeoutMs: number,
): Promise<void> {
  return new Promise((resolve, reject) => {
    const basePath   = params.apiPath ?? "/rest"
    const authHeader = "Basic " + Buffer.from(`${params.username}:${params.password}`).toString("base64")

    const options: https.RequestOptions = {
      hostname:           params.host,
      port:               params.port,
      path:               basePath + path,
      method:             "DELETE",
      headers:            { Authorization: authHeader, "Content-Type": "application/json" },
      rejectUnauthorized: params.useSsl ? params.verifySsl : undefined,
    }

    const lib = params.useSsl ? https : http
    const timer = setTimeout(() => {
      req.destroy(new Error(`Connection to ${params.host}:${params.port} timed out after ${timeoutMs / 1000}s`))
    }, timeoutMs)

    const req = lib.request(options, (res) => {
      let body = ""
      res.setEncoding("utf8")
      res.on("data", (chunk: string) => { body = appendRosBody(body, chunk, req) })
      res.on("end", () => {
        clearTimeout(timer)
        if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
          reject(new MikrotikError(res.statusCode ?? 0, path, body)); return
        }
        resolve()
      })
    })

    req.on("error", (err) => {
      clearTimeout(timer)
      reject(err)
    })
    req.end()
  })
}

function rosPatch(
  params: MikrotikConnectParams,
  path: string,
  body: Record<string, string>,
  timeoutMs: number,
): Promise<unknown> {
  return new Promise((resolve, reject) => {
    const basePath = params.apiPath ?? "/rest"
    const authHeader = "Basic " + Buffer.from(`${params.username}:${params.password}`).toString("base64")
    const payload = JSON.stringify(body)

    const options: https.RequestOptions = {
      hostname: params.host,
      port: params.port,
      path: basePath + path,
      method: "PATCH",
      headers: {
        Authorization: authHeader,
        "Content-Type": "application/json",
        "Content-Length": Buffer.byteLength(payload),
      },
      rejectUnauthorized: params.useSsl ? params.verifySsl : undefined,
    }

    const lib = params.useSsl ? https : http
    const timer = setTimeout(() => {
      req.destroy(new Error(`Connection to ${params.host}:${params.port} timed out after ${timeoutMs / 1000}s`))
    }, timeoutMs)

    const req = lib.request(options, (res) => {
      let buf = ""
      res.setEncoding("utf8")
      res.on("data", (chunk: string) => { buf = appendRosBody(buf, chunk, req) })
      res.on("end", () => {
        clearTimeout(timer)
        if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
          reject(new MikrotikError(res.statusCode ?? 0, path, buf)); return
        }
        if (buf.trim() === "") {
          resolve({})
          return
        }
        try { resolve(JSON.parse(buf)) } catch { resolve({}) }
      })
    })

    req.on("error", (err) => {
      clearTimeout(timer)
      reject(err)
    })
    req.write(payload)
    req.end()
  })
}

/**
 * RouterOS REST для одиночных меню (`/system/resource`, `/system/identity`) отдаёт JSON-массив из одного объекта `[{...}]`,
 * а не сам объект. Без распаковки поля читаются с массива → всегда undefined и в БД уходят нули при «успехе».
 * @see https://help.mikrotik.com/docs/spaces/ROS/pages/47579162/REST+API
 */
function unwrapRestSingle<T extends object>(raw: unknown): T {
  if (Array.isArray(raw)) {
    const first = raw[0]
    if (first && typeof first === "object") return first as T
    throw new Error("RouterOS REST: пустой массив вместо записи")
  }
  if (raw && typeof raw === "object") return raw as T
  throw new Error("RouterOS REST: неожиданное тело ответа")
}

function routerFileBasename(fileName: string): string {
  return fileName.replace(/^\/+/, "").split("/").pop() ?? fileName
}

function matchesUploadedFile(entryName: string, requested: string): boolean {
  const base = routerFileBasename(requested)
  return entryName === requested
    || entryName === base
    || entryName === `flash/${base}`
    || entryName.endsWith(`/${base}`)
}

export function firewallRestPath(
  family: FirewallFamily,
  table: FirewallTable | "address-list",
): string {
  const root = family === "ip6" ? "/ipv6/firewall" : "/ip/firewall"
  return `${root}/${table}`
}

export function encodeRosId(rosId: string): string {
  const id = rosId.startsWith("*") ? rosId : `*${rosId.replace(/^\*/, "")}`
  return encodeURIComponent(id)
}

// ── MikrotikClient ─────────────────────────────────────────────────────────────

export class MikrotikClient {
  constructor(private readonly params: MikrotikConnectParams) {}

  /** Convenience factory from a DB Server row */
  static fromServer(server: Server): MikrotikClient {
    return new MikrotikClient({
      host:      server.host,
      port:      server.port,
      useSsl:    server.useSsl,
      verifySsl: server.verifySsl,
      username:  server.username,
      password:  server.password,
    })
  }

  async get<T>(path: string, timeoutMs = 10_000): Promise<T> {
    return rosRequest(this.params, path, timeoutMs) as Promise<T>
  }

  async post<T>(path: string, body: Record<string, string>, timeoutMs = 15_000, signal?: AbortSignal): Promise<T> {
    return rosPost(this.params, path, body, timeoutMs, signal) as Promise<T>
  }

  async put<T>(path: string, body: Record<string, string>, timeoutMs = 15_000): Promise<T> {
    return rosPut(this.params, path, body, timeoutMs) as Promise<T>
  }

  async delete(path: string, timeoutMs = 10_000): Promise<void> {
    return rosDelete(this.params, path, timeoutMs)
  }

  async patch<T>(path: string, body: Record<string, string>, timeoutMs = 15_000): Promise<T> {
    return rosPatch(this.params, path, body, timeoutMs) as Promise<T>
  }

  // ── typed helpers ──────────────────────────────────────────────────────────

  async getIdentity(): Promise<RosIdentity> {
    const raw = await this.get<unknown>("/system/identity")
    return unwrapRestSingle<RosIdentity>(raw)
  }

  async getResource(): Promise<RosResource> {
    const raw = await this.get<unknown>("/system/resource")
    if (Array.isArray(raw)) {
      if (raw.length === 0) throw new Error("RouterOS REST: пустой массив вместо записи /system/resource")
      if (raw.length === 1) return unwrapRestSingle<RosResource>(raw[0])
      /** Несколько объектов: взять строку с total-memory (агрегат), а не дочернюю запись с «чужим» uptime. */
      const objs = raw.filter((x): x is Record<string, unknown> => x != null && typeof x === "object" && !Array.isArray(x))
      /** На части железа несколько строк; «первая с непустым total-memory» может парситься в 0 — берём запись с максимальным RAM. */
      let best: Record<string, unknown> | undefined
      let bestBytes = -1
      for (const o of objs) {
        const tm = parseRosDataSizeBytes(o["total-memory"])
        const fm = parseRosDataSizeBytes(o["free-memory"])
        const score = tm > 0 ? tm : fm
        if (score > bestBytes) {
          bestBytes = score
          best = o
        }
      }
      const main = objs.find((o) => {
        const tm = o["total-memory"]
        return tm != null && String(tm).trim() !== ""
      })
      return ((bestBytes > 0 ? best : undefined) ?? main ?? objs[0]) as unknown as RosResource
    }
    return unwrapRestSingle<RosResource>(raw)
  }

  async getInterfaces(): Promise<RosInterface[]> {
    return this.get<RosInterface[]>("/interface")
  }

  async getIpAddresses(): Promise<RosIpAddress[]> {
    return this.get<RosIpAddress[]>("/ip/address")
  }

  async getBgpSessions(): Promise<RosBgpSession[]> {
    return this.get<RosBgpSession[]>("/routing/bgp/session")
  }

  /** Returns the raw (un-typed) BGP session objects — used for debugging */
  async getBgpSessionsRaw(): Promise<unknown[]> {
    return this.get<unknown[]>("/routing/bgp/session")
  }

  // ── OSPF ──────────────────────────────────────────────────────────────────

  async getOspfNeighbors(): Promise<RosOspfNeighbor[]> {
    return this.get<RosOspfNeighbor[]>("/routing/ospf/neighbor")
  }

  async getOspfAreas(): Promise<RosOspfArea[]> {
    return this.get<RosOspfArea[]>("/routing/ospf/area")
  }

  async getOspfInterfaceTemplates(): Promise<RosOspfInterfaceTemplate[]> {
    return this.get<RosOspfInterfaceTemplate[]>("/routing/ospf/interface-template")
  }

  async getOspfInstances(): Promise<RosOspfInstance[]> {
    return this.get<RosOspfInstance[]>("/routing/ospf/instance")
  }

  async getBfdSessions(): Promise<RosBfdSession[]> {
    return this.get<RosBfdSession[]>("/routing/bfd/session")
  }

  async setOspfInterfaceTemplateCost(templateId: string, cost: number): Promise<void> {
    const safeId = templateId.startsWith("*") ? templateId : `*${templateId.replace(/^\*/, "")}`
    await this.patch(`/routing/ospf/interface-template/${encodeURIComponent(safeId)}`, { cost: String(cost) })
  }

  // ── extra endpoints for exec route ────────────────────────────────────────

  async getIpRoutes(): Promise<RosIpRoute[]> {
    return this.get<RosIpRoute[]>("/ip/route")
  }

  async getFirewallFilters(): Promise<RosFirewallFilter[]> {
    return this.getFirewallRules("ip", "filter")
  }

  async getFirewallRules(family: FirewallFamily, table: FirewallTable): Promise<RosFirewallFilter[]> {
    return this.get<RosFirewallFilter[]>(firewallRestPath(family, table))
  }

  async getFirewallAddressList(family: FirewallFamily): Promise<RosFirewallAddressList[]> {
    return this.get<RosFirewallAddressList[]>(firewallRestPath(family, "address-list"))
  }

  async getLogs(limit = 50): Promise<RosLogEntry[]> {
    return this.get<RosLogEntry[]>(`/log?limit=${limit}`)
  }

  async ping(
    address: string,
    count = 4,
    interfaceName?: string,
    options?: { interval?: string },
  ): Promise<RosPingResult[]> {
    const body: Record<string, string> = {
      address,
      count:    String(count),
    }
    if (options?.interval && options.interval.trim()) body.interval = options.interval.trim()
    else body.interval = "0.2s"
    if (interfaceName && interfaceName.trim()) body.interface = interfaceName.trim()
    return this.post<RosPingResult[]>("/tool/ping", body, 20_000)
  }

  async bandwidthTest(params: {
    address: string
    user: string
    password: string
    protocol?: "tcp" | "udp"
    direction?: "transmit" | "receive" | "both"
    durationSec?: number
  }): Promise<Array<Record<string, string>>> {
    const body: Record<string, string> = {
      address: params.address,
      user: params.user,
      password: params.password,
      protocol: params.protocol ?? "tcp",
      direction: params.direction ?? "both",
      duration: `${Math.max(3, params.durationSec ?? 10)}s`,
    }
    return this.post<Array<Record<string, string>>>("/tool/bandwidth-test", body, 30_000)
  }

  async getCertificates(): Promise<Array<Record<string, string | undefined>>> {
    const raw = await this.get<unknown>("/certificate")
    if (!Array.isArray(raw)) return []
    return raw.filter((row): row is Record<string, string | undefined> => row != null && typeof row === "object")
  }

  async listFiles(): Promise<Array<{ name: string }>> {
    const raw = await this.get<unknown>("/file")
    if (!Array.isArray(raw)) return []
    return raw
      .filter((row): row is Record<string, unknown> => row != null && typeof row === "object")
      .map((row) => ({ name: String(row.name ?? "") }))
      .filter((row) => row.name.length > 0)
  }

  private async resolveUploadedFileName(requested: string): Promise<string> {
    const files = await this.listFiles()
    const match = files.find((file) => matchesUploadedFile(file.name, requested))
    if (!match) {
      throw new Error(`Файл ${routerFileBasename(requested)} не найден на RouterOS после загрузки`)
    }
    return match.name
  }

  async uploadTextFile(fileName: string, contents: string, timeoutMs = 30_000): Promise<string> {
    const normalized = routerFileBasename(fileName)
    const payload = contents.endsWith("\n") ? contents : `${contents}\n`
    const candidates = [`flash/${normalized}`, normalized]
    let lastError: unknown

    for (const name of candidates) {
      try {
        await this.put("/file", { name, contents: payload }, timeoutMs)
        return await this.resolveUploadedFileName(name)
      } catch (error) {
        lastError = error
      }

      try {
        await this.post("/file/add", { name, contents: payload, type: "file" }, timeoutMs)
        return await this.resolveUploadedFileName(name)
      } catch (error) {
        lastError = error
      }
    }

    throw lastError instanceof Error
      ? lastError
      : new Error(`Не удалось загрузить файл ${normalized} на RouterOS`)
  }

  async importCertificate(params: {
    fileName: string
    name: string
    trusted?: boolean
    trustStore?: string
    passphrase?: string
  }): Promise<unknown> {
    const body: Record<string, string> = {
      "file-name": params.fileName,
      name: params.name,
      trusted: params.trusted === false ? "no" : "yes",
      "trust-store": params.trustStore?.trim() || "www,api",
    }
    if (params.passphrase?.trim()) body.passphrase = params.passphrase.trim()
    return this.post("/certificate/import", body, 60_000)
  }

  async applyCertificateToServices(certName: string): Promise<void> {
    const body = {
      disabled: "no",
      certificate: certName,
    }
    for (const serviceName of ["www-ssl", "api-ssl"]) {
      await this.patchIpService(serviceName, body)
    }
  }

  private async patchIpService(serviceName: string, body: Record<string, string>): Promise<void> {
    const pathByName = `/ip/service/${encodeURIComponent(serviceName)}`
    try {
      await this.patch(pathByName, body, 30_000)
      return
    } catch {
      const services = await this.get<Array<Record<string, string | undefined>>>("/ip/service")
      const row = services.find((service) => String(service.name ?? "") === serviceName)
      const id = row?.[".id"]
      if (!id) throw new Error(`Сервис RouterOS ${serviceName} не найден`)
      await this.patch(`/ip/service/${encodeURIComponent(id)}`, body, 30_000)
    }
  }

  async exportConfigScript(): Promise<string> {
    const raw = await this.post<unknown>("/console/export", {}, 30_000)

    const asText = (v: unknown): string | null => {
      if (typeof v === "string") return v.trim().length > 0 ? v : null
      if (Array.isArray(v)) {
        const parts = v
          .map((item) => asText(item))
          .filter((s): s is string => typeof s === "string" && s.length > 0)
        return parts.length > 0 ? parts.join("\n") : null
      }
      if (v && typeof v === "object") {
        const rec = v as Record<string, unknown>
        const direct =
          asText(rec.output) ??
          asText(rec.stdout) ??
          asText(rec.data) ??
          asText(rec.ret) ??
          asText(rec["!re"])
        if (direct) return direct
        const serialized = JSON.stringify(rec, null, 2)
        return serialized.length > 2 ? serialized : null
      }
      return null
    }

    const txt = asText(raw)
    if (txt && txt.trim().length > 0) return txt

    // Fallback: на части RouterOS /console/export возвращает пустое тело.
    // Тогда строим .rsc-скрипт из основных read-only разделов REST.
    return this.buildSyntheticExportScript()
  }

  private async buildSyntheticExportScript(): Promise<string> {
    const now = new Date().toISOString()
    const lines: string[] = [
      "# synthetic export generated by MikrotikManager",
      `# generated-at: ${now}`,
      "",
    ]

    const identity = await this.getIdentity().catch(() => null)
    if (identity?.name) {
      lines.push("/system identity")
      lines.push(`set name="${identity.name.replace(/"/g, "\\\"")}"`)
      lines.push("")
    }

    const interfaces = await this.getInterfaces().catch(() => [])
    if (interfaces.length > 0) {
      lines.push("/interface")
      for (const i of interfaces) {
        if (!i.name) continue
        const mtu = i["actual-mtu"] ?? i.mtu
        const parts = [
          `name="${String(i.name).replace(/"/g, "\\\"")}"`,
          mtu ? `mtu=${mtu}` : null,
          i.disabled === "true" ? "disabled=yes" : "disabled=no",
        ].filter((v): v is string => typeof v === "string")
        lines.push(`:put "interface ${parts.join(" ")}"`)
      }
      lines.push("")
    }

    const addrs = await this.getIpAddresses().catch(() => [])
    if (addrs.length > 0) {
      lines.push("/ip address")
      for (const a of addrs) {
        if (!a.address || !a.interface) continue
        const comment = a.comment ? ` comment="${String(a.comment).replace(/"/g, "\\\"")}"` : ""
        lines.push(`add address=${a.address} interface="${String(a.interface).replace(/"/g, "\\\"")}"${comment}`)
      }
      lines.push("")
    }

    const routes = await this.getIpRoutes().catch(() => [])
    if (routes.length > 0) {
      lines.push("/ip route")
      for (const r of routes) {
        const dst = r["dst-address"]
        const gw = r["gateway"]
        if (!dst || !gw) continue
        const distance = r.distance ? ` distance=${r.distance}` : ""
        lines.push(`add dst-address=${dst} gateway=${gw}${distance}`)
      }
      lines.push("")
    }

    const firewall = await this.getFirewallFilters().catch(() => [])
    if (firewall.length > 0) {
      lines.push("/ip firewall filter")
      for (const f of firewall) {
        if (!f.chain || !f.action) continue
        const parts = [`chain=${f.chain}`, `action=${f.action}`]
        if (f.protocol) parts.push(`protocol=${f.protocol}`)
        if (f["src-address"]) parts.push(`src-address=${f["src-address"]}`)
        if (f["dst-address"]) parts.push(`dst-address=${f["dst-address"]}`)
        if (f["dst-port"]) parts.push(`dst-port=${f["dst-port"]}`)
        if (f["src-port"]) parts.push(`src-port=${f["src-port"]}`)
        if (f.disabled === "true") parts.push("disabled=yes")
        lines.push(`add ${parts.join(" ")}`)
      }
      lines.push("")
    }

    if (lines.length <= 3) {
      throw new Error("RouterOS вернул пустой export и fallback-данные недоступны")
    }
    return lines.join("\n")
  }
}

// ── Error type ─────────────────────────────────────────────────────────────────

export class MikrotikError extends Error {
  constructor(
    public readonly statusCode: number,
    public readonly path: string,
    public readonly body: string,
  ) {
    super(`RouterOS API error ${statusCode} on ${path}: ${body}`)
    this.name = "MikrotikError"
  }
}
