function random_index(arr)
    return rand(keys(arr))
end


function findswapablequbits(net, node, pred_low, pred_high, choose_low, choose_high, chooseslots; agelimit=nothing)
    reg = net[node]
    low_queryresults  = [
        n for n in queryall(reg, EntanglementCounterpart, pred_low, ❓, ❓; locked=false, assigned=true)
        if isnothing(agelimit) || !isolderthan(n.slot, agelimit) # TODO add age limit to query and queryall
    ]
    high_queryresults = [
        n for n in queryall(reg, EntanglementCounterpart, pred_high, ❓, ❓; locked=false, assigned=true)
        if isnothing(agelimit) || !isolderthan(n.slot, agelimit) # TODO add age limit to query and queryall
    ]

    choosefunc = chooseslots isa Vector{Int} ? in(chooseslots) : chooseslots
    low_queryresults = [qr for qr in low_queryresults if choosefunc(qr.slot.idx)]
    high_queryresults = [qr for qr in high_queryresults if choosefunc(qr.slot.idx)]

    (isempty(low_queryresults) || isempty(high_queryresults)) && return nothing
    il = choose_low((qr.tag[2] for qr in low_queryresults)) # TODO make [2] into a nice named property
    ih = choose_high((qr.tag[2] for qr in high_queryresults))
    return (low_queryresults[il], high_queryresults[ih])
end


"""
$TYPEDEF

A protocol, running at a given node, that finds swappable entangled pairs and performs the swap.

Consider setting an `agelimit` on qubits
and using it together with the cutoff protocol, [`CutoffProt`](@ref),
which deletes qubits that are about to go past their cutoff/retention time.

$TYPEDFIELDS
"""
@kwdef struct SwapperProt <: AbstractProtocol
    """time-and-schedule-tracking instance from `ConcurrentSim`"""
    sim::Simulation
    """a network graph of registers"""
    net::RegisterNet
    """the vertex of the node where swapping is happening"""
    node::Int
    """function `Int->Bool` or a vector of allowed slot indices, specifying the slots to take among swappable slots in the node"""
    chooseslots::Union{Vector{Int},Function} = alwaystrue
    """the vertex of one of the remote nodes for the swap, arbitrarily referred to as the "low" node (or a predicate function or a wildcard); if you are working on a repeater chain, a good choice is `<(current_node)`, i.e. any node to the "left" of the current node"""
    nodeL::QueryArgs = ❓
    """the vertex of the other remote node for the swap, the "high" counterpart of `nodeL`; if you are working on a repeater chain, a good choice is `>(current_node)`, i.e. any node to the "right" of the current node"""
    nodeH::QueryArgs = ❓
    """the `nodeL` predicate can return many positive candidates; `chooseL` picks one of them (by index into the array of filtered `nodeL` results), defaults to a random pick `arr->rand(keys(arr))`; if you are working on a repeater chain a good choice is `argmin`, i.e. the node furthest to the "left" """
    chooseL::Function = random_index
    """the `nodeH` counterpart for `chooseH`; if you are working on a repeater chain a good choice is `argmax`, i.e. the node furthest to the "right" """
    chooseH::Function = random_index
    """fixed "busy time" duration immediately before starting entanglement generation attempts"""
    local_busy_time::Float64 = 0.0 # TODO the gates should have that busy time built in
    """how long to wait before retrying to lock qubits if no qubits are available (`nothing` for queuing up and waiting)"""
    retry_lock_time::Union{Float64,Nothing} = 0.1
    """how many rounds of this protocol to run (`-1` for infinite))"""
    rounds::Int = -1
    """what is the oldest a qubit should be to be picked for a swap (to avoid swapping with qubits that are about to be deleted, the agelimit should be shorter than the retention time of the cutoff protocol) (`nothing` for no limit) -- you probably want to use [`CutoffProt`](@ref) if you have an agelimit"""
    agelimit::Union{Float64,Nothing} = nothing
    """maximum number of history tags to retain per slot in FIFO order (`nothing` for unbounded retention)"""
    max_history_per_slot::Union{Int,Nothing} = 3

    function SwapperProt(sim, net, node, chooseslots, nodeL, nodeH, chooseL, chooseH, local_busy_time, retry_lock_time, rounds, agelimit, max_history_per_slot)
        @domain local_busy_time ≥ 0
        @domain isnothing(retry_lock_time) || retry_lock_time > 0
        @domain rounds == -1 || rounds ≥ 0
        @domain isnothing(agelimit) || agelimit ≥ 0
        @domain isnothing(max_history_per_slot) || max_history_per_slot ≥ 0
        return new(sim, net, node, chooseslots, nodeL, nodeH, chooseL, chooseH, local_busy_time, retry_lock_time, rounds, agelimit, max_history_per_slot)
    end
end

protocol_catalog_metadata(::Type{SwapperProt}) = (
    attachment = :node,
    attachment_fields = (node=:node,),
    required_fields = (),
)

#TODO "convenience constructor for the missing things and finish this docstring"
function SwapperProt(sim::Simulation, net::RegisterNet, node::Int; kwargs...)
    return SwapperProt(;sim, net, node, kwargs...)
end

SwapperProt(net::RegisterNet, node::Int; kwargs...) = SwapperProt(get_time_tracker(net), net, node; kwargs...)

function _enforce_history_cap!(slot::RegRef, max_history_per_slot::Union{Int,Nothing})
    isnothing(max_history_per_slot) && return nothing
    histories = queryall(slot, EntanglementHistory, ❓, ❓, ❓, ❓, ❓, ❓, ❓; filo=false)
    for history in Iterators.take(histories, max(0, length(histories) - max_history_per_slot))
        untag!(slot, history.id)
    end
    return nothing
end

@resumable function (prot::SwapperProt)()
    rounds = prot.rounds
    round = 1
    while rounds != 0
        qubit_pair_ = findswapablequbits(prot.net, prot.node, prot.nodeL, prot.nodeH, prot.chooseL, prot.chooseH, prot.chooseslots; agelimit=prot.agelimit)
        if isnothing(qubit_pair_)
            if isnothing(prot.retry_lock_time)
                @debug(
                    "Swappable pair unavailable",
                    _group=LOG_GROUPS.protocol,
                    event=:swappable_pair_unavailable,
                    protocol_log_context(prot)...,
                    round=round,
                    wait_mode=:tag_change,
                    retry_after_s=nothing,
                )
                @yield onchange(prot.net[prot.node], Tag)
            else
                @debug(
                    "Swappable pair unavailable",
                    _group=LOG_GROUPS.protocol,
                    event=:swappable_pair_unavailable,
                    protocol_log_context(prot)...,
                    round=round,
                    wait_mode=:fixed_delay,
                    retry_after_s=prot.retry_lock_time,
                )
                @yield timeout(prot.sim, prot.retry_lock_time::Float64)
            end
            continue
        end
        # The compiler is not smart enough to figure out that qubit_pair_ is not nothing, so we need to tell it explicitly. A new variable name is needed due to @resumable.
        qubit_pair = qubit_pair_::NTuple{2, QueryOnRegResult}

        (q1, tag1) = qubit_pair[1].slot, qubit_pair[1].tag
        (q2, tag2) = qubit_pair[2].slot, qubit_pair[2].tag
        if q1.idx == q2.idx
            @error(
                "Counterpart tags conflict on one slot",
                _group=LOG_GROUPS.protocol,
                event=:counterpart_tag_conflict,
                protocol_log_context(prot)...,
                slot=q1.idx,
                remote_nodes=(tag1[2], tag2[2]),
                pair_id=(tag1[4], tag2[4]),
            )
            if isnothing(prot.retry_lock_time)
                @yield onchange(prot.net[prot.node], Tag)
            else
                @yield timeout(prot.sim, prot.retry_lock_time::Float64)
            end
            continue
        end

        @yield lock(q1) & lock(q2) # this should not really need a yield thanks to `findswapablequbits` which queries only for unlocked qubits, but it is better to be defensive

        # double check that in between the initial query and the lock
        # we did not have another process use these qubits for something else
        current1_ = query(q1, tag1; assigned=true)
        current2_ = query(q2, tag2; assigned=true)
        if isnothing(current1_) || isnothing(current2_)
            unlock(q1)
            unlock(q2)
            continue
        end
        current1 = current1_::QueryOnRegResult
        current2 = current2_::QueryOnRegResult

        untag!(q1, current1.id)
        # store a history of whom we were entangled to: remote_node_idx, remote_slot_idx, remote_swapnode_idx, remote_swapslot_idx, local_swap_idx, local_chunk_id, swapped_chunk_id
        tag!(q1, EntanglementHistory, tag1[2], tag1[3], tag2[2], tag2[3], q2.idx, tag1[4], tag2[4])
        _enforce_history_cap!(q1, prot.max_history_per_slot)

        untag!(q2, current2.id)
        # store a history of whom we were entangled to: remote_node_idx, remote_slot_idx, remote_swapnode_idx, remote_swapslot_idx, local_swap_idx, local_chunk_id, swapped_chunk_id
        tag!(q2, EntanglementHistory, tag2[2], tag2[3], tag1[2], tag1[3], q1.idx, tag2[4], tag1[4])
        _enforce_history_cap!(q2, prot.max_history_per_slot)

        uptotime!((q1, q2), now(prot.sim))
        swapcircuit = LocalEntanglementSwap()
        xmeas, zmeas = swapcircuit(q1, q2)
        # send from here to new entanglement counterpart:
        # tag with EntanglementUpdateX target_pair_id, other_pair_id, past_local_node, past_local_slot_idx past_remote_slot_idx new_remote_node, new_remote_slot, correction
        msg1 = Tag(EntanglementUpdateX, tag1[4], tag2[4], prot.node, q1.idx, tag1[3], tag2[2], tag2[3], Int(xmeas))
        put!(channel(prot.net, prot.node=>tag1[2]; permit_forward=true), msg1)
        @debug(
            "Sent a swap update",
            _group=LOG_GROUPS.protocol,
            event=:swap_update_sent,
            protocol_log_context(prot)...,
            round=round,
            src_slot=q1.idx,
            dst_node=tag1[2],
            dst_slot=tag1[3],
            message_type=:EntanglementUpdateX,
            pair_id=tag1[4],
            correction=Int(xmeas),
        )
        # send from here to new entanglement counterpart:
        # tag with EntanglementUpdateZ target_pair_id, other_pair_id, past_local_node, past_local_slot_idx past_remote_slot_idx new_remote_node, new_remote_slot, correction
        msg2 = Tag(EntanglementUpdateZ, tag2[4], tag1[4], prot.node, q2.idx, tag2[3], tag1[2], tag1[3], Int(zmeas))
        put!(channel(prot.net, prot.node=>tag2[2]; permit_forward=true), msg2)
        @debug(
            "Sent a swap update",
            _group=LOG_GROUPS.protocol,
            event=:swap_update_sent,
            protocol_log_context(prot)...,
            round=round,
            src_slot=q2.idx,
            dst_node=tag2[2],
            dst_slot=tag2[3],
            message_type=:EntanglementUpdateZ,
            pair_id=tag2[4],
            correction=Int(zmeas),
        )
        @yield timeout(prot.sim, prot.local_busy_time)
        unlock(q1)
        unlock(q2)
        rounds==-1 || (rounds -= 1)
        round += 1
    end
end
