import { eq } from "drizzle-orm"
import type { FlowMapHop, FlowMapHopsDto } from "@mmapp/contracts/traffic-flow"
import { db } from "../db/index.js"
import { servers, userInterfaceBindings } from "../db/schema.js"
import { flowRowMatchesFilter } from "./traffic-flow-apps.js"
import { dedupFlowRowsMaxBytes } from "./traffic-flow-dedup.js"
import { getFlowListenerState, listFlowRowsForWindow } from "./traffic-flow-ingest.js"
import { resolveIfaceName } from "./traffic-flow-ifaces.js"
import { classifyFlowPlane, shouldKeepPlane } from "./traffic-flow-planes.js"
import { loadFlowTopology, resolveEn } from "./traffic-flow-topology.js"

export interface FlowMapHopsQuery {
  minutes: number
  serverId?: number
  userId?: string
  iface?: string
  dedup?: boolean
  excludeMesh?: boolean
  excludeOverlay?: boolean
}

interface HopAcc {
  fromId: string
  fromLabel: string
  toId: string
  toLabel: string
  kind: FlowMapHop["kind"]
  iface?: string
  bytes: number
  bytesFwd: number
  bytesRev: number
}

function userIfaceAllow(userId: string): Map<number, Set<string>> | null {
  if (!userId) return null
  const binds = db.select().from(userInterfaceBindings).where(eq(userInterfaceBindings.userId, userId)).all()
  const allow = new Map<number, Set<string>>()
  for (const b of binds) {
    const set = allow.get(b.serverId) ?? new Set<string>()
    set.add(b.interfaceName)
    allow.set(b.serverId, set)
  }
  return allow
}

function ifaceUsable(name: string): boolean {
  return Boolean(name) && name !== "—"
}

function bump(acc: Map<string, HopAcc>, key: string, seed: Omit<HopAcc, "bytes" | "bytesFwd" | "bytesRev">, bytes: number, dir: "fwd" | "rev" | "both"): void {
  const prev = acc.get(key)
  const addFwd = dir === "fwd" || dir === "both" ? bytes : 0
  const addRev = dir === "rev" || dir === "both" ? bytes : 0
  if (prev) {
    prev.bytes += bytes
    prev.bytesFwd += addFwd
    prev.bytesRev += addRev
    if (seed.iface && !prev.iface) prev.iface = seed.iface
    return
  }
  acc.set(key, {
    ...seed,
    bytes,
    bytesFwd: addFwd,
    bytesRev: addRev,
  })
}

function toHop(a: HopAcc, windowSec: number): FlowMapHop {
  return {
    fromId: a.fromId,
    fromLabel: a.fromLabel,
    toId: a.toId,
    toLabel: a.toLabel,
    kind: a.kind,
    ...(a.iface ? { iface: a.iface } : {}),
    bytes: a.bytes,
    bps: (a.bytes * 8) / windowSec,
    bpsFwd: (a.bytesFwd * 8) / windowSec,
    bpsRev: (a.bytesRev * 8) / windowSec,
  }
}

/** Hop-rates для карты сети: те же фильтры, что у общего NetFlow (dedup / mesh / overlay). */
export function buildFlowMapHops(q: FlowMapHopsQuery): FlowMapHopsDto {
  const windowSec = Math.max(60, q.minutes * 60)
  const raw = listFlowRowsForWindow(q.minutes)
  const allow = q.userId ? userIfaceAllow(q.userId) : null
  const serverRows = db.select().from(servers).all()
  const nameById = new Map(serverRows.map((s) => [s.id, s.name || s.host]))
  const ifaceFilter = q.iface && q.iface !== "__all__" ? q.iface : ""
  const wantDedup = q.dedup !== false && !ifaceFilter
  const excludeMesh = q.excludeMesh !== false
  const excludeOverlay = q.excludeOverlay !== false
  const topo = loadFlowTopology()

  const matched = []
  for (const r of raw) {
    const resolved = resolveIfaceName(r.serverId, r.inIface)
    const outResolved = resolveIfaceName(r.serverId, r.outIface)
    if (!flowRowMatchesFilter(r, resolved.name, q, allow)) continue
    const plane = classifyFlowPlane({
      src: r.src,
      dst: r.dst,
      proto: r.proto,
      srcPort: r.srcPort,
      dstPort: r.dstPort,
      inIface: resolved.name,
      outIface: outResolved.name,
    }, topo.plane)
    if (!shouldKeepPlane(plane, { excludeMesh, excludeOverlay })) continue
    matched.push(r)
  }

  const working = wantDedup ? dedupFlowRowsMaxBytes(matched) : matched
  const hops = new Map<string, HopAcc>()

  for (const r of working) {
    const inRes = resolveIfaceName(r.serverId, r.inIface)
    const outRes = resolveIfaceName(r.serverId, r.outIface)
    const inName = inRes.name
    const outName = outRes.name
    const fromId = String(r.serverId)
    const fromLabel = nameById.get(r.serverId) ?? fromId
    const wanSet = topo.wanIfaces.get(r.serverId)

    const inOk = ifaceUsable(inName)
    const outOk = ifaceUsable(outName)
    const sameIface = inOk && outOk && inName.toLowerCase() === outName.toLowerCase()
    if (sameIface) {
      bump(hops, `iface|${fromId}|${inName.toLowerCase()}`, {
        fromId,
        fromLabel,
        toId: "",
        toLabel: "",
        kind: "iface",
        iface: inName,
      }, r.bytes, "fwd")
    } else {
      if (inOk) {
        bump(hops, `iface|${fromId}|${inName.toLowerCase()}`, {
          fromId,
          fromLabel,
          toId: "",
          toLabel: "",
          kind: "iface",
          iface: inName,
        }, r.bytes, "rev")
      }
      if (outOk) {
        bump(hops, `iface|${fromId}|${outName.toLowerCase()}`, {
          fromId,
          fromLabel,
          toId: "",
          toLabel: "",
          kind: "iface",
          iface: outName,
        }, r.bytes, "fwd")
      }
    }

    const enOut = ifaceUsable(outName) ? resolveEn(topo, r.nextHop, outName) : null
    const enIn = ifaceUsable(inName) ? resolveEn(topo, "", inName) : null
    const en = (enOut && enOut.id !== r.serverId ? enOut : null)
      ?? (enIn && enIn.id !== r.serverId ? enIn : null)
    if (en) {
      const toId = String(en.id)
      const dir: "fwd" | "rev" = enOut && enOut.id === en.id ? "fwd" : "rev"
      const greIface = dir === "fwd" && ifaceUsable(outName) ? outName : (ifaceUsable(inName) ? inName : undefined)
      bump(hops, `gre|${fromId}|${toId}`, {
        fromId,
        fromLabel,
        toId,
        toLabel: en.name,
        kind: "gre",
        iface: greIface,
      }, r.bytes, dir)
    }

    if (wanSet?.size) {
      if (ifaceUsable(inName) && wanSet.has(inName)) {
        bump(hops, `wan|${fromId}|${inName.toLowerCase()}`, {
          fromId,
          fromLabel,
          toId: "",
          toLabel: "",
          kind: "wan",
          iface: inName,
        }, r.bytes, "rev")
      }
      if (ifaceUsable(outName) && wanSet.has(outName) && outName.toLowerCase() !== inName.toLowerCase()) {
        bump(hops, `wan|${fromId}|${outName.toLowerCase()}`, {
          fromId,
          fromLabel,
          toId: "",
          toLabel: "",
          kind: "wan",
          iface: outName,
        }, r.bytes, "fwd")
      }
    }
  }

  const listener = getFlowListenerState()
  return {
    hops: [...hops.values()]
      .map((a) => toHop(a, windowSec))
      .sort((a, b) => b.bytes - a.bytes),
    live: listener.bound,
    rangeMinutes: q.minutes,
    windowSec,
    dedupApplied: wantDedup,
    excludeMeshApplied: excludeMesh,
    excludeOverlayApplied: excludeOverlay,
  }
}
