import { and, desc, eq }            from "drizzle-orm"
import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
import { db }                          from "../db/index.js"
import { serverSnapshots, servers, trafficSamples, uptimeSpeedProbes } from "../db/schema.js"
import { MikrotikClient }              from "../services/mikrotik.js"
import { ServerIdParamSchema, type ServerIdParams }         from "../types/server.js"
import type {
  RosOspfNeighbor, RosOspfArea, RosOspfInterfaceTemplate, RosOspfInstance,
  RosBfdSession,
  OspfNeighborRead, OspfInterfaceRead, OspfInstanceRead, BfdSessionRead,
} from "../types/server.js"
import { z } from "zod"

type ServerRow = typeof servers.$inferSelect
type TrafficSampleRow = typeof trafficSamples.$inferSelect
type SpeedProbeRow = typeof uptimeSpeedProbes.$inferSelect

const OspfOptimizeBodySchema = z.object({
  pingWeight: z.number().int().min(0).max(100).default(60),
})

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

/** Parse RouterOS duration string like "1h30m17s", "40s", "10m" → seconds */
function parseDuration(s: string | undefined): number {
  if (!s) return 0
  let total = 0
  for (const m of s.matchAll(/(\d+)(w|d|h|m(?!s)|s)/g)) {
    const n = parseInt(m[1])
    switch (m[2]) {
      case "w": total += n * 604800; break
      case "d": total += n * 86400;  break
      case "h": total += n * 3600;   break
      case "m": total += n * 60;     break
      case "s": total += n;          break
    }
  }
  return total
}

/** Build area-name → area-id lookup from a list of raw areas */
function buildAreaMap(areas: RosOspfArea[]): Map<string, string> {
  const map = new Map<string, string>()
  for (const a of areas) {
    map.set(a.name, a["area-id"] ?? "0.0.0.0")
  }
  return map
}

/** Parse "200ms" | "1s" | "500ms" → milliseconds */
function parseMs(s: string | undefined): number {
  if (!s) return 0
  const ms = s.match(/^(\d+)ms$/)
  if (ms) return parseInt(ms[1])
  const sec = s.match(/^(\d+)s$/)
  if (sec) return parseInt(sec[1]) * 1000
  return parseInt(s) || 0
}

/** Normalize BFD state: "up" → "Up", "down" → "Down", etc. */
function normalizeBfdState(state: string | undefined): string {
  const map: Record<string, string> = {
    up: "Up", down: "Down", init: "Init", admindown: "AdminDown",
  }
  return map[(state ?? "").toLowerCase()] ?? (state ?? "Down")
}

/** Extract IP and interface from RouterOS address format "10.0.0.1%eth0" */
function parseAddrIface(addr: string): { ip: string; iface: string } {
  const [ip, iface = ""] = addr.split("%")
  return { ip, iface }
}

/** Fetch all OSPF + BFD data for one server */
async function fetchServerOspf(server: ServerRow) {
  const client = MikrotikClient.fromServer(server)
  const [neighbors, areas, ifaceTemplates, instances, bfdSessions] = await Promise.all([
    client.getOspfNeighbors(),
    client.getOspfAreas(),
    client.getOspfInterfaceTemplates(),
    client.getOspfInstances(),
    client.getBfdSessions().catch(() => [] as RosBfdSession[]),  // BFD is optional
  ])
  return { neighbors, areas, ifaceTemplates, instances, bfdSessions }
}

// ── BFD parser ────────────────────────────────────────────────────────────────

function parseBfdSessions(server: ServerRow, raw: RosBfdSession[]): BfdSessionRead[] {
  return raw.map((s, idx) => {
    const local  = parseAddrIface(s["local-address"])
    const remote = parseAddrIface(s["remote-address"])
    // Prefer interface from local-address; fallback to remote-address suffix
    const iface  = local.iface || remote.iface

    return {
      id:           s[".id"] ?? String(idx),
      serverId:     server.id,
      serverName:   server.name || server.host,
      serverSite:   server.site,
      serverCountry: server.country,
      localAddr:    local.ip,
      remoteAddr:   remote.ip,
      interface:    iface,
      state:        normalizeBfdState(s["state"]),
      uptime:       s["uptime"] ?? null,
      multihop:     s["multihop"] === "true",
      multiplier:   parseInt(s["multiplier"] ?? "3") || 3,
      txInterval:   parseMs(s["actual-tx-interval"] ?? s["desired-tx-interval"]),
      rxInterval:   parseMs(s["required-min-rx"]),
      holdTime:     parseMs(s["hold-time"]),
      packetsRx:    parseInt(s["packets-rx"] ?? "0") || 0,
      packetsTx:    parseInt(s["packets-tx"] ?? "0") || 0,
      stateChanges: parseInt(s["state-changes"] ?? "0") || 0,
    }
  })
}

// ── parsers ───────────────────────────────────────────────────────────────────

function parseNeighbors(
  server: ServerRow,
  neighbors: RosOspfNeighbor[],
  areaMap: Map<string, string>,
): OspfNeighborRead[] {
  return neighbors.map((n, idx) => ({
    id:           n[".id"] ?? String(idx),
    serverId:     server.id,
    serverName:   server.name || server.host,
    serverSite:   server.site,
    serverCountry: server.country,
    address:      n.address,
    routerId:     n["router-id"],
    instance:     n.instance,
    area:         n.area,
    areaId:       areaMap.get(n.area) ?? n.area,
    interface:    n.interface,
    state:        n.state,
    uptime:       n.adjacency ?? null,
    stateChanges: parseInt(n["state-changes"] ?? "0") || 0,
    priority:     parseInt(n.priority ?? "1") || 1,
  }))
}

function parseInterfaces(
  server: ServerRow,
  templates: RosOspfInterfaceTemplate[],
  areas: RosOspfArea[],
  instances: RosOspfInstance[],
  areaMap: Map<string, string>,
): OspfInterfaceRead[] {
  // Build instance-id → instance name map (for interface-template instance-id field)
  const instanceIdMap = new Map<string, string>()
  instances.forEach((inst, i) => { instanceIdMap.set(String(i), inst.name) })

  return templates.map((t, idx) => {
    // Resolve interface name: may be a "*ID" reference (RouterOS internal ID)
    const ifaceName = (t.interfaces ?? "").startsWith("*")
      ? `(ref ${t.interfaces})`
      : (t.interfaces ?? "—")

    return {
      id:           t[".id"] ?? String(idx),
      serverId:     server.id,
      serverName:   server.name || server.host,
      serverSite:   server.site,
      serverCountry: server.country,
      instance:     instanceIdMap.get(t["instance-id"] ?? "") ?? t["instance-id"] ?? "",
      area:         t.area,
      areaId:       areaMap.get(t.area) ?? t.area,
      interface:    ifaceName,
      cost:         parseInt(t.cost ?? "10") || 10,
      type:         t.type ?? "broadcast",
      disabled:     t.disabled === "true",
      inactive:     t.inactive === "true",
      priority:     parseInt(t.priority ?? "1") || 1,
      helloInterval: parseDuration(t["hello-interval"]),
      deadInterval:  parseDuration(t["dead-interval"]),
      useBfd:       t["use-bfd"] === "true",
    }
  })
}

function parseInstances(
  server: ServerRow,
  instances: RosOspfInstance[],
): OspfInstanceRead[] {
  return instances.map((inst, idx) => ({
    id:           inst[".id"] ?? String(idx),
    serverId:     server.id,
    serverName:   server.name || server.host,
    serverSite:   server.site,
    serverCountry: server.country,
    name:         inst.name,
    routerId:     inst["router-id"],
    version:      parseInt(inst.version ?? "2") || 2,
    disabled:     inst.disabled === "true",
    inactive:     inst.inactive === "true",
    redistribute: inst.redistribute ?? "",
  }))
}

function calcRouteScore(pingMs: number, dlMbps: number, ulMbps: number, pingWeight: number) {
  const pingScore = Math.max(0, 100 - pingMs * 0.6)
  const speedScore = Math.min(100, (dlMbps + ulMbps) / 18)
  const w = pingWeight / 100
  return Math.round(w * pingScore + (1 - w) * speedScore)
}

function getLatestSnapshotLatencyMs(serverId: number): number {
  const latest = db
    .select()
    .from(serverSnapshots)
    .where(eq(serverSnapshots.serverId, serverId))
    .orderBy(desc(serverSnapshots.polledAt))
    .limit(1)
    .all()
  return latest.length > 0 ? Math.max(1, Math.round(latest[0].latencyMs ?? 100)) : 100
}

function latestTrafficByInterface(serverId: number): Map<string, TrafficSampleRow> {
  const rows = db
    .select()
    .from(trafficSamples)
    .where(eq(trafficSamples.serverId, serverId))
    .all()
  const map = new Map<string, TrafficSampleRow>()
  for (const row of rows) {
    const prev = map.get(row.interfaceName)
    if (!prev || row.sampledAt > prev.sampledAt) map.set(row.interfaceName, row)
  }
  return map
}

function normalizeSpeedMbps(row: TrafficSampleRow | undefined): { dlMbps: number; ulMbps: number } {
  if (!row) return { dlMbps: 10, ulMbps: 10 }
  const dl = Math.max(1, Math.round((row.rxBps ?? 0) / 1_000_000))
  const ul = Math.max(1, Math.round((row.txBps ?? 0) / 1_000_000))
  return { dlMbps: dl, ulMbps: ul }
}

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

function parsePingTimeMs(raw: string | undefined): number | null {
  if (!raw) return null
  const s = String(raw).trim().toLowerCase().replace(",", ".")
  const us = s.match(/^(\d+(?:\.\d+)?)\s*us$/)
  if (us) return Number.parseFloat(us[1]) / 1000
  const ms = s.match(/^(\d+(?:\.\d+)?)\s*ms$/)
  if (ms) return Number.parseFloat(ms[1])
  const sec = s.match(/^(\d+(?:\.\d+)?)\s*s$/)
  if (sec) return Number.parseFloat(sec[1]) * 1000
  const clock = s.match(/^(\d+):(\d+):(\d+(?:\.\d+)?)$/)
  if (clock) {
    const h = Number.parseFloat(clock[1])
    const m = Number.parseFloat(clock[2])
    const sc = Number.parseFloat(clock[3])
    return (h * 3600 + m * 60 + sc) * 1000
  }
  return null
}

function avgPingMsFromResults(rows: Array<{ time?: string; status?: string }>): number | null {
  const ok = rows.filter((r) => r.time && String(r.status ?? "").toLowerCase() !== "timeout")
  const vals = ok
    .map((r) => parsePingTimeMs(r.time))
    .filter((v): v is number => v != null && Number.isFinite(v))
  if (!vals.length) return null
  return Math.max(1, Math.round(vals.reduce((a, b) => a + b, 0) / vals.length))
}

async function measurePingByInterface(
  client: MikrotikClient,
  ifaceName: string,
  targetAddr: string | undefined,
  fallbackMs: number,
): Promise<number> {
  if (!targetAddr) return fallbackMs
  try {
    const rows = await client.ping(targetAddr, 2, ifaceName, { interval: "0.2s" })
    return avgPingMsFromResults(rows) ?? fallbackMs
  } catch {
    return fallbackMs
  }
}

type OspfRankedInterface = {
  id: string
  interface: string
  currentCost: number
  pingMs: number
  dlMbps: number
  ulMbps: number
  score: number
  optimalCost: number
}

async function buildOspfOptimizationPlan(server: ServerRow, pingWeight: number): Promise<OspfRankedInterface[]> {
  const { ifaceTemplates, areas, instances, neighbors } = await fetchServerOspf(server)
  const areaMap = buildAreaMap(areas)
  const parsed = parseInterfaces(server, ifaceTemplates, areas, instances, areaMap)
  const editable = parsed.filter((i) => !i.disabled && !isRefInterfaceName(i.interface))
  const localLatencyMs = getLatestSnapshotLatencyMs(server.id)
  const latestTraffic = latestTrafficByInterface(server.id)

  // 1) Сопоставляем remote OSPF router-id -> сервер из каталога (чтобы взять унифицированный ping как в карте /route-optimizer).
  const neededRouterIds = new Set(neighbors.map((n) => String(n["router-id"] ?? "")).filter((x) => x.length > 0))

  async function buildRouterIdToServerMap(needed: Set<string>): Promise<Map<string, ServerRow["id"]>> {
    const enabledServers = db.select().from(servers).where(eq(servers.enabled, true)).all()
    const out = new Map<string, ServerRow["id"]>()
    const neededLeft = new Set(needed)

    for (const s of enabledServers) {
      if (neededLeft.size === 0) break
      try {
        const c = MikrotikClient.fromServer(s)
        const inst = await c.getOspfInstances()
        for (const i of inst) {
          const rid = String(i["router-id"] ?? "").trim()
          if (!rid) continue
          if (neededLeft.has(rid)) {
            out.set(rid, s.id)
            neededLeft.delete(rid)
          }
        }
      } catch {
        // ignore per-server errors, preview should still work
      }
    }
    return out
  }

  const routerIdToServerId = await buildRouterIdToServerMap(neededRouterIds)

  // 2) Сначала выбираем remote serverId для каждого OSPF интерфейса (по neighbor router-id).
  function pickBestNeighborForInterface(iface: string) {
    const cand = neighbors.filter((n) => n.interface === iface)
    if (!cand.length) return null
    const rank = (st: string) => (st === "Full" ? 100 : st === "2-Way" ? 60 : 30)
    return cand.sort((a, b) => rank(b.state) - rank(a.state))[0] ?? null
  }

  const remoteServerIds = new Set<number>()
  const remoteForIface = new Map<string, number>()
  for (const iface of editable) {
    const n = pickBestNeighborForInterface(iface.interface)
    if (!n) continue
    const remoteServerId = routerIdToServerId.get(String(n["router-id"] ?? "").trim())
    if (remoteServerId != null) {
      remoteServerIds.add(remoteServerId)
      remoteForIface.set(iface.interface, remoteServerId)
    }
  }

  // 3) Предгружаем uptimeSpeedProbes: оттуда берём и ping, и скорость (единый источник как у карты).
  const allSourceProbes = db
    .select()
    .from(uptimeSpeedProbes)
    .where(eq(uptimeSpeedProbes.srcServerId, server.id))
    .all()
    .filter((r: SpeedProbeRow) => r.enabled !== false)
  const speedProbesByDest = new Map<number, SpeedProbeRow[]>()
  for (const p of allSourceProbes) {
    const arr = speedProbesByDest.get(p.dstServerId) ?? []
    arr.push(p)
    speedProbesByDest.set(p.dstServerId, arr)
  }

  function normIface(s: string) {
    return s.trim().toLowerCase()
  }

  function pickBestSpeedProbe(probes: SpeedProbeRow[], ifaceName: string): SpeedProbeRow | undefined {
    if (probes.length === 0) return undefined
    const scoreProbeForIface = (p: SpeedProbeRow, targetIface: string): number => {
      const got = normIface(p.srcInterface)
      const target = normIface(targetIface)
      // Сверяемся с /gre: если у пробы есть srcInterface и у OSPF iface есть имя,
      // допускаем только точное совпадение интерфейса.
      if (got && target && got !== target) return -1
      const ifaceScore =
        got && target && got === target ? 100
          : (!got && target) ? 35
            : (!target && got) ? 15
              : 5
      const freshness =
        p.lastPingAt ? Math.max(0, 20 - Math.floor((Date.now() - Date.parse(p.lastPingAt)) / (60 * 60 * 1000))) : 0
      return ifaceScore
        + (p.lastPingRttMs != null ? 25 : 0)
        + (p.lastTxAvgMbps != null ? 12 : 0)
        + (p.lastRxAvgMbps != null ? 12 : 0)
        + freshness
    }

    let best: SpeedProbeRow | undefined
    let bestScore = -1
    for (const p of probes) {
      const s = scoreProbeForIface(p, ifaceName)
      if (s > bestScore) {
        bestScore = s
        best = p
      }
    }
    return best
  }

  function assignProbesUniquely(
    ifaceNames: string[],
    probes: SpeedProbeRow[],
  ): Map<string, SpeedProbeRow> {
    const assigned = new Map<string, SpeedProbeRow>()
    if (ifaceNames.length === 0 || probes.length === 0) return assigned

    const scoreProbeForIface = (p: SpeedProbeRow, ifaceName: string): number => {
      const got = normIface(p.srcInterface)
      const target = normIface(ifaceName)
      if (got && target && got !== target) return -1
      const ifaceScore =
        got && target && got === target ? 100
          : (!got && target) ? 35
            : (!target && got) ? 15
              : 5
      const freshness =
        p.lastPingAt ? Math.max(0, 20 - Math.floor((Date.now() - Date.parse(p.lastPingAt)) / (60 * 60 * 1000))) : 0
      return ifaceScore
        + (p.lastPingRttMs != null ? 25 : 0)
        + (p.lastTxAvgMbps != null ? 12 : 0)
        + (p.lastRxAvgMbps != null ? 12 : 0)
        + freshness
    }

    const leftIfaces = new Set(ifaceNames)
    const usedProbeIds = new Set<string>()

    while (leftIfaces.size > 0) {
      let bestIface: string | null = null
      let bestProbe: SpeedProbeRow | null = null
      let bestScore = -1

      for (const iface of leftIfaces) {
        for (const p of probes) {
          if (usedProbeIds.has(p.id)) continue
          const s = scoreProbeForIface(p, iface)
          if (s > bestScore) {
            bestScore = s
            bestIface = iface
            bestProbe = p
          }
        }
      }

      if (!bestIface || !bestProbe) break
      assigned.set(bestIface, bestProbe)
      leftIfaces.delete(bestIface)
      usedProbeIds.add(bestProbe.id)
    }

    // Если проб меньше чем интерфейсов — добираем лучшими совпадениями (reuse допустим).
    for (const iface of leftIfaces) {
      const fallback = pickBestSpeedProbe(probes, iface)
      if (fallback) assigned.set(iface, fallback)
    }

    return assigned
  }

  const assignedProbeByIface = new Map<string, SpeedProbeRow>()
  const ifacesByDst = new Map<number | null, string[]>()
  for (const iface of editable) {
    const dst = remoteForIface.get(iface.interface) ?? null
    const arr = ifacesByDst.get(dst) ?? []
    arr.push(iface.interface)
    ifacesByDst.set(dst, arr)
  }
  for (const [dst, ifaceList] of ifacesByDst) {
    const probesByDst = dst != null ? speedProbesByDest.get(dst) ?? [] : []
    const pool = probesByDst.length > 0 ? probesByDst : allSourceProbes
    const assigned = assignProbesUniquely(ifaceList, pool)
    for (const [iface, probe] of assigned.entries()) assignedProbeByIface.set(iface, probe)
  }

  return editable
    .map((iface) => {
      const t = latestTraffic.get(iface.interface)
      const speed = normalizeSpeedMbps(t)

      const dstServerId = remoteForIface.get(iface.interface)
      const bestProbe = assignedProbeByIface.get(iface.interface)

      const pingMs =
        bestProbe?.lastPingRttMs != null
          ? Math.max(1, Math.round(bestProbe.lastPingRttMs))
          : dstServerId != null
            ? Math.min(995, Math.round(localLatencyMs + getLatestSnapshotLatencyMs(dstServerId)))
            : localLatencyMs

      const dlMbps =
        bestProbe?.lastTxAvgMbps != null
          ? Math.max(1, Math.round(bestProbe.lastTxAvgMbps))
          : speed.dlMbps

      const ulMbps =
        bestProbe?.lastRxAvgMbps != null
          ? Math.max(1, Math.round(bestProbe.lastRxAvgMbps))
          : speed.ulMbps

      return {
        id: iface.id,
        interface: iface.interface,
        currentCost: iface.cost,
        pingMs,
        dlMbps,
        ulMbps,
        score: calcRouteScore(pingMs, dlMbps, ulMbps, pingWeight),
        optimalCost: 0,
      }
    })
    .sort((a, b) => b.score - a.score || a.interface.localeCompare(b.interface))
    .map((row, idx) => ({ ...row, optimalCost: (idx + 1) * 10 }))
}

// ── route plugin ──────────────────────────────────────────────────────────────

const ospfRoutes: FastifyPluginAsyncZod = async (app) => {

  // GET /api/ospf/neighbors — aggregate OSPF neighbors from ALL enabled servers
  app.get("/ospf/neighbors", async (_req, reply) => {
    const allServers = db.select().from(servers).where(eq(servers.enabled, true)).all()

    const results = await Promise.all(
      allServers.map(async (server) => {
        try {
          const { neighbors, areas } = await fetchServerOspf(server)
          const areaMap = buildAreaMap(areas)
          return parseNeighbors(server, neighbors, areaMap)
        } catch {
          return []
        }
      }),
    )

    return reply.send(results.flat())
  })

  // GET /api/ospf/interfaces — aggregate OSPF interface templates from ALL enabled servers
  app.get("/ospf/interfaces", async (_req, reply) => {
    const allServers = db.select().from(servers).where(eq(servers.enabled, true)).all()

    const results = await Promise.all(
      allServers.map(async (server) => {
        try {
          const { ifaceTemplates, areas, instances } = await fetchServerOspf(server)
          const areaMap = buildAreaMap(areas)
          return parseInterfaces(server, ifaceTemplates, areas, instances, areaMap)
        } catch {
          return []
        }
      }),
    )

    return reply.send(results.flat())
  })

  // GET /api/ospf/instances — aggregate OSPF instances from ALL enabled servers
  app.get("/ospf/instances", async (_req, reply) => {
    const allServers = db.select().from(servers).where(eq(servers.enabled, true)).all()

    const results = await Promise.all(
      allServers.map(async (server) => {
        try {
          const { instances } = await fetchServerOspf(server)
          return parseInstances(server, instances)
        } catch {
          return []
        }
      }),
    )

    return reply.send(results.flat())
  })

  // GET /api/ospf/all — single round-trip: neighbors + interfaces + instances + BFD
  app.get("/ospf/all", async (_req, reply) => {
    const allServers = db.select().from(servers).where(eq(servers.enabled, true)).all()

    const perServer = await Promise.all(
      allServers.map(async (server) => {
        try {
          const { neighbors, areas, ifaceTemplates, instances, bfdSessions } = await fetchServerOspf(server)
          const areaMap = buildAreaMap(areas)
          return {
            neighbors:   parseNeighbors(server, neighbors, areaMap),
            interfaces:  parseInterfaces(server, ifaceTemplates, areas, instances, areaMap),
            instances:   parseInstances(server, instances),
            bfdSessions: parseBfdSessions(server, bfdSessions),
          }
        } catch {
          return { neighbors: [], interfaces: [], instances: [], bfdSessions: [] }
        }
      }),
    )

    return reply.send({
      neighbors:   perServer.flatMap(r => r.neighbors),
      interfaces:  perServer.flatMap(r => r.interfaces),
      instances:   perServer.flatMap(r => r.instances),
      bfdSessions: perServer.flatMap(r => r.bfdSessions),
    })
  })

  // GET /api/bfd/sessions — BFD sessions only (for direct access)
  app.get("/bfd/sessions", async (_req, reply) => {
    const allServers = db.select().from(servers).where(eq(servers.enabled, true)).all()
    const results = await Promise.all(
      allServers.map(async (server) => {
        try {
          const client = MikrotikClient.fromServer(server)
          const raw    = await client.getBfdSessions()
          return parseBfdSessions(server, raw)
        } catch {
          return []
        }
      }),
    )
    return reply.send(results.flat())
  })

  // GET /api/servers/:id/ospf — single server OSPF + BFD data
  app.get("/servers/:id/ospf", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
    const params = req.params as ServerIdParams
    const server = db
      .select().from(servers)
      .where(eq(servers.id, params.id))
      .limit(1).all()[0]

    if (!server) return reply.status(404).send({ error: "Server not found" })

    try {
      const { neighbors, areas, ifaceTemplates, instances, bfdSessions } = await fetchServerOspf(server)
      const areaMap = buildAreaMap(areas)
      return reply.send({
        neighbors:   parseNeighbors(server, neighbors, areaMap),
        interfaces:  parseInterfaces(server, ifaceTemplates, areas, instances, areaMap),
        instances:   parseInstances(server, instances),
        bfdSessions: parseBfdSessions(server, bfdSessions),
        areas:       areas.map(a => ({ name: a.name, areaId: a["area-id"] ?? "0.0.0.0", type: a.type, disabled: a.disabled === "true", inactive: a.inactive === "true", instance: a.instance })),
      })
    } catch (err) {
      return reply.status(500).send({ error: String(err) })
    }
  })

  // POST /api/servers/:id/ospf/optimize — compute + apply optimal OSPF cost for active templates
  app.post(
    "/servers/:id/ospf/optimize/preview",
    { schema: { params: ServerIdParamSchema, body: OspfOptimizeBodySchema } },
    async (req, reply) => {
      const params = req.params as ServerIdParams
      const server = db
        .select().from(servers)
        .where(eq(servers.id, params.id))
        .limit(1).all()[0]

      if (!server) return reply.status(404).send({ error: "Server not found" })

      try {
        const pingWeight = req.body.pingWeight
        const ranked = await buildOspfOptimizationPlan(server, pingWeight)
        const changed = ranked.filter((r) => r.currentCost !== r.optimalCost)
        return reply.send({
          serverId: server.id,
          serverName: server.name || server.host,
          pingWeight,
          interfacesTotal: ranked.length,
          changedCount: changed.length,
          unchangedCount: ranked.length - changed.length,
          interfaces: ranked,
          changes: changed,
        })
      } catch (err) {
        return reply.status(500).send({ error: String(err) })
      }
    },
  )

  app.post(
    "/servers/:id/ospf/optimize",
    { schema: { params: ServerIdParamSchema, body: OspfOptimizeBodySchema } },
    async (req, reply) => {
      const params = req.params as ServerIdParams
      const server = db
        .select().from(servers)
        .where(eq(servers.id, params.id))
        .limit(1).all()[0]

      if (!server) return reply.status(404).send({ error: "Server not found" })

      try {
        const pingWeight = req.body.pingWeight
        const ranked = await buildOspfOptimizationPlan(server, pingWeight)

        const client = MikrotikClient.fromServer(server)
        const changed = ranked.filter((r) => r.currentCost !== r.optimalCost)
        const applied: Array<{ interface: string; from: number; to: number }> = []
        for (const item of changed) {
          await client.setOspfInterfaceTemplateCost(item.id, item.optimalCost)
          applied.push({ interface: item.interface, from: item.currentCost, to: item.optimalCost })
        }

        return reply.send({
          serverId: server.id,
          serverName: server.name || server.host,
          pingWeight,
          optimizedCount: changed.length,
          applied,
          interfaces: ranked,
        })
      } catch (err) {
        return reply.status(500).send({ error: String(err) })
      }
    },
  )
}

export default ospfRoutes
