///|
/// Export names and descriptions are preserved as protocol bytes.
pub(all) struct ExportEntry {
  name : Bytes
  description : Bytes
}

///|
/// Information obtained with `NBD_OPT_INFO`, without entering transmission mode.
pub(all) struct RemoteExportInfo {
  size : UInt64
  flags : UInt32
}

///|
/// Inspect an export without acquiring a transmission-mode connection.
///
/// This is useful for status commands and preflight checks. It asks only for
/// `NBD_INFO_EXPORT`; servers may send other informational replies, which are
/// ignored after their framing is validated.
pub async fn inspect_export(
  host : String,
  port : Int32,
  name : Bytes,
  timeout_ms~ : Int32 = 30000,
) -> RemoteExportInfo {
  if port <= 0 || port > 65535 || name.length() > 4096 || timeout_ms <= 0 {
    raise InvalidArgument("invalid port, export name, or timeout")
  }
  @async.with_timeout(timeout_ms, () => {
    let tcp = @socket.Tcp::connect_to_host(host, port~)
    defer tcp.close()
    ignore(client_handshake(tcp))
    let body : Array[Byte] = []
    put32(body, name.length().reinterpret_as_uint())
    for byte in name {
      body.push(byte)
    }
    put16(body, 1U)
    put16(body, 0U)
    send_option(tcp, 6U, Bytes::from_array(body))
    let mut result : RemoteExportInfo? = None
    for _ in 0..<256 {
      let header = tcp.read_exactly(20)
      if get64(header, 0) != 0x0003e889045565a9UL || get32(header, 8) != 6U {
        raise Protocol("invalid export information reply")
      }
      let kind = get32(header, 12)
      let length = get32(header, 16)
      if length > 65536U {
        raise Protocol("export information reply exceeds limit")
      }
      let data = tcp.read_exactly(length.reinterpret_as_int())
      if (kind & 0x80000000U) != 0U {
        raise Server(kind)
      }
      if kind == 1U {
        if length != 0U || result is None {
          raise Protocol("incomplete export information reply")
        }
        send_option(tcp, 2U, b"")
        return result.unwrap()
      }
      if kind != 3U || length < 2U {
        raise Protocol("unexpected export information reply")
      }
      if get16(data, 0) == 0U {
        if length != 12U || result is Some(_) {
          raise Protocol("invalid duplicate export information")
        }
        let flags = get16(data, 10)
        if (flags & 1U) == 0U {
          raise Protocol("server did not set HAS_FLAGS")
        }
        result = Some({ size: get64(data, 2), flags, })
      }
    }
    raise Protocol("too many export information replies")
  })
}

///|
/// List exports over a separate connection without entering transmission mode.
pub async fn list_exports(
  host : String,
  port : Int32,
  timeout_ms~ : Int32 = 30000,
) -> Array[ExportEntry] {
  if port <= 0 || port > 65535 || timeout_ms <= 0 {
    raise InvalidArgument("invalid port or timeout")
  }
  @async.with_timeout(timeout_ms, () => {
    let tcp = @socket.Tcp::connect_to_host(host, port~)
    defer tcp.close()
    ignore(client_handshake(tcp))
    send_option(tcp, 3U, b"")
    let exports : Array[ExportEntry] = []
    let mut total = 0U
    for _ in 0..<1025 {
      let hdr = tcp.read_exactly(20)
      if get64(hdr, 0) != 0x0003e889045565a9UL || get32(hdr, 8) != 3U {
        raise Protocol("invalid export list reply")
      }
      let kind = get32(hdr, 12)
      let len = get32(hdr, 16)
      if len > 65536U || total > 1048576U - len {
        raise Protocol("export list exceeds limit")
      }
      total += len
      let data = tcp.read_exactly(len.reinterpret_as_int())
      if (kind & 0x80000000U) != 0U {
        raise Server(kind)
      }
      if kind == 1U {
        if len != 0U {
          raise Protocol("invalid export list acknowledgement")
        }
        send_option(tcp, 2U, b"")
        return exports
      }
      if kind != 2U || len < 4U || exports.length() >= 1024 {
        raise Protocol("invalid export list entry")
      }
      let name_len = get32(data, 0)
      if name_len > 4096U || name_len > len - 4U {
        raise Protocol("invalid export name length")
      }
      let end = 4 + name_len.reinterpret_as_int()
      exports.push({
        name: data[4:end].to_owned(),
        description: data[end:].to_owned(),
      })
    }
    raise Protocol("too many export entries")
  })
}