///|
/// Single-flight IPMI 2.0 LAN client. Only suite 17 and User privilege are
/// supported. No password or key appears in a derived Show/Debug implementation.
pub struct Client {
  priv socket : @socket.UdpClient
  priv console_id : UInt32
  priv bmc_id : UInt32
  priv keys : SessionKeys
  priv timeout_ms : Int32
  priv mut send_sequence : UInt32
  priv mut receive_sequence : UInt32
  priv mut request_sequence : Int32
  priv mut busy : Bool
  priv mut closed : Bool
}

///|
async fn datagram(
  socket : @socket.UdpClient,
  request : Bytes,
  timeout_ms : Int32,
) -> Bytes {
  @async.with_timeout(timeout_ms, async fn() {
    socket.send(request)
    let buffer : FixedArray[Byte] = FixedArray::make(2048, 0)
    let count = socket.recv(buffer)
    guard count <= 1120 else { raise invalid("oversized RMCP+ datagram") }
    Bytes::from_array(Array::makei(count, i => buffer[i]))
  })
}

///|
async fn handshake_exchange(
  socket : @socket.UdpClient,
  kind : Int32,
  payload : Bytes,
  timeout_ms : Int32,
) -> Bytes {
  let packet = decode_handshake(
    datagram(socket, encode_handshake(kind, payload), timeout_ms),
  )
  guard packet.payload_type == kind + 1 else {
    raise invalid("unexpected handshake response type")
  }
  packet.payload
}

///|
fn validate_destination(addr : @socket.Addr) -> Unit raise ProtocolError {
  guard addr.port() >= 1 && !addr.is_multicast() else {
    raise invalid("a non-multicast peer and nonzero port are required")
  }
  if addr.is_ipv6() {
    let text = addr.to_string()
    guard !text.has_prefix("[::]:") &&
      !text.has_prefix("[::%") &&
      !text.has_prefix("[::ffff:0:0]") &&
      !text.has_prefix("[::ffff:ffff:ffff]") else {
      raise invalid(
        "unspecified or limited-broadcast destination is not allowed",
      )
    }
  } else {
    guard addr.ip() != 0U && addr.ip() != 0xffffffffU else {
      raise invalid(
        "unspecified or limited-broadcast destination is not allowed",
      )
    }
  }
}

///|
/// Connect to one explicit peer address, authenticate RAKP2/4, and request
/// User privilege. No anonymous access, Kg override, retry or downgrade fallback.
/// Each exchange has a timeout (default 2000 ms, range 1..60000).
/// Username/password are raw bytes. Nothing is written to hardware configuration.
pub async fn Client::connect(
  addr : @socket.Addr,
  username : Bytes,
  password : Bytes,
  timeout_ms~ : Int32 = 2000,
) -> Client {
  guard timeout_ms >= 1 && timeout_ms <= 60000 else {
    raise invalid("timeout 1..60000 ms required")
  }
  validate_destination(addr)
  // Validate credentials before opening a socket or consuming network resources.
  ignore(handshake(username, password, 1U, Bytes::make(16, 0)))
  let random = @crypto.random(21)
  let console_id = u32(random, 0)
  let h = handshake(username, password, console_id, slice(random, 4, 20))
  let tag = random[20].to_int()
  let socket = @socket.UdpClient(addr)
  errdefer socket.close()
  let open = decode_open_session(
    handshake_exchange(
      socket,
      0x10,
      open_session_request(tag, console_id),
      timeout_ms,
    ),
    tag,
    console_id,
  )
  let challenge = decode_rakp2(
    handshake_exchange(
      socket,
      0x12,
      rakp1_request(tag, open.bmc_id, h.console_random, username),
      timeout_ms,
    ),
    tag,
    console_id,
  )
  let keys = verify_rakp2(h, open.bmc_id, challenge)
  let confirmation = decode_rakp4(
    handshake_exchange(
      socket,
      0x14,
      rakp3_request(tag, open.bmc_id, rakp3_code(h, challenge)),
      timeout_ms,
    ),
    tag,
    console_id,
  )
  verify_rakp4(h, open.bmc_id, challenge, keys, confirmation)
  let client : Client = {
    socket,
    console_id,
    bmc_id: open.bmc_id,
    keys,
    timeout_ms,
    send_sequence: 1U,
    receive_sequence: 0U,
    request_sequence: 0,
    busy: false,
    closed: false,
  }
  errdefer client.abort()
  let privilege = client.exchange(6, 0x3b, b"\x02")
  guard privilege.completion_code == 0 && privilege.data == b"\x02" else {
    raise invalid("BMC refused User session privilege")
  }
  client
}

///|
async fn Client::exchange(
  self : Client,
  netfn : Int32,
  command : Int32,
  data : Bytes,
) -> Response {
  guard !self.closed else { raise invalid("session is closed") }
  guard !self.busy else { raise invalid("only one request may be in flight") }
  self.busy = true
  defer {
    self.busy = false
  }
  errdefer self.abort()
  guard self.send_sequence != 0U && self.send_sequence < 0xffffffffU else {
    raise invalid("session sequence exhausted; reconnect required")
  }
  let request_sequence = self.request_sequence
  let request = encode_request(netfn, command, request_sequence, data)
  let packet = protect_payload(
    request,
    self.bmc_id,
    self.send_sequence,
    self.keys,
    @crypto.random(16),
  )
  // Never reuse a sequence or IV after an uncertain exchange outcome.
  self.send_sequence += 1U
  self.request_sequence = (request_sequence + 1) & 63
  let (sequence, clear) = unprotect_payload(
    datagram(self.socket, packet, self.timeout_ms),
    self.console_id,
    self.receive_sequence,
    self.keys,
  )
  let response = decode_response(clear, netfn, command, request_sequence)
  self.receive_sequence = sequence
  response
}

///|
/// Execute one of the three explicitly read-only command variants.
/// Any I/O, authentication, identity or framing failure closes the session.
pub async fn Client::read(self : Client, command : ReadCommand) -> Response {
  match command {
    GetDeviceId => self.exchange(6, 1, b"")
    GetChassisStatus => self.exchange(0, 1, b"")
    GetSensorReading(sensor) =>
      self.exchange(4, 0x2d, Bytes::from_array([sensor]))
  }
}

///|
/// Close the remote session and always close the local socket. Idempotent once
/// closed. A busy session must be aborted or allowed to complete first.
pub async fn Client::close(self : Client) -> Unit {
  if self.closed {
    return
  }
  guard !self.busy else {
    raise invalid("cannot gracefully close a busy session")
  }
  defer self.abort()
  let data : Array[Byte] = []
  put32(data, self.bmc_id)
  let response = self.exchange(6, 0x3c, Bytes::from_array(data))
  guard response.completion_code == 0 else {
    raise invalid("BMC rejected Close Session")
  }
}

///|
/// Close the local socket immediately without sending a command. Safe to repeat.
/// The BMC may retain its session until its own inactivity timeout.
pub fn Client::abort(self : Client) -> Unit {
  if !self.closed {
    self.closed = true
    self.socket.close()
  }
}