## PCAP File Format Support for NimPacket
##
## Pure Nim implementation of the PCAP file format (no libpcap dependency).
## Supports reading and writing standard .pcap files compatible with
## Wireshark, tcpdump, and other tools.
##
## Also includes a deterministic replay engine with:
## - Exact timing preservation from original capture
## - Adjustable replay speed (0.5x, 1x, 2x, etc.)
## - Flow-level selection (filter by src/dst IP, ports, protocol)
## - Repeatable, identical runs every time
##
## PCAP Format Reference:
## - Global Header: 24 bytes (magic, version, snaplen, link type)
## - Per-Packet Header: 16 bytes (timestamp, lengths)
## - Packet Data: raw bytes
##
## Example:
## ```nim
## import pcap, nimpacket
##
## # Read a capture file
## let capture = readPcap("traffic.pcap")
## echo pcapStats(capture)
##
## # Filter by protocol
## let tcpOnly = filterPcapByProtocol(capture, "tcp")
##
## # Write crafted packets
## let pkt = (myIp / myTcp).toBytes()
## writePcap("crafted.pcap", @[@pkt])
## ```

import std/[streams, endians, times, os, strutils]

# Forward declaration of nimpacket types we need for filtering
# We import at usage site to avoid circular deps

const
  PCAP_MAGIC_LE* = 0xA1B2C3D4'u32   ## Little-endian magic number
  PCAP_MAGIC_BE* = 0xD4C3B2A1'u32   ## Big-endian (swapped) magic number
  PCAP_VERSION_MAJOR* = 2'u16
  PCAP_VERSION_MINOR* = 4'u16
  PCAP_DEFAULT_SNAPLEN* = 65535'u32
  PCAP_LINKTYPE_ETHERNET* = 1'u32   ## Ethernet link layer
  PCAP_LINKTYPE_RAW* = 101'u32      ## Raw IP (no link layer)

type
  PcapGlobalHeader* = object
    magicNumber*: uint32     ## 0xA1B2C3D4 (LE) or 0xD4C3B2A1 (BE)
    versionMajor*: uint16    ## 2
    versionMinor*: uint16    ## 4
    thiszone*: int32         ## GMT offset (usually 0)
    sigfigs*: uint32         ## Timestamp accuracy (usually 0)
    snaplen*: uint32         ## Max captured packet length (usually 65535)
    network*: uint32         ## Link-layer type (1 = Ethernet)

  PcapPacketHeader* = object
    tsSec*: uint32           ## Timestamp seconds (Unix epoch)
    tsUsec*: uint32          ## Timestamp microseconds
    inclLen*: uint32         ## Number of bytes captured
    origLen*: uint32         ## Original packet length on wire

  PcapPacket* = object
    header*: PcapPacketHeader
    data*: seq[byte]

  PcapFile* = object
    globalHeader*: PcapGlobalHeader
    packets*: seq[PcapPacket]

  PcapError* = object of CatchableError

  ## Flow identifier for replay filtering
  PcapFlow* = object
    srcIP*: string
    dstIP*: string
    srcPort*: uint16
    dstPort*: uint16
    protocol*: string

  ## Replay configuration
  ReplayConfig* = object
    speed*: float            ## Replay speed multiplier (1.0 = original, 2.0 = 2x fast)
    loop*: bool              ## Loop replay
    count*: int              ## Number of loops (0 = infinite when loop=true)
    startPacket*: int        ## Start from this packet index
    endPacket*: int          ## End at this packet index (-1 = all)
    filterProtocol*: string  ## Filter by protocol ("" = all)
    filterSrcIP*: string     ## Filter by source IP ("" = all)
    filterDstIP*: string     ## Filter by destination IP ("" = all)
    filterDstPort*: int      ## Filter by destination port (0 = all)
    filterSrcPort*: int      ## Filter by source port (0 = all)

# =============================================================================
# Constructors
# =============================================================================

proc newPcapGlobalHeader*(snaplen: uint32 = PCAP_DEFAULT_SNAPLEN,
                          network: uint32 = PCAP_LINKTYPE_ETHERNET): PcapGlobalHeader =
  result.magicNumber = PCAP_MAGIC_LE
  result.versionMajor = PCAP_VERSION_MAJOR
  result.versionMinor = PCAP_VERSION_MINOR
  result.thiszone = 0
  result.sigfigs = 0
  result.snaplen = snaplen
  result.network = network

proc newPcapPacketHeader*(inclLen, origLen: uint32,
                          tsSec: uint32 = 0, tsUsec: uint32 = 0): PcapPacketHeader =
  result.tsSec = tsSec
  result.tsUsec = tsUsec
  result.inclLen = inclLen
  result.origLen = origLen

proc newReplayConfig*(speed: float = 1.0): ReplayConfig =
  result.speed = speed
  result.loop = false
  result.count = 1
  result.startPacket = 0
  result.endPacket = -1
  result.filterProtocol = ""
  result.filterSrcIP = ""
  result.filterDstIP = ""
  result.filterDstPort = 0
  result.filterSrcPort = 0

# =============================================================================
# Byte-swap helpers
# =============================================================================

proc swap16(x: uint16): uint16 =
  (x shl 8) or (x shr 8)

proc swap32(x: uint32): uint32 =
  ((x and 0xFF) shl 24) or
  (((x shr 8) and 0xFF) shl 16) or
  (((x shr 16) and 0xFF) shl 8) or
  ((x shr 24) and 0xFF)

proc swapInt32(x: int32): int32 =
  cast[int32](swap32(cast[uint32](x)))

# =============================================================================
# Serialization (to bytes) — always writes native (little-endian on x86)
# =============================================================================

proc pcapGlobalHeaderToBytes*(h: PcapGlobalHeader): seq[byte] =
  result = newSeq[byte](24)
  var magic = h.magicNumber
  var major = h.versionMajor
  var minor = h.versionMinor
  var zone = h.thiszone
  var sigfigs = h.sigfigs
  var snaplen = h.snaplen
  var network = h.network

  littleEndian32(addr result[0], addr magic)
  littleEndian16(addr result[4], addr major)
  littleEndian16(addr result[6], addr minor)
  copyMem(addr result[8], addr zone, 4)  # int32, native order
  littleEndian32(addr result[12], addr sigfigs)
  littleEndian32(addr result[16], addr snaplen)
  littleEndian32(addr result[20], addr network)

proc pcapPacketHeaderToBytes*(h: PcapPacketHeader): seq[byte] =
  result = newSeq[byte](16)
  var tsSec = h.tsSec
  var tsUsec = h.tsUsec
  var inclLen = h.inclLen
  var origLen = h.origLen

  littleEndian32(addr result[0], addr tsSec)
  littleEndian32(addr result[4], addr tsUsec)
  littleEndian32(addr result[8], addr inclLen)
  littleEndian32(addr result[12], addr origLen)

# =============================================================================
# Parsing (from bytes) — handles both byte orders
# =============================================================================

proc parsePcapGlobalHeader*(data: seq[byte]): PcapGlobalHeader =
  if data.len < 24:
    raise newException(PcapError, "Not enough data for PCAP global header (need 24 bytes, got " & $data.len & ")")

  # Read magic to detect byte order
  littleEndian32(addr result.magicNumber, unsafeAddr data[0])

  let needSwap = result.magicNumber == PCAP_MAGIC_BE

  if result.magicNumber != PCAP_MAGIC_LE and result.magicNumber != PCAP_MAGIC_BE:
    raise newException(PcapError, "Invalid PCAP magic number: 0x" & result.magicNumber.toHex(8) &
                       " (expected 0xA1B2C3D4 or 0xD4C3B2A1)")

  # Normalize magic to LE
  result.magicNumber = PCAP_MAGIC_LE

  littleEndian16(addr result.versionMajor, unsafeAddr data[4])
  littleEndian16(addr result.versionMinor, unsafeAddr data[6])
  copyMem(addr result.thiszone, unsafeAddr data[8], 4)
  littleEndian32(addr result.sigfigs, unsafeAddr data[12])
  littleEndian32(addr result.snaplen, unsafeAddr data[16])
  littleEndian32(addr result.network, unsafeAddr data[20])

  if needSwap:
    result.versionMajor = swap16(result.versionMajor)
    result.versionMinor = swap16(result.versionMinor)
    result.thiszone = swapInt32(result.thiszone)
    result.sigfigs = swap32(result.sigfigs)
    result.snaplen = swap32(result.snaplen)
    result.network = swap32(result.network)

proc parsePcapPacketHeader*(data: seq[byte], swapBytes: bool = false): PcapPacketHeader =
  if data.len < 16:
    raise newException(PcapError, "Not enough data for PCAP packet header (need 16 bytes, got " & $data.len & ")")

  littleEndian32(addr result.tsSec, unsafeAddr data[0])
  littleEndian32(addr result.tsUsec, unsafeAddr data[4])
  littleEndian32(addr result.inclLen, unsafeAddr data[8])
  littleEndian32(addr result.origLen, unsafeAddr data[12])

  if swapBytes:
    result.tsSec = swap32(result.tsSec)
    result.tsUsec = swap32(result.tsUsec)
    result.inclLen = swap32(result.inclLen)
    result.origLen = swap32(result.origLen)

# =============================================================================
# File I/O
# =============================================================================

proc readPcap*(filename: string): PcapFile =
  ## Read a complete PCAP file. Detects byte order from magic number.
  if not fileExists(filename):
    raise newException(PcapError, "PCAP file not found: " & filename)

  let fileData = readFile(filename)
  if fileData.len < 24:
    raise newException(PcapError, "File too small to be a PCAP file: " & filename)

  var data = newSeq[byte](fileData.len)
  for i in 0..<fileData.len:
    data[i] = fileData[i].byte

  # Parse global header
  result.globalHeader = parsePcapGlobalHeader(data[0..<24])

  # Detect if we need byte swapping (original magic was BE)
  var rawMagic: uint32
  littleEndian32(addr rawMagic, unsafeAddr data[0])
  let needSwap = rawMagic == PCAP_MAGIC_BE

  # Parse packets
  var offset = 24
  while offset + 16 <= data.len:
    let pktHeader = parsePcapPacketHeader(data[offset..<(offset + 16)], needSwap)
    offset += 16

    if pktHeader.inclLen > result.globalHeader.snaplen:
      raise newException(PcapError, "Packet captured length (" & $pktHeader.inclLen &
                         ") exceeds snaplen (" & $result.globalHeader.snaplen & ")")

    if offset + pktHeader.inclLen.int > data.len:
      raise newException(PcapError, "Truncated packet data at offset " & $offset &
                         " (need " & $pktHeader.inclLen & " bytes, have " & $(data.len - offset) & ")")

    var pkt = PcapPacket(header: pktHeader)
    pkt.data = data[offset..<(offset + pktHeader.inclLen.int)]
    result.packets.add(pkt)

    offset += pktHeader.inclLen.int

proc writePcap*(filename: string, pcapFile: PcapFile) =
  ## Write a PcapFile to disk.
  let f = newFileStream(filename, fmWrite)
  if f.isNil:
    raise newException(PcapError, "Cannot open file for writing: " & filename)
  defer: f.close()

  let ghBytes = pcapGlobalHeaderToBytes(pcapFile.globalHeader)
  f.writeData(unsafeAddr ghBytes[0], ghBytes.len)

  for pkt in pcapFile.packets:
    let phBytes = pcapPacketHeaderToBytes(pkt.header)
    f.writeData(unsafeAddr phBytes[0], phBytes.len)
    if pkt.data.len > 0:
      f.writeData(unsafeAddr pkt.data[0], pkt.data.len)

proc writePcap*(filename: string, packets: seq[seq[byte]],
                snaplen: uint32 = PCAP_DEFAULT_SNAPLEN,
                linkType: uint32 = PCAP_LINKTYPE_ETHERNET) =
  ## Convenience: write raw packet byte arrays to PCAP with auto-generated timestamps.
  var pcap = PcapFile(globalHeader: newPcapGlobalHeader(snaplen, linkType))

  let now = epochTime()
  for i, pktData in packets:
    let ts = now + (i.float * 0.001)  # 1ms apart
    let sec = ts.uint32
    let usec = ((ts - sec.float) * 1_000_000).uint32
    let capLen = min(pktData.len.uint32, snaplen)

    var pkt = PcapPacket(
      header: newPcapPacketHeader(capLen, pktData.len.uint32, sec, usec),
      data: pktData[0..<capLen.int]
    )
    pcap.packets.add(pkt)

  writePcap(filename, pcap)

# =============================================================================
# Packet protocol detection (lightweight, no full parse dependency)
# =============================================================================

proc detectProtocol(data: seq[byte], linkType: uint32): string =
  ## Detect the transport protocol of a packet. Returns "tcp", "udp", "icmp",
  ## "icmpv6", "arp", "dns", or "unknown".
  var offset = 0
  var etherType: uint16 = 0

  if linkType == PCAP_LINKTYPE_ETHERNET:
    if data.len < 14:
      return "unknown"
    etherType = (data[12].uint16 shl 8) or data[13].uint16
    offset = 14

    if etherType == 0x0806:
      return "arp"
  elif linkType == PCAP_LINKTYPE_RAW:
    if data.len < 1:
      return "unknown"
    let version = data[0] shr 4
    if version == 4:
      etherType = 0x0800
    elif version == 6:
      etherType = 0x86DD
    else:
      return "unknown"
  else:
    return "unknown"

  if etherType == 0x0800:
    # IPv4
    if data.len < offset + 20:
      return "unknown"
    let proto = data[offset + 9]
    case proto:
    of 1: return "icmp"
    of 6:
      # Check if this might be DNS (port 53)
      let ihl = (data[offset] and 0x0F).int * 4
      if data.len >= offset + ihl + 4:
        let dstPort = (data[offset + ihl + 2].uint16 shl 8) or data[offset + ihl + 3].uint16
        let srcPort = (data[offset + ihl].uint16 shl 8) or data[offset + ihl + 1].uint16
        if dstPort == 53 or srcPort == 53:
          return "dns"
      return "tcp"
    of 17:
      # Check if this might be DNS (port 53) or DHCP (port 67/68)
      let ihl = (data[offset] and 0x0F).int * 4
      if data.len >= offset + ihl + 4:
        let srcPort = (data[offset + ihl].uint16 shl 8) or data[offset + ihl + 1].uint16
        let dstPort = (data[offset + ihl + 2].uint16 shl 8) or data[offset + ihl + 3].uint16
        if dstPort == 53 or srcPort == 53:
          return "dns"
        if dstPort == 67 or dstPort == 68 or srcPort == 67 or srcPort == 68:
          return "dhcp"
      return "udp"
    else: return "unknown"
  elif etherType == 0x86DD:
    # IPv6
    if data.len < offset + 40:
      return "unknown"
    let nextHeader = data[offset + 6]
    case nextHeader:
    of 6: return "tcp"
    of 17: return "udp"
    of 58: return "icmpv6"
    else: return "unknown"

  return "unknown"

proc extractFlowInfo(data: seq[byte], linkType: uint32): PcapFlow =
  ## Extract flow information (IPs, ports, protocol) from packet data.
  var offset = 0
  var etherType: uint16 = 0

  if linkType == PCAP_LINKTYPE_ETHERNET:
    if data.len < 14:
      return
    etherType = (data[12].uint16 shl 8) or data[13].uint16
    offset = 14
  elif linkType == PCAP_LINKTYPE_RAW:
    if data.len < 1:
      return
    let version = data[0] shr 4
    if version == 4:
      etherType = 0x0800
    elif version == 6:
      etherType = 0x86DD

  result.protocol = detectProtocol(data, linkType)

  if etherType == 0x0800 and data.len >= offset + 20:
    # IPv4 - extract IPs
    let a = data[offset + 12]; let b = data[offset + 13]
    let c = data[offset + 14]; let d = data[offset + 15]
    result.srcIP = $a & "." & $b & "." & $c & "." & $d

    let e = data[offset + 16]; let f = data[offset + 17]
    let g = data[offset + 18]; let h = data[offset + 19]
    result.dstIP = $e & "." & $f & "." & $g & "." & $h

    # Extract ports for TCP/UDP
    let ihl = (data[offset] and 0x0F).int * 4
    let proto = data[offset + 9]
    if (proto == 6 or proto == 17) and data.len >= offset + ihl + 4:
      result.srcPort = (data[offset + ihl].uint16 shl 8) or data[offset + ihl + 1].uint16
      result.dstPort = (data[offset + ihl + 2].uint16 shl 8) or data[offset + ihl + 3].uint16

# =============================================================================
# Statistics
# =============================================================================

proc pcapStats*(pcapFile: PcapFile): string =
  ## Returns a human-readable stats summary.
  let count = pcapFile.packets.len

  if count == 0:
    return "PCAP Stats:\n  Packets: 0\n  Link type: " & $pcapFile.globalHeader.network &
           "\n  Snaplen: " & $pcapFile.globalHeader.snaplen

  var totalBytes: uint64 = 0
  var minLen: uint32 = high(uint32)
  var maxLen: uint32 = 0
  var firstTs: float64 = pcapFile.packets[0].header.tsSec.float64 +
                          pcapFile.packets[0].header.tsUsec.float64 / 1_000_000.0
  var lastTs: float64 = firstTs

  # Protocol counters
  var protoCount: array[6, int]  # tcp, udp, icmp, arp, dns, other
  let linkType = pcapFile.globalHeader.network

  for pkt in pcapFile.packets:
    totalBytes += pkt.header.inclLen.uint64
    if pkt.header.inclLen < minLen:
      minLen = pkt.header.inclLen
    if pkt.header.inclLen > maxLen:
      maxLen = pkt.header.inclLen

    let ts = pkt.header.tsSec.float64 + pkt.header.tsUsec.float64 / 1_000_000.0
    if ts < firstTs: firstTs = ts
    if ts > lastTs: lastTs = ts

    let proto = detectProtocol(pkt.data, linkType)
    case proto:
    of "tcp": protoCount[0] += 1
    of "udp": protoCount[1] += 1
    of "icmp", "icmpv6": protoCount[2] += 1
    of "arp": protoCount[3] += 1
    of "dns": protoCount[4] += 1
    else: protoCount[5] += 1

  let duration = lastTs - firstTs
  let avgLen = totalBytes.float / count.float

  result = "PCAP Stats:\n"
  result &= "  Packets: " & $count & "\n"
  result &= "  Duration: " & formatFloat(duration, ffDecimal, 3) & "s\n"
  result &= "  Total bytes: " & $totalBytes & "\n"
  result &= "  Avg packet size: " & formatFloat(avgLen, ffDecimal, 1) & " bytes\n"
  result &= "  Min packet size: " & $minLen & " bytes\n"
  result &= "  Max packet size: " & $maxLen & " bytes\n"
  result &= "  Link type: " & $pcapFile.globalHeader.network & "\n"
  result &= "  Snaplen: " & $pcapFile.globalHeader.snaplen & "\n"
  result &= "  Protocols:\n"
  if protoCount[0] > 0: result &= "    TCP: " & $protoCount[0] & "\n"
  if protoCount[1] > 0: result &= "    UDP: " & $protoCount[1] & "\n"
  if protoCount[2] > 0: result &= "    ICMP: " & $protoCount[2] & "\n"
  if protoCount[3] > 0: result &= "    ARP: " & $protoCount[3] & "\n"
  if protoCount[4] > 0: result &= "    DNS: " & $protoCount[4] & "\n"
  if protoCount[5] > 0: result &= "    Other: " & $protoCount[5] & "\n"

proc pcapStatsJson*(pcapFile: PcapFile): string =
  ## Returns stats as a JSON-like string for Python bindings.
  let count = pcapFile.packets.len
  var totalBytes: uint64 = 0
  var minLen: uint32 = if count > 0: high(uint32) else: 0
  var maxLen: uint32 = 0
  var firstTs: float64 = 0
  var lastTs: float64 = 0
  var protoCount: array[6, int]
  let linkType = pcapFile.globalHeader.network

  if count > 0:
    firstTs = pcapFile.packets[0].header.tsSec.float64 +
              pcapFile.packets[0].header.tsUsec.float64 / 1_000_000.0
    lastTs = firstTs

  for pkt in pcapFile.packets:
    totalBytes += pkt.header.inclLen.uint64
    if pkt.header.inclLen < minLen: minLen = pkt.header.inclLen
    if pkt.header.inclLen > maxLen: maxLen = pkt.header.inclLen

    let ts = pkt.header.tsSec.float64 + pkt.header.tsUsec.float64 / 1_000_000.0
    if ts < firstTs: firstTs = ts
    if ts > lastTs: lastTs = ts

    let proto = detectProtocol(pkt.data, linkType)
    case proto:
    of "tcp": protoCount[0] += 1
    of "udp": protoCount[1] += 1
    of "icmp", "icmpv6": protoCount[2] += 1
    of "arp": protoCount[3] += 1
    of "dns": protoCount[4] += 1
    else: protoCount[5] += 1

  let duration = if count > 0: lastTs - firstTs else: 0.0

  result = "{\"packet_count\":" & $count
  result &= ",\"duration\":" & formatFloat(duration, ffDecimal, 6)
  result &= ",\"total_bytes\":" & $totalBytes
  result &= ",\"min_packet_size\":" & $minLen
  result &= ",\"max_packet_size\":" & $maxLen
  result &= ",\"avg_packet_size\":" & formatFloat(if count > 0: totalBytes.float / count.float else: 0.0, ffDecimal, 1)
  result &= ",\"link_type\":" & $pcapFile.globalHeader.network
  result &= ",\"snaplen\":" & $pcapFile.globalHeader.snaplen
  result &= ",\"protocols\":{\"tcp\":" & $protoCount[0]
  result &= ",\"udp\":" & $protoCount[1]
  result &= ",\"icmp\":" & $protoCount[2]
  result &= ",\"arp\":" & $protoCount[3]
  result &= ",\"dns\":" & $protoCount[4]
  result &= ",\"other\":" & $protoCount[5] & "}}"

# =============================================================================
# Filtering
# =============================================================================

proc filterPcapByProtocol*(pcapFile: PcapFile, protocol: string): PcapFile =
  ## Filter packets by transport protocol ("tcp", "udp", "icmp", "arp", "dns", "dhcp").
  result.globalHeader = pcapFile.globalHeader
  let proto = protocol.toLowerAscii()
  let linkType = pcapFile.globalHeader.network

  for pkt in pcapFile.packets:
    let detected = detectProtocol(pkt.data, linkType)
    if detected == proto:
      result.packets.add(pkt)

proc filterPcapByFlow*(pcapFile: PcapFile, config: ReplayConfig): PcapFile =
  ## Filter packets by flow criteria (IP, port, protocol).
  result.globalHeader = pcapFile.globalHeader
  let linkType = pcapFile.globalHeader.network

  for pkt in pcapFile.packets:
    let flow = extractFlowInfo(pkt.data, linkType)
    var matches = true

    if config.filterProtocol.len > 0 and flow.protocol != config.filterProtocol.toLowerAscii():
      matches = false
    if config.filterSrcIP.len > 0 and flow.srcIP != config.filterSrcIP:
      matches = false
    if config.filterDstIP.len > 0 and flow.dstIP != config.filterDstIP:
      matches = false
    if config.filterSrcPort > 0 and flow.srcPort != config.filterSrcPort.uint16:
      matches = false
    if config.filterDstPort > 0 and flow.dstPort != config.filterDstPort.uint16:
      matches = false

    if matches:
      result.packets.add(pkt)

# =============================================================================
# Replay Engine
# =============================================================================

proc getReplayPackets*(pcapFile: PcapFile, config: ReplayConfig): seq[PcapPacket] =
  ## Get the filtered and sliced list of packets for replay.
  var filtered = pcapFile

  # Apply flow filter
  if config.filterProtocol.len > 0 or config.filterSrcIP.len > 0 or
     config.filterDstIP.len > 0 or config.filterSrcPort > 0 or
     config.filterDstPort > 0:
    filtered = filterPcapByFlow(pcapFile, config)

  # Apply packet range
  let startIdx = max(0, config.startPacket)
  let endIdx = if config.endPacket < 0: filtered.packets.len - 1
               else: min(config.endPacket, filtered.packets.len - 1)

  if startIdx > endIdx or startIdx >= filtered.packets.len:
    return @[]

  result = filtered.packets[startIdx..endIdx]

proc getReplayDelaysUs*(packets: seq[PcapPacket], speed: float): seq[int64] =
  ## Calculate inter-packet delays in microseconds, adjusted for replay speed.
  ## First packet always has delay 0.
  if packets.len == 0:
    return @[]

  result = newSeq[int64](packets.len)
  result[0] = 0

  let speedFactor = if speed <= 0.0: 1.0 else: speed

  for i in 1..<packets.len:
    let prevTs = packets[i-1].header.tsSec.int64 * 1_000_000 + packets[i-1].header.tsUsec.int64
    let curTs = packets[i].header.tsSec.int64 * 1_000_000 + packets[i].header.tsUsec.int64
    var delayUs = curTs - prevTs
    if delayUs < 0: delayUs = 0
    result[i] = (delayUs.float / speedFactor).int64

proc replaySchedule*(pcapFile: PcapFile, config: ReplayConfig): seq[tuple[delayUs: int64, data: seq[byte]]] =
  ## Build a complete replay schedule: list of (delay_microseconds, packet_data) tuples.
  ## This is deterministic — same input always produces same output.
  let packets = getReplayPackets(pcapFile, config)
  if packets.len == 0:
    return @[]

  let delays = getReplayDelaysUs(packets, config.speed)

  for i in 0..<packets.len:
    result.add((delayUs: delays[i], data: packets[i].data))

proc replayScheduleJson*(pcapFile: PcapFile, config: ReplayConfig): string =
  ## Returns the replay schedule as a JSON array string for Python bindings.
  ## Each element: {"delay_us": N, "data_hex": "..."}
  let schedule = replaySchedule(pcapFile, config)

  result = "["
  for i, item in schedule:
    if i > 0: result &= ","
    var hexStr = ""
    for b in item.data:
      hexStr &= b.toHex(2)
    result &= "{\"delay_us\":" & $item.delayUs
    result &= ",\"data_hex\":\"" & hexStr.toLowerAscii() & "\"}"
  result &= "]"

# =============================================================================
# Packet summary (one-line per packet)
# =============================================================================

proc packetSummary*(pkt: PcapPacket, index: int, linkType: uint32): string =
  ## One-line summary of a packet.
  let flow = extractFlowInfo(pkt.data, linkType)
  let ts = pkt.header.tsSec.float64 + pkt.header.tsUsec.float64 / 1_000_000.0

  result = "#" & $index & "  "
  result &= formatFloat(ts, ffDecimal, 6) & "  "
  result &= $pkt.header.inclLen & "B  "
  result &= flow.protocol.toUpperAscii()

  if flow.srcIP.len > 0:
    result &= "  " & flow.srcIP
    if flow.srcPort > 0:
      result &= ":" & $flow.srcPort
    result &= " -> " & flow.dstIP
    if flow.dstPort > 0:
      result &= ":" & $flow.dstPort

# =============================================================================
# Utility: read PCAP to JSON (for Python bindings)
# =============================================================================

proc readPcapJson*(filename: string): string =
  ## Read a PCAP file and return JSON representation.
  let pcap = readPcap(filename)

  result = "{\"global_header\":{"
  result &= "\"magic\":\"0x" & pcap.globalHeader.magicNumber.toHex(8) & "\""
  result &= ",\"version_major\":" & $pcap.globalHeader.versionMajor
  result &= ",\"version_minor\":" & $pcap.globalHeader.versionMinor
  result &= ",\"snaplen\":" & $pcap.globalHeader.snaplen
  result &= ",\"network\":" & $pcap.globalHeader.network
  result &= "},\"packets\":["

  for i, pkt in pcap.packets:
    if i > 0: result &= ","
    result &= "{\"ts_sec\":" & $pkt.header.tsSec
    result &= ",\"ts_usec\":" & $pkt.header.tsUsec
    result &= ",\"incl_len\":" & $pkt.header.inclLen
    result &= ",\"orig_len\":" & $pkt.header.origLen
    result &= ",\"data_hex\":\""
    for b in pkt.data:
      result &= b.toHex(2)
    result &= "\"}"

  result &= "],\"packet_count\":" & $pcap.packets.len & "}"

proc pcapPacketCount*(filename: string): int =
  ## Quick packet count without loading all data into memory at once.
  ## Still reads the file, but only counts packet headers.
  let pcap = readPcap(filename)
  return pcap.packets.len
