import type { GreTunnel, Server } from "@/lib/data"
import type { WanJhEdge } from "@/lib/network-map-layout"
import {
  formatGreOuterForDisplay,
  normalizeGreEndpointAddr,
  wanUplinkIpMatchesGreOuter,
} from "@/lib/gre-endpoint-resolve"

/** Поля speed-пробы из мониторинга (Мониторинг → скорость), нужные для GRE на карте. */
export interface GreSpeedProbeSnapshot {
  id: string
  srcServerId: string
  dstServerId: string
  enabled?: boolean
  srcInterface?: string
  dstInterface?: string
  lastTxAvgMbps?: number | null
  lastRxAvgMbps?: number | null
  lastPingRttMs?: number | null
}

export type GreTunnelProbeEdge = {
  tunnel: GreTunnel
  fromServer: Server
  toServer: Server
}

/** Неориентированная пара узлов: один ключ для HR→MSK и MSK→HR (две строки GRE в БД). */
export function canonicalGreServerPairKey(a: Pick<Server, "id">, b: Pick<Server, "id">): string {
  const x = String(a.id)
  const y = String(b.id)
  return x <= y ? `${x}\t${y}` : `${y}\t${x}`
}

function normIface(i?: string | null): string {
  return (i ?? "").trim().toLowerCase()
}

/** WAN-интерфейс узла, чей внешний IP совпадает с GRE outer (endpoint каталога). */
export function ifaceNameForGreOuterIp(
  server: Server,
  outerIp: string,
  resolvedIpv4ByHost?: ReadonlyMap<string, string>,
): string | null {
  const r = normalizeGreEndpointAddr(outerIp)
  if (!r || r === "0.0.0.0") return null
  for (const w of server.wanUplinks ?? []) {
    if (wanUplinkIpMatchesGreOuter(w.ip, outerIp, resolvedIpv4ByHost)) return w.iface.trim()
  }
  return null
}

/** Короткая подпись для бейджа: внешние IP и имена WAN при наличии в каталоге. */
export function greOuterSummaryLine(
  tunnel: GreTunnel,
  fromServer: Server,
  toServer: Server,
  resolvedIpv4ByHost?: ReadonlyMap<string, string>,
): string {
  const lo = formatGreOuterForDisplay(tunnel.localAddress, resolvedIpv4ByHost) || "auto"
  const ro = formatGreOuterForDisplay(tunnel.remoteAddress, resolvedIpv4ByHost) || "?"
  const li = ifaceNameForGreOuterIp(fromServer, tunnel.localAddress, resolvedIpv4ByHost)
  const ri = ifaceNameForGreOuterIp(toServer, tunnel.remoteAddress, resolvedIpv4ByHost)
  const ipPart = `${lo}→${ro}`
  if (li || ri) return `${ipPart} · ${li ?? "?"}→${ri ?? "?"}`
  return ipPart
}

function scoreProbeForGreTunnel(
  p: GreSpeedProbeSnapshot,
  tunnel: GreTunnel,
  fromServer: Server,
  toServer: Server,
  resolvedIpv4ByHost?: ReadonlyMap<string, string>,
): number {
  const pf = String(p.srcServerId)
  const pt = String(p.dstServerId)
  const fid = String(fromServer.id)
  const tid = String(toServer.id)
  const forward = pf === fid && pt === tid
  const backward = pf === tid && pt === fid
  if (!forward && !backward) return -1

  const si = normIface(p.srcInterface)
  const di = normIface(p.dstInterface)
  let s = 0

  if (forward) {
    const srcW = ifaceNameForGreOuterIp(fromServer, tunnel.localAddress, resolvedIpv4ByHost)
    const dstW = ifaceNameForGreOuterIp(toServer, tunnel.remoteAddress, resolvedIpv4ByHost)
    const srcM = !!(srcW && si === normIface(srcW))
    const dstM = !!(dstW && di === normIface(dstW))
    if (srcM) s += 52
    if (dstM) s += 52
    if (srcM && dstM) s += 34
    if (!srcW && dstW && di === normIface(dstW)) s += 45
    if (!dstW && srcW && si === normIface(srcW)) s += 45
  } else {
    const srcW = ifaceNameForGreOuterIp(toServer, tunnel.remoteAddress, resolvedIpv4ByHost)
    const dstW = ifaceNameForGreOuterIp(fromServer, tunnel.localAddress, resolvedIpv4ByHost)
    const srcM = !!(srcW && si === normIface(srcW))
    const dstM = !!(dstW && di === normIface(dstW))
    if (srcM) s += 52
    if (dstM) s += 52
    if (srcM && dstM) s += 34
    if (!srcW && dstW && di === normIface(dstW)) s += 45
    if (!dstW && srcW && si === normIface(srcW)) s += 45
  }

  if (p.lastPingRttMs != null || p.lastTxAvgMbps != null || p.lastRxAvgMbps != null)
    s += 4
  return s
}

function probesForServerPair(
  probes: GreSpeedProbeSnapshot[],
  fromServer: Server,
  toServer: Server,
): GreSpeedProbeSnapshot[] {
  const fid = String(fromServer.id)
  const tid = String(toServer.id)
  return probes.filter((p) => {
    if (p.enabled === false) return false
    const pf = String(p.srcServerId)
    const pt = String(p.dstServerId)
    return (pf === fid && pt === tid) || (pf === tid && pt === fid)
  })
}

/**
 * Для всех GRE на карте назначает speed-пробы без повторного использования одной пробы
 * на два разных туннеля между одной и той же парой узлов (RT / MTS и т.п.).
 *
 * Пара узлов **неориентированная**: туннель в БД на HR→MSK и зеркальная запись MSK→HR
 * попадают в одну группу — иначе обе стороны независимо выбирают одни и те же пробы
 * и бейджи дублируются на разных линиях карты.
 */
export function assignSpeedProbesToGreTunnels(
  edges: GreTunnelProbeEdge[],
  probes: GreSpeedProbeSnapshot[],
  resolvedIpv4ByHost?: ReadonlyMap<string, string>,
): ReadonlyMap<string, GreSpeedProbeSnapshot | undefined> {
  const result = new Map<string, GreSpeedProbeSnapshot | undefined>()

  const byPair = new Map<string, GreTunnelProbeEdge[]>()
  for (const e of edges) {
    const k = canonicalGreServerPairKey(e.fromServer, e.toServer)
    const arr = byPair.get(k) ?? []
    arr.push(e)
    byPair.set(k, arr)
  }

  for (const group of byPair.values()) {
    const assigned = assignProbesWithinPair(group, probes, resolvedIpv4ByHost)
    for (const [tunnelId, sp] of assigned) result.set(tunnelId, sp)
  }

  return result
}

function assignProbesWithinPair(
  edges: GreTunnelProbeEdge[],
  allProbes: GreSpeedProbeSnapshot[],
  resolved?: ReadonlyMap<string, string>,
): Map<string, GreSpeedProbeSnapshot | undefined> {
  const out = new Map<string, GreSpeedProbeSnapshot | undefined>()
  if (edges.length === 0) return out

  const E = [...edges].sort((a, b) => a.tunnel.id.localeCompare(b.tunnel.id))
  const pairProbes = probesForServerPair(allProbes, E[0].fromServer, E[0].toServer)

  if (E.length === 1) {
    out.set(
      E[0].tunnel.id,
      findSpeedProbeForGreEdge(E[0].tunnel, E[0].fromServer, E[0].toServer, allProbes, resolved),
    )
    return out
  }

  const n = E.length
  const m = pairProbes.length

  if (m === 0) {
    for (const e of E) out.set(e.tunnel.id, undefined)
    return out
  }

  // Полное назначение: каждому туннелю своя проба, максимум суммы score (RT и MTS не делят одну пробу).
  let bestSum = -Infinity
  let bestAssign: Map<string, GreSpeedProbeSnapshot> | null = null

  function dfs(i: number, used: Set<string>, cur: Map<string, GreSpeedProbeSnapshot>) {
    if (i === n) {
      let sum = 0
      for (const e of E) {
        const p = cur.get(e.tunnel.id)
        if (!p) return
        sum += scoreProbeForGreTunnel(p, e.tunnel, e.fromServer, e.toServer, resolved)
      }
      if (sum > bestSum) {
        bestSum = sum
        bestAssign = new Map(cur)
      }
      return
    }
    const edge = E[i]!
    for (const p of pairProbes) {
      if (used.has(p.id)) continue
      if (scoreProbeForGreTunnel(p, edge.tunnel, edge.fromServer, edge.toServer, resolved) < 0)
        continue
      used.add(p.id)
      cur.set(edge.tunnel.id, p)
      dfs(i + 1, used, cur)
      cur.delete(edge.tunnel.id)
      used.delete(p.id)
    }
  }

  // Перебор инъекций только при небольшом n — иначе сразу жадный алгоритм.
  if (n <= 7 && n <= m) dfs(0, new Set(), new Map())

  /** TS не видит присваивание из замыкания dfs — явное приведение. */
  const inject = bestAssign as Map<string, GreSpeedProbeSnapshot> | null
  if (inject !== null && inject.size === n) {
    for (const ed of E) out.set(ed.tunnel.id, inject.get(ed.tunnel.id))
    return out
  }

  // Недостаточно проб или нет полного матчинга — жадно по убыванию score, без повторов.
  const used = new Set<string>()
  const remaining = new Set(E.map((e) => e.tunnel.id))
  while (remaining.size > 0) {
    let pickEdge: GreTunnelProbeEdge | undefined
    let pickProbe: GreSpeedProbeSnapshot | undefined
    let pickScore = -Infinity

    for (const tid of remaining) {
      const e = E.find((x) => x.tunnel.id === tid)!
      for (const p of pairProbes) {
        if (used.has(p.id)) continue
        const sc = scoreProbeForGreTunnel(p, e.tunnel, e.fromServer, e.toServer, resolved)
        if (sc < 0) continue
        if (
          sc > pickScore ||
          (sc === pickScore && pickProbe != null && p.id.localeCompare(pickProbe.id) < 0)
        ) {
          pickScore = sc
          pickEdge = e
          pickProbe = p
        }
      }
    }

    if (!pickEdge || !pickProbe) {
      for (const tid of remaining) out.set(tid, undefined)
      break
    }

    out.set(pickEdge.tunnel.id, pickProbe)
    used.add(pickProbe.id)
    remaining.delete(pickEdge.tunnel.id)
  }

  return out
}

/**
 * Подбирает speed-пробу для одного GRE (без учёта соседних туннелей той же пары узлов).
 * Для нескольких туннелей между теми же серверами используйте assignSpeedProbesToGreTunnels.
 */
export function findSpeedProbeForGreEdge(
  tunnel: GreTunnel,
  fromServer: Server,
  toServer: Server,
  probes: GreSpeedProbeSnapshot[],
  resolvedIpv4ByHost?: ReadonlyMap<string, string>,
): GreSpeedProbeSnapshot | undefined {
  const cand = probes.filter(
    (p) =>
      p.enabled !== false &&
      scoreProbeForGreTunnel(p, tunnel, fromServer, toServer, resolvedIpv4ByHost) >= 0,
  )
  if (cand.length === 0) return undefined

  let best = cand[0]!
  let bestScore = scoreProbeForGreTunnel(best, tunnel, fromServer, toServer, resolvedIpv4ByHost)
  for (let i = 1; i < cand.length; i++) {
    const p = cand[i]!
    const sc = scoreProbeForGreTunnel(p, tunnel, fromServer, toServer, resolvedIpv4ByHost)
    if (sc > bestScore) {
      best = p
      bestScore = sc
      continue
    }
    if (sc === bestScore) {
      const hasData = (x: GreSpeedProbeSnapshot) =>
        x.lastPingRttMs != null || x.lastTxAvgMbps != null || x.lastRxAvgMbps != null
      if (hasData(p) && !hasData(best)) best = p
    }
  }
  return best
}

export function mergeGreMetricsWithSpeedProbe(
  sp: GreSpeedProbeSnapshot | undefined,
  fallback: { pingMs: number | null; dlMbps: number | null; ulMbps: number | null },
): {
  pingMs: number | null
  dlMbps: number | null
  ulMbps: number | null
  hasSpeedMonitor: boolean
} {
  const hasSpeedMonitor =
    sp != null &&
    (sp.lastPingRttMs != null || sp.lastTxAvgMbps != null || sp.lastRxAvgMbps != null)
  const pingMs = sp?.lastPingRttMs ?? fallback.pingMs
  const dlMbps =
    sp?.lastTxAvgMbps != null ? Math.round(sp.lastTxAvgMbps) : fallback.dlMbps
  const ulMbps =
    sp?.lastRxAvgMbps != null ? Math.round(sp.lastRxAvgMbps) : fallback.ulMbps
  return { pingMs, dlMbps, ulMbps, hasSpeedMonitor }
}

// ── WAN satellite → JumpHost: speed-пробы с src = Home Router и выбранным WAN-iface ──

export function wanJhEdgeMapKey(edge: Pick<WanJhEdge, "homeId" | "wanIdx" | "jhId">): string {
  return `${edge.homeId}\t${edge.wanIdx}\t${edge.jhId}`
}

function scoreSpeedProbeForWanJhEdge(
  p: GreSpeedProbeSnapshot,
  edge: Pick<WanJhEdge, "homeId" | "wanIdx" | "jhId">,
  home: Server,
): number {
  if (String(p.srcServerId) !== edge.homeId || String(p.dstServerId) !== edge.jhId) return -1
  if (p.enabled === false) return -1
  const wanIface = normIface(home.wanUplinks?.[edge.wanIdx]?.iface)
  const si = normIface(p.srcInterface)
  if (si && wanIface && si !== wanIface) return -1
  let s = 0
  if (si && wanIface && si === wanIface) s += 100
  else if (!si && wanIface) s += 35
  else if (!wanIface && si) s += 15
  else if (!si && !wanIface) s += 5
  if (p.lastPingRttMs != null || p.lastTxAvgMbps != null || p.lastRxAvgMbps != null) s += 6
  return s
}

function probesHomeToJh(allProbes: GreSpeedProbeSnapshot[], homeId: string, jhId: string): GreSpeedProbeSnapshot[] {
  return allProbes.filter((p) => {
    if (p.enabled === false) return false
    return String(p.srcServerId) === homeId && String(p.dstServerId) === jhId
  })
}

function assignSpeedProbesWithinWanJhGroup(
  edges: WanJhEdge[],
  home: Server,
  allProbes: GreSpeedProbeSnapshot[],
): Map<string, GreSpeedProbeSnapshot | undefined> {
  const out = new Map<string, GreSpeedProbeSnapshot | undefined>()
  if (edges.length === 0) return out
  const E = [...edges].sort((a, b) => a.wanIdx - b.wanIdx || a.jhId.localeCompare(b.jhId))
  const pairProbes = probesHomeToJh(allProbes, E[0].homeId, E[0].jhId)

  if (E.length === 1) {
    let best: GreSpeedProbeSnapshot | undefined
    let bestS = -1
    for (const p of pairProbes) {
      const sc = scoreSpeedProbeForWanJhEdge(p, E[0], home)
      if (sc > bestS) {
        bestS = sc
        best = p
      }
    }
    out.set(wanJhEdgeMapKey(E[0]), best != null && bestS >= 0 ? best : undefined)
    return out
  }

  const n = E.length
  const m = pairProbes.length
  if (m === 0) {
    for (const e of E) out.set(wanJhEdgeMapKey(e), undefined)
    return out
  }

  let bestSum = -Infinity
  let bestAssign: Map<string, GreSpeedProbeSnapshot> | null = null

  function dfs(i: number, used: Set<string>, cur: Map<string, GreSpeedProbeSnapshot>) {
    if (i === n) {
      let sum = 0
      for (const e of E) {
        const p = cur.get(wanJhEdgeMapKey(e))
        if (!p) return
        sum += scoreSpeedProbeForWanJhEdge(p, e, home)
      }
      if (sum > bestSum) {
        bestSum = sum
        bestAssign = new Map(cur)
      }
      return
    }
    const edge = E[i]!
    const slotKey = wanJhEdgeMapKey(edge)
    for (const p of pairProbes) {
      if (used.has(p.id)) continue
      if (scoreSpeedProbeForWanJhEdge(p, edge, home) < 0) continue
      used.add(p.id)
      cur.set(slotKey, p)
      dfs(i + 1, used, cur)
      cur.delete(slotKey)
      used.delete(p.id)
    }
  }

  if (n <= 7 && n <= m) dfs(0, new Set(), new Map())

  const inject = bestAssign as Map<string, GreSpeedProbeSnapshot> | null
  if (inject !== null && inject.size === n) {
    for (const ed of E) out.set(wanJhEdgeMapKey(ed), inject.get(wanJhEdgeMapKey(ed)))
    return out
  }

  const used = new Set<string>()
  const remaining = new Set(E.map((e) => wanJhEdgeMapKey(e)))
  while (remaining.size > 0) {
    let pickEdge: WanJhEdge | undefined
    let pickProbe: GreSpeedProbeSnapshot | undefined
    let pickScore = -Infinity
    for (const key of remaining) {
      const e = E.find((x) => wanJhEdgeMapKey(x) === key)!
      for (const p of pairProbes) {
        if (used.has(p.id)) continue
        const sc = scoreSpeedProbeForWanJhEdge(p, e, home)
        if (sc < 0) continue
        if (
          sc > pickScore ||
          (sc === pickScore && pickProbe != null && p.id.localeCompare(pickProbe.id) < 0)
        ) {
          pickScore = sc
          pickEdge = e
          pickProbe = p
        }
      }
    }
    if (!pickEdge || !pickProbe) {
      for (const key of remaining) out.set(key, undefined)
      break
    }
    out.set(wanJhEdgeMapKey(pickEdge), pickProbe)
    used.add(pickProbe.id)
    remaining.delete(wanJhEdgeMapKey(pickEdge))
  }
  return out
}

/**
 * Назначает speed-пробы сегментам WAN→JH (Home → спутник → JH): по паре серверов и iface источника,
 * без повторного использования одной пробы на два WAN одного home→jh (RT / MTS).
 */
export function assignSpeedProbesToWanJhEdges(
  edges: WanJhEdge[],
  servers: Server[],
  probes: GreSpeedProbeSnapshot[],
): ReadonlyMap<string, GreSpeedProbeSnapshot | undefined> {
  const result = new Map<string, GreSpeedProbeSnapshot | undefined>()
  const homeById = new Map(servers.filter((s) => s.type === "home-router").map((s) => [s.id, s] as const))

  const byPair = new Map<string, WanJhEdge[]>()
  for (const e of edges) {
    const k = `${e.homeId}\t${e.jhId}`
    const arr = byPair.get(k) ?? []
    arr.push(e)
    byPair.set(k, arr)
  }

  for (const group of byPair.values()) {
    const home = homeById.get(group[0].homeId)
    if (!home) {
      for (const e of group) result.set(wanJhEdgeMapKey(e), undefined)
      continue
    }
    const assigned = assignSpeedProbesWithinWanJhGroup(group, home, probes)
    for (const [k, sp] of assigned) result.set(k, sp)
  }

  return result
}
