///|
/// Malformed, unsupported, mismatched, or failed IPMI exchange.
#derive(@debug.Debug)
pub suberror ProtocolError {
  ProtocolError(String)
}

///|
pub extend ProtocolError with @debug.Debug::{to_repr}

///|
fn invalid(message : String) -> ProtocolError {
  ProtocolError(message)
}

///|
fn byte(value : Int32) -> Byte {
  value.to_byte()
}

///|
fn slice(data : Bytes, start : Int32, end : Int32) -> Bytes {
  Bytes::from_array(Array::makei(end - start, i => data[start + i]))
}

///|
fn u32(data : Bytes, at : Int32) -> UInt32 {
  data[at].to_uint() |
  (data[at + 1].to_uint() << 8) |
  (data[at + 2].to_uint() << 16) |
  (data[at + 3].to_uint() << 24)
}

///|
fn put32(out : Array[Byte], value : UInt32) -> Unit {
  for shift in [0, 8, 16, 24] {
    out.push((value >> shift).to_byte())
  }
}

///|
fn append(out : Array[Byte], data : Bytes) -> Unit {
  for item in data {
    out.push(item)
  }
}

///|
fn checksum(data : ArrayView[Byte]) -> Byte {
  let mut sum = 0
  for item in data {
    sum = (sum + item.to_int()) & 255
  }
  byte((256 - sum) & 255)
}

///|
/// IPMI LAN request. Sequence is a six-bit value; data is bounded to 1024 bytes.
/// This low-level encoder does not transmit anything.
pub fn encode_request(
  netfn : Int32,
  command : Int32,
  sequence : Int32,
  data : Bytes,
) -> Bytes raise ProtocolError {
  guard netfn >= 0 && netfn <= 62 && netfn % 2 == 0 else {
    raise invalid("request netfn must be even and in 0..62")
  }
  guard command >= 0 && command <= 255 && sequence >= 0 && sequence <= 63 else {
    raise invalid("command or sequence out of range")
  }
  guard data.length() <= 1024 else {
    raise invalid("request data exceeds 1024 bytes")
  }
  let out : Array[Byte] = [0x20, byte(netfn << 2)]
  out.push(checksum(out[:]))
  out.push(0x81)
  out.push(byte(sequence << 2))
  out.push(byte(command))
  append(out, data)
  out.push(checksum(out[3:]))
  Bytes::from_array(out)
}

///|
#derive(Eq, @debug.Debug)
pub struct Response {
  completion_code : Int32
  data : Bytes
}

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

///|
pub extend Response with @debug.Debug::{to_repr}

///|
/// Validate both checksums and correlate addresses, LUNs, netfn, command and sequence.
/// Nonzero completion codes remain structured protocol results.
pub fn decode_response(
  data : Bytes,
  netfn : Int32,
  command : Int32,
  sequence : Int32,
) -> Response raise ProtocolError {
  guard netfn >= 0 &&
    netfn <= 62 &&
    netfn % 2 == 0 &&
    command >= 0 &&
    command <= 255 &&
    sequence >= 0 &&
    sequence <= 63 else {
    raise invalid("invalid expected response identity")
  }
  guard data.length() >= 8 && data.length() <= 1032 else {
    raise invalid("invalid IPMI response length")
  }
  let values = data.to_array()
  guard checksum(values[0:3]) == 0 && checksum(values[3:]) == 0 else {
    raise invalid("invalid IPMI checksum")
  }
  guard data[0] == 0x81 &&
    data[1].to_int() == (netfn + 1) << 2 &&
    data[3] == 0x20 &&
    data[4].to_int() == sequence << 2 &&
    data[5].to_int() == command else {
    raise invalid("IPMI response does not match request")
  }
  {
    completion_code: data[6].to_int(),
    data: slice(data, 7, data.length() - 1),
  }
}

///|
#derive(Eq, @debug.Debug)
pub struct DeviceId {
  device_id : Int32
  device_revision : Int32
  firmware_major : Int32
  firmware_minor_bcd : Int32
  firmware_available : Bool
  ipmi_major : Int32
  ipmi_minor : Int32
  manufacturer_id : UInt32
  product_id : Int32
  auxiliary_firmware : Bytes?
}

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

///|
pub extend DeviceId with @debug.Debug::{to_repr}

///|
/// Decode Get Device ID (Application netfn 06h, command 01h).
pub fn Response::device_id(self : Response) -> DeviceId raise ProtocolError {
  guard self.completion_code == 0 else {
    raise invalid("Get Device ID returned a failure completion code")
  }
  let d = self.data
  guard d.length() == 11 || d.length() == 15 else {
    raise invalid("Get Device ID payload must contain 11 or 15 bytes")
  }
  {
    device_id: d[0].to_int(),
    device_revision: d[1].to_int() & 15,
    firmware_major: d[2].to_int() & 127,
    firmware_minor_bcd: d[3].to_int(),
    firmware_available: (d[2].to_int() & 128) == 0,
    ipmi_major: d[4].to_int() & 15,
    ipmi_minor: d[4].to_int() >> 4,
    manufacturer_id: d[6].to_uint() |
    (d[7].to_uint() << 8) |
    (d[8].to_uint() << 16),
    product_id: d[9].to_int() | (d[10].to_int() << 8),
    auxiliary_firmware: if d.length() == 15 {
      Some(slice(d, 11, 15))
    } else {
      None
    },
  }
}

///|
/// Unauthenticated RMCP+ handshake envelope. Authenticated IPMI packets use a
/// different parser that verifies integrity before exposing plaintext.
#derive(Eq, @debug.Debug)
pub struct HandshakePacket {
  payload_type : Int32
  payload : Bytes
}

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

///|
pub extend HandshakePacket with @debug.Debug::{to_repr}

///|
pub fn encode_handshake(
  payload_type : Int32,
  payload : Bytes,
) -> Bytes raise ProtocolError {
  guard payload_type >= 0x10 && payload_type <= 0x15 else {
    raise invalid("not an RMCP+ handshake payload type")
  }
  guard payload.length() <= 1024 else {
    raise invalid("handshake payload too large")
  }
  let out : Array[Byte] = [0x06, 0, 0xff, 0x07, 0x06, byte(payload_type)]
  put32(out, 0U)
  put32(out, 0U)
  out.push(byte(payload.length() & 255))
  out.push(byte(payload.length() >> 8))
  append(out, payload)
  Bytes::from_array(out)
}

///|
pub fn decode_handshake(data : Bytes) -> HandshakePacket raise ProtocolError {
  guard data.length() >= 16 && data.length() <= 1040 else {
    raise invalid("invalid handshake datagram length")
  }
  guard data[0] == 6 &&
    data[1] == 0 &&
    data[2] == 0xff &&
    data[3] == 7 &&
    data[4] == 6 else {
    raise invalid("invalid RMCP+ header")
  }
  let kind = data[5].to_int()
  guard kind >= 0x10 && kind <= 0x15 && u32(data, 6) == 0 && u32(data, 10) == 0 else {
    raise invalid("invalid handshake envelope")
  }
  let length = data[14].to_int() | (data[15].to_int() << 8)
  guard data.length() == 16 + length else {
    raise invalid("RMCP+ payload length mismatch")
  }
  { payload_type: kind, payload: slice(data, 16, data.length()), }
}

///|
/// Open a User-privilege session with cipher suite 17 only:
/// RAKP-HMAC-SHA256, HMAC-SHA256-128, AES-CBC-128. No downgrade negotiation.
pub fn open_session_request(
  tag : Int32,
  console_id : UInt32,
) -> Bytes raise ProtocolError {
  guard tag >= 0 && tag <= 255 && console_id != 0U else {
    raise invalid("invalid session tag or zero console session ID")
  }
  let out : Array[Byte] = [byte(tag), 2, 0, 0]
  put32(out, console_id)
  for (kind, algorithm) in [(0, 3), (1, 4), (2, 1)] {
    out.append([byte(kind), 0, 0, 8, byte(algorithm), 0, 0, 0])
  }
  Bytes::from_array(out)
}

///|
#derive(Eq, @debug.Debug)
pub struct OpenSession {
  bmc_id : UInt32
}

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

///|
pub extend OpenSession with @debug.Debug::{to_repr}

///|
/// Reject a non-success status, wrong identity, excessive privilege, or downgrade.
pub fn decode_open_session(
  data : Bytes,
  tag : Int32,
  console_id : UInt32,
) -> OpenSession raise ProtocolError {
  guard tag >= 0 && tag <= 255 && console_id != 0U else {
    raise invalid("invalid expected session identity")
  }
  guard data.length() >= 8 else {
    raise invalid("truncated Open Session response")
  }
  guard data[0].to_int() == tag && u32(data, 4) == console_id else {
    raise invalid("Open Session response identity mismatch")
  }
  guard data[1] == 0 else { raise invalid("BMC rejected Open Session") }
  guard data.length() == 36 &&
    data[2] == 2 &&
    data[3] == 0 &&
    u32(data, 8) != 0U else {
    raise invalid("invalid Open Session response")
  }
  for (offset, kind, algorithm) in [(12, 0, 3), (20, 1, 4), (28, 2, 1)] {
    guard data[offset].to_int() == kind &&
      data[offset + 1] == 0 &&
      data[offset + 2] == 0 &&
      data[offset + 3] == 8 &&
      data[offset + 4].to_int() == algorithm &&
      data[offset + 5] == 0 &&
      data[offset + 6] == 0 &&
      data[offset + 7] == 0 else {
      raise invalid("BMC selected an unsupported cipher suite")
    }
  }
  { bmc_id: u32(data, 8), }
}