// Transport abstractions shared by the resolver and the native socket adapters.

///|
pub enum TransportError {
  Timeout(Int)
  Truncated
  ConnectionFailed(String)
  SendFailed(String)
  RecvFailed(String)
  InvalidServer(String)
  InvalidMessage(String)
} derive(Debug, Eq)

///|
pub fn transport_error_message(err : TransportError) -> String {
  match err {
    Timeout(ms) => "Timeout after " + ms.to_string() + "ms"
    Truncated => "UDP response truncated and TCP fallback failed"
    ConnectionFailed(reason) => "Connection failed: " + reason
    SendFailed(reason) => "Send failed: " + reason
    RecvFailed(reason) => "Receive failed: " + reason
    InvalidServer(reason) => "Invalid DNS server: " + reason
    InvalidMessage(reason) => "Invalid DNS message: " + reason
  }
}

///|
pub fn is_retryable(err : TransportError) -> Bool {
  match err {
    Timeout(_) | ConnectionFailed(_) | RecvFailed(_) => true
    _ => false
  }
}

///|
pub fn is_timeout(err : TransportError) -> Bool {
  match err {
    Timeout(_) => true
    _ => false
  }
}

///|
/// An injectable asynchronous DNS transport.
///
/// The callback boundary keeps the resolver independent from socket backends and
/// makes deterministic resolver tests possible without a network connection.
pub(all) struct DnsTransport {
  request : async (Array[Byte]) -> Result[Array[Byte], TransportError]
  udp_payload_size : UInt16
}

///|
pub fn DnsTransport::from_request(
  request : async (Array[Byte]) -> Result[Array[Byte], TransportError],
  udp_payload_size? : UInt16 = 1232,
) -> DnsTransport {
  { request, udp_payload_size }
}

///|
pub async fn DnsTransport::send(
  self : DnsTransport,
  payload : Array[Byte],
) -> Result[Array[Byte], TransportError] {
  (self.request)(payload)
}

///|
pub fn DnsTransport::max_payload(self : DnsTransport) -> UInt16 {
  self.udp_payload_size
}

///|
fn array_to_bytes(payload : Array[Byte]) -> Bytes {
  FixedArray::from_array(payload).unsafe_reinterpret_as_bytes()
}

///|
fn bytes_to_array(payload : Bytes) -> Array[Byte] {
  Array::from_fixed_array(payload.to_fixedarray())
}

///|
pub struct TransportStats {
  bytes_sent : Ref[Int]
  bytes_received : Ref[Int]
  packets_sent : Ref[Int]
  packets_received : Ref[Int]
  errors : Ref[Int]
  timeouts : Ref[Int]
  truncations : Ref[Int]
}

///|
pub fn TransportStats::new() -> TransportStats {
  {
    bytes_sent: Ref(0),
    bytes_received: Ref(0),
    packets_sent: Ref(0),
    packets_received: Ref(0),
    errors: Ref(0),
    timeouts: Ref(0),
    truncations: Ref(0),
  }
}

///|
pub fn TransportStats::record_send(self : TransportStats, bytes : Int) -> Unit {
  self.bytes_sent.val += bytes
  self.packets_sent.val += 1
}

///|
pub fn TransportStats::record_recv(self : TransportStats, bytes : Int) -> Unit {
  self.bytes_received.val += bytes
  self.packets_received.val += 1
}

///|
pub fn TransportStats::record_error(
  self : TransportStats,
  err : TransportError,
) -> Unit {
  self.errors.val += 1
  if is_timeout(err) {
    self.timeouts.val += 1
  }
  if err is Truncated {
    self.truncations.val += 1
  }
}

///|
pub fn TransportStats::average_send_size(self : TransportStats) -> Int {
  if self.packets_sent.val == 0 {
    0
  } else {
    self.bytes_sent.val / self.packets_sent.val
  }
}