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

// A QUIC connection's numbering and handshake-input core (RFC 9000 §12.3, RFC 9001
// §4.1.3). A connection keeps three independent packet-number spaces — Initial,
// Handshake, and Application (1-RTT) — and a CRYPTO byte stream per encryption level,
// each ordered on its own. This skeleton routes a received packet's number to its space
// (for acknowledgement) and a received CRYPTO frame to its level's reassembler (yielding
// the contiguous TLS handshake bytes to feed the handshake), and hands out send-side
// packet numbers per space. It holds no keys or socket — the key schedule and the async
// UDP transport wrap this pure core.

///|
/// A QUIC encryption level / packet-number space (RFC 9001 §4.1.1). 0-RTT shares the
/// Application space, so the three long-lived spaces are Initial, Handshake, and
/// Application.
#derive(Eq, Debug)
pub(all) enum Level {
  Initial
  Handshake
  Application
}

///|
/// A QUIC connection's per-space numbering/ACK state and per-level CRYPTO streams.
pub struct Conn {
  is_server : Bool
  initial_space : @packet.Space
  handshake_space : @packet.Space
  application_space : @packet.Space
  /// Further paths' packet-number spaces (draft-ietf-quic-multipath §2.3). Each path id after
  /// 0 numbers its packets from zero and acknowledges them separately; path 0 is
  /// `application_space`, so a connection with one path allocates nothing extra.
  path_spaces : Map[UInt64, @packet.Space]
  initial_crypto : @stream.Assembler
  handshake_crypto : @stream.Assembler
  application_crypto : @stream.Assembler
}

///|
/// A fresh connection (client or server) with empty spaces and CRYPTO streams.
pub fn Conn::new(is_server : Bool) -> Conn {
  {
    is_server,
    initial_space: @packet.Space::new(),
    handshake_space: @packet.Space::new(),
    application_space: @packet.Space::new(),
    path_spaces: Map([]),
    initial_crypto: @stream.Assembler::new(),
    handshake_crypto: @stream.Assembler::new(),
    application_crypto: @stream.Assembler::new(),
  }
}

///|
/// Whether this endpoint is the server.
pub fn Conn::is_server(self : Conn) -> Bool {
  self.is_server
}

///|
/// The packet-number space for `level`.
pub fn Conn::space(self : Conn, level : Level) -> @packet.Space {
  match level {
    Initial => self.initial_space
    Handshake => self.handshake_space
    Application => self.application_space
  }
}

///|
/// The packet-number space for `level` on path `path`. Only Application has one per path: the
/// handshake runs on path 0, and the draft gives 0-RTT and 1-RTT a space for each path id
/// (draft-ietf-quic-multipath §2.3). Path 0 is the connection's own space.
pub fn Conn::space_on(
  self : Conn,
  path : UInt64,
  level : Level,
) -> @packet.Space {
  if level != Level::Application || path == 0UL {
    return self.space(level)
  }
  match self.path_spaces.get(path) {
    Some(space) => space
    None => {
      let space = @packet.Space::new()
      self.path_spaces[path] = space
      space
    }
  }
}

///|
/// How many paths the connection numbers packets for, path 0 among them.
pub fn Conn::paths(self : Conn) -> Int32 {
  self.path_spaces.length() + 1
}

///|
/// The CRYPTO reassembler for `level`.
pub fn Conn::crypto_stream(self : Conn, level : Level) -> @stream.Assembler {
  match level {
    Initial => self.initial_crypto
    Handshake => self.handshake_crypto
    Application => self.application_crypto
  }
}

///|
/// Allocate the next packet number to send at `level`.
pub fn Conn::next_packet_number(self : Conn, level : Level) -> Int64 {
  self.space(level).next_packet_number()
}

///|
/// Record that a packet numbered `pn` arrived at `level`; `ack_eliciting` marks one that
/// must be acknowledged.
pub fn Conn::on_packet_received(
  self : Conn,
  level : Level,
  pn : Int64,
  ack_eliciting : Bool,
) -> Unit {
  self.space(level).on_packet_received(pn, ack_eliciting)
}

///|
/// Record the peer's acknowledgement of up to `largest` at `level`.
pub fn Conn::on_ack_received(
  self : Conn,
  level : Level,
  largest : Int64,
) -> Unit {
  self.space(level).on_ack_received(largest)
}

///|
/// Build the ACK frame owed at `level`, or `None` if nothing has been received there.
pub fn Conn::build_ack(
  self : Conn,
  level : Level,
  ack_delay : UInt64,
) -> @frame.Frame? {
  self.space(level).build_ack(ack_delay)
}

///|
/// Feed a CRYPTO frame's `data` at `offset` and `level` into that level's handshake
/// stream, returning the newly contiguous TLS handshake bytes now readable (empty while
/// an earlier gap is still outstanding). CRYPTO streams carry no fin.
pub fn Conn::on_crypto_frame(
  self : Conn,
  level : Level,
  offset : UInt64,
  data : Bytes,
) -> Bytes raise @stream.Refused {
  let stream = self.crypto_stream(level)
  // A fragment past the window would have to be held across the gap it leaves. §7.5 lets an
  // endpoint refuse that, and a handshake with a gap this wide is not going to finish.
  if offset > stream.limit() {
    raise @stream.Exceeded(limit=stream.limit(), got=offset)
  }
  stream.insert(offset, data, fin=false)
  stream.read()
}

///|
pub extend Level with Debug::{to_repr}

///|
pub extend Level with Eq::{not_equal, equal}