///|
/// UDP seam used by voice discovery and media transport.
pub(open) trait VoiceUdp {
  async fn recv(Self, FixedArray[Byte]) -> Int
  async fn send(Self, Bytes) -> Unit
  async fn close(Self) -> Unit
}

///|
/// Errors raised by voice UDP setup and IP discovery.
pub(all) suberror VoiceUdpError {
  InvalidDiscoveryPacket(reason~ : String)
  IpDiscoveryFailed(attempts~ : Int, reason~ : String)
} derive(Debug, Eq)

///|
priv suberror VoiceDiscoveryTimeout {
  VoiceDiscoveryTimeout
} derive(Debug)

///|
priv struct SocketVoiceUdp {
  client : @socket.UdpClient
}

///|
impl VoiceUdp for SocketVoiceUdp with fn recv(self, buffer) {
  self.client.recv(buffer)
}

///|
impl VoiceUdp for SocketVoiceUdp with fn send(self, bytes) {
  self.client.send(bytes)
}

///|
impl VoiceUdp for SocketVoiceUdp with fn close(self) {
  self.client.close()
}

///|
/// Open a connected UDP socket for a Discord voice server.
pub async fn open_voice_udp(ip : String, port : Int) -> &VoiceUdp {
  let address = @socket.Addr::resolve(ip, port~)
  let client = @socket.UdpClient(address)
  SocketVoiceUdp::{ client, }
}

///|
/// Build Discord's 74-byte voice IP discovery request.
pub fn build_ip_discovery_request(ssrc : UInt) -> Bytes {
  let bytes : Array[Byte] = [0, 1, 0, 70]
  bytes.push((ssrc >> 24).to_byte())
  bytes.push((ssrc >> 16).to_byte())
  bytes.push((ssrc >> 8).to_byte())
  bytes.push(ssrc.to_byte())
  for _ in 8..<74 {
    bytes.push(0)
  }
  Bytes::from_array(bytes)
}

///|
fn read_u32_be(bytes : Bytes, offset : Int) -> UInt {
  (bytes[offset].to_uint() << 24) |
  (bytes[offset + 1].to_uint() << 16) |
  (bytes[offset + 2].to_uint() << 8) |
  bytes[offset + 3].to_uint()
}

///|
/// Parse Discord's 74-byte voice IP discovery response.
pub fn parse_ip_discovery_response(
  bytes : Bytes,
  expected_ssrc~ : UInt,
) -> (String, Int) raise VoiceUdpError {
  if bytes.length() != 74 {
    raise InvalidDiscoveryPacket(reason="response must contain 74 bytes")
  }
  let packet_type = (bytes[0].to_int() << 8) | bytes[1].to_int()
  let payload_length = (bytes[2].to_int() << 8) | bytes[3].to_int()
  if packet_type != 2 || payload_length != 70 {
    raise InvalidDiscoveryPacket(
      reason="expected discovery response type 2 with length 70",
    )
  }
  if read_u32_be(bytes, 4) != expected_ssrc {
    raise InvalidDiscoveryPacket(reason="response SSRC does not match request")
  }
  let mut terminator = -1
  for index in 8..<72 {
    if bytes[index] == 0 {
      terminator = index
      break
    }
  }
  if terminator < 0 {
    raise InvalidDiscoveryPacket(
      reason="response address is not NUL terminated",
    )
  }
  let address = @utf8.decode(bytes[8:terminator]) catch {
    _ => raise InvalidDiscoveryPacket(reason="response address is not UTF-8")
  }
  if address.is_empty() {
    raise InvalidDiscoveryPacket(reason="response address is empty")
  }
  let port = (bytes[72].to_int() << 8) | bytes[73].to_int()
  (address, port)
}

///|
/// Run voice IP discovery, retrying only receive timeouts.
pub async fn discover_external_address(
  udp : &VoiceUdp,
  ssrc : UInt,
  retries? : Int = 3,
  timeout_ms? : Int = 1000,
) -> (String, Int) {
  if retries <= 0 || timeout_ms <= 0 {
    raise VoiceUdpError::IpDiscoveryFailed(
      attempts=0,
      reason="retries and timeout_ms must be positive",
    )
  }
  let request = build_ip_discovery_request(ssrc)
  for attempt in 0.. udp.recv(buffer),
      error=VoiceDiscoveryTimeout,
    ) catch {
      VoiceDiscoveryTimeout => -1
      error => raise error
    }
    if received < 0 {
      if attempt + 1 == retries {
        raise VoiceUdpError::IpDiscoveryFailed(
          attempts=retries,
          reason="timed out waiting for discovery response",
        )
      }
      continue
    }
    let response = Bytes::from_array(buffer[:received])
    return parse_ip_discovery_response(response, expected_ssrc=ssrc)
  }
  raise VoiceUdpError::IpDiscoveryFailed(
    attempts=retries,
    reason="discovery exhausted",
  )
}