// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

// Spare connection ids (RFC 9000 §5.1, §19.15, §19.16). A peer that changes path changes its id too,
// so the two ends of the old path and the new cannot be linked by anyone watching; for that each
// end hands the other ids in advance. A server's come from its endpoint, since they have to be
// unique across connections, and this is the bookkeeping on either side of that.

///|
/// What a connection has given its peer, and what the peer has given it.
priv struct Ids {
  /// The spare ids this end has issued and not had retired, by sequence number.
  issued : Map[UInt64, Bytes]
  /// Whether the peer is done with the id from the handshake, which is sequence number 0.
  mut handshake_retired : Bool
  mut next_seq : UInt64
  /// How many the peer will take at once (its active_connection_id_limit).
  mut limit : UInt64
  /// Retired, and not yet forgotten by the endpoint.
  retired : Array[Bytes]
  /// What the peer has offered: sequence number, id and reset token.
  offered : Array[(UInt64, Bytes, Bytes)]
  /// The sequence number of the id being sent to.
  mut peer_seq : UInt64
  /// The id the peer last addressed this end by.
  mut heard_on : Bytes
}

///|
fn Ids::new() -> Ids {
  {
    issued: Map([]),
    handshake_retired: false,
    next_seq: 1UL,
    limit: 2UL,
    retired: [],
    offered: [],
    peer_seq: 0UL,
    heard_on: b"",
  }
}

///|
fn Ids::take_retired(self : Ids) -> Array[Bytes] {
  let out = self.retired.copy()
  self.retired.clear()
  out
}

///|
/// Whether this connection's peer is owed another id: the handshake's counts as one held, unless it
/// has been retired.
fn[A] Session::owes_id(self : Session[A]) -> Bool {
  guard self.phase == Active && self.handshake_done() else { return false }
  let cap = if self.ids.limit < self.cfg.connection_ids {
    self.ids.limit
  } else {
    self.cfg.connection_ids
  }
  let held = self.ids.issued.length().to_uint64() +
    (if self.ids.handshake_retired { 0UL } else { 1UL })
  held < cap
}

///|
/// Whether the handshake is over, so 1-RTT frames can go out.
fn[A] Session::handshake_done(self : Session[A]) -> Bool {
  match self.tls {
    Some(tls) => tls.is_connected()
    None => self.app_tx is Some(_)
  }
}

///|
fn[A] Session::issue_id(self : Session[A], cid : Bytes, token : Bytes) -> Unit {
  let seq = self.ids.next_seq
  self.ids.next_seq = seq + 1UL
  self.ids.issued[seq] = cid
  self.sender.queue_control(
    NewConnectionId(seq~, retire_prior_to=0UL, conn_id=cid, reset_token=token),
  )
}

///|
/// The peer is done with an id of ours. Naming one never issued is a protocol violation (§19.16).
fn[A] Session::on_retire_id(
  self : Session[A],
  seq : UInt64,
  now : Int64,
) -> Unit {
  if seq >= self.ids.next_seq {
    return self.close(protocol_violation, b"retired an id never issued", now)
  }
  // The handshake's id stays routable: it is the connection's key, and retiring it frees nothing.
  if seq == 0UL {
    self.ids.handshake_retired = true
  } else if self.ids.issued.get(seq) is Some(cid) {
    self.ids.issued.remove(seq)
    self.ids.retired.push(cid)
  }
}

///|
/// An id from the peer, for when this end has to change the one it sends to (§5.1.2). Anything
/// below `retire_prior_to` has to go, the one in use with it.
fn[A] Session::on_new_id(
  self : Session[A],
  seq : UInt64,
  retire_prior_to : UInt64,
  cid : Bytes,
  token : Bytes,
  now : Int64,
) -> Unit {
  if retire_prior_to > seq {
    return self.close(
      frame_encoding_error, b"retire_prior_to past the sequence", now,
    )
  }
  if self.ids.offered.iter().all(entry => entry.0 != seq) &&
    seq > self.ids.peer_seq {
    self.ids.offered.push((seq, cid, token))
  }
  let kept = []
  for entry in self.ids.offered {
    if entry.0 < retire_prior_to {
      self.sender.queue_control(RetireConnectionId(entry.0))
    } else {
      kept.push(entry)
    }
  }
  self.ids.offered.clear()
  self.ids.offered.append(kept)
  if self.ids.peer_seq < retire_prior_to {
    ignore(self.next_peer_id())
  }
}

///|
/// Start sending to the next id the peer offered, and retire the one in use. False when it offered
/// none, in which case the connection goes on with the one it has (§9.5).
fn[A] Session::next_peer_id(self : Session[A]) -> Bool {
  guard self.ids.offered.length() > 0 else { return false }
  let (seq, cid, _) = self.ids.offered.remove(0)
  self.sender.queue_control(RetireConnectionId(self.ids.peer_seq))
  self.ids.peer_seq = seq
  self.peer_cid = cid
  true
}