import { db, sqliteDatabase } from "../db/index.js"
import { appUsers, servers, userInterfaceBindings } from "../db/schema.js"
import { mapRosInterfaceType } from "../modules/users/iface-type.js"
import type { PlaneTopology } from "./traffic-flow-planes.js"

export interface FlowClientBinding {
  userId: string
  login: string
  name: string
  serverId: number
  interfaceName: string
}

export interface FlowEnNode {
  id: number
  name: string
  hosts: string[]
}

export interface FlowTopology {
  clientIfaces: Map<number, Set<string>>
  clientByIface: Map<string, FlowClientBinding>
  enNodes: FlowEnNode[]
  enHosts: Set<string>
  jhHosts: Set<string>
  wanIfaces: Map<number, Set<string>>
  plane: PlaneTopology
}

let seeded: FlowTopology | null = null

function parseWanUplinks(raw: string): Array<{ iface?: string; ip?: string }> {
  try {
    const parsed = JSON.parse(raw || "[]") as unknown
    return Array.isArray(parsed) ? parsed as Array<{ iface?: string; ip?: string }> : []
  } catch {
    return []
  }
}

function ifaceKey(serverId: number, name: string): string {
  return `${serverId}|${name}`
}

export function loadFlowTopology(): FlowTopology {
  if (seeded) return seeded
  const serverRows = db.select().from(servers).all()
  const users = db.select().from(appUsers).all()
  const binds = db.select().from(userInterfaceBindings).all()
  const loginById = new Map(users.map((u) => [u.id, u]))
  const clientIfaces = new Map<number, Set<string>>()
  const clientByIface = new Map<string, FlowClientBinding>()
  const allClientNames = new Set<string>()
  for (const b of binds) {
    const set = clientIfaces.get(b.serverId) ?? new Set<string>()
    set.add(b.interfaceName)
    clientIfaces.set(b.serverId, set)
    allClientNames.add(b.interfaceName)
    const user = loginById.get(b.userId)
    clientByIface.set(ifaceKey(b.serverId, b.interfaceName), {
      userId: b.userId,
      login: user?.login || b.userId,
      name: user?.name || user?.login || b.userId,
      serverId: b.serverId,
      interfaceName: b.interfaceName,
    })
  }
  const enHosts = new Set<string>()
  const jhHosts = new Set<string>()
  const enNodes: FlowEnNode[] = []
  const wanIfaces = new Map<number, Set<string>>()
  for (const s of serverRows) {
    const wans = parseWanUplinks(s.wanUplinks)
    const hosts = [s.host, ...wans.map((w) => String(w.ip ?? "").trim())].filter(Boolean)
    const wanSet = new Set(wans.map((w) => String(w.iface ?? "").trim()).filter(Boolean))
    if (wanSet.size) wanIfaces.set(s.id, wanSet)
    if (s.type === "exit-node") {
      for (const h of hosts) enHosts.add(h)
      enNodes.push({ id: s.id, name: s.name || s.host, hosts })
    }
    if (s.type === "jump-host") {
      for (const h of hosts) jhHosts.add(h)
    }
  }
  return {
    clientIfaces,
    clientByIface,
    enNodes,
    enHosts,
    jhHosts,
    wanIfaces,
    plane: {
      clientIfaceNames: allClientNames,
      enHosts,
      jhHosts,
    },
  }
}

export function seedFlowTopologyForTests(topo: FlowTopology | null): void {
  seeded = topo
}

export function resolveClient(
  topo: FlowTopology,
  serverId: number,
  inIfaceName: string,
): FlowClientBinding | null {
  return topo.clientByIface.get(ifaceKey(serverId, inIfaceName)) ?? null
}

export function resolveEn(
  topo: FlowTopology,
  nextHop: string,
  outIfaceName: string,
): FlowEnNode | null {
  if (nextHop) {
    const hit = topo.enNodes.find((n) => n.hosts.includes(nextHop))
    if (hit) return hit
  }
  const needle = outIfaceName.trim().toLowerCase()
  if (!needle) return null
  return topo.enNodes.find((n) => {
    const name = n.name.toLowerCase()
    const host = (n.hosts[0] ?? "").toLowerCase()
    return (name && needle.includes(name)) || (host && needle.includes(host.split(".")[0] ?? ""))
  }) ?? null
}

export function enGreIfaceNames(topo: FlowTopology, serverId: number, ifaceNames: string[]): string[] {
  const client = topo.clientIfaces.get(serverId) ?? new Set<string>()
  return ifaceNames.filter((name) => {
    if (client.has(name)) return false
    if (name === "wg-flow") return false
    return mapRosInterfaceType("", name) === "gre"
  })
}

export function latestWireBps(serverId: number, ifaceNames: string[]): { bps: number; bytes: number } {
  if (!ifaceNames.length) return { bps: 0, bytes: 0 }
  const placeholders = ifaceNames.map(() => "?").join(",")
  const rows = sqliteDatabase.prepare(`
    SELECT interface_name AS name, rx_bps AS rxBps, tx_bps AS txBps, rx_bytes AS rxBytes, tx_bytes AS txBytes
    FROM traffic_samples
    WHERE server_id = ? AND interface_name IN (${placeholders})
    ORDER BY sampled_at DESC
  `).all(serverId, ...ifaceNames) as Array<{
    name: string
    rxBps: number
    txBps: number
    rxBytes: number
    txBytes: number
  }>
  const seen = new Set<string>()
  let bps = 0
  let bytes = 0
  for (const r of rows) {
    if (seen.has(r.name)) continue
    seen.add(r.name)
    bps += (Number(r.rxBps) || 0) + (Number(r.txBps) || 0)
    bytes += (Number(r.rxBytes) || 0) + (Number(r.txBytes) || 0)
  }
  return { bps, bytes }
}
