///|
fn push_u16_be(output : Array[Byte], value : Int) -> Unit {
  output.push(((value >> 8) & 0xFF).to_byte())
  output.push((value & 0xFF).to_byte())
}

///|
fn push_u32_be(output : Array[Byte], value : Int) -> Unit {
  output.push(((value >> 24) & 0xFF).to_byte())
  output.push(((value >> 16) & 0xFF).to_byte())
  output.push(((value >> 8) & 0xFF).to_byte())
  output.push((value & 0xFF).to_byte())
}

///|
fn read_u16_be(data : Bytes, offset : Int) -> Int {
  (data[offset].to_int() << 8) + data[offset + 1].to_int()
}

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

///|
/// AlarmCR block request — used during AR connect to negotiate alarm channels.
pub(all) struct AlarmCRBlockReq {
  alarm_cr_type : Int
  lt : Int
  alarm_cr_properties : Int
  rta_timeout_factor : Int
  rta_retries : Int
  local_alarm_reference : Int
  max_alarm_data_length : Int
  alarm_cr_tag_header_high : Int
  alarm_cr_tag_header_low : Int
} derive(Eq, Debug)

///|
pub fn AlarmCRBlockReq::default() -> AlarmCRBlockReq {
  AlarmCRBlockReq::{
    alarm_cr_type: 1,
    lt: alarm_lt,
    alarm_cr_properties: 0,
    rta_timeout_factor: default_rta_timeout_factor,
    rta_retries: default_rta_retries,
    local_alarm_reference: 0,
    max_alarm_data_length: default_max_alarm_data_length,
    alarm_cr_tag_header_high: default_alarm_cr_tag_header_high,
    alarm_cr_tag_header_low: default_alarm_cr_tag_header_low,
  }
}

///|
/// Encode an AlarmCRBlockReq into bytes (block body, without block header).
pub fn encode_alarm_cr_block_req(req : AlarmCRBlockReq) -> Bytes {
  let output : Array[Byte] = []
  push_u16_be(output, req.alarm_cr_type)
  push_u16_be(output, req.lt)
  push_u32_be(output, req.alarm_cr_properties)
  push_u16_be(output, req.rta_timeout_factor)
  push_u16_be(output, req.rta_retries)
  push_u16_be(output, req.local_alarm_reference)
  push_u16_be(output, req.max_alarm_data_length)
  push_u16_be(output, req.alarm_cr_tag_header_high)
  push_u16_be(output, req.alarm_cr_tag_header_low)
  Bytes::from_array(output)
}

///|
/// AlarmCR block response — returned by device after connect request.
pub(all) struct AlarmCRBlockRes {
  alarm_cr_type : Int
  local_alarm_reference : Int
  max_alarm_data_length : Int
} derive(Eq, Debug)

///|
/// Parse an AlarmCRBlockRes from bytes (block body).
pub fn parse_alarm_cr_block_res(
  data : Bytes,
  offset : Int,
) -> AlarmCRBlockRes raise @frame.FrameError {
  guard data.length() >= offset + 6 else {
    raise @frame.FrameError::InvalidMacLength(data.length())
  }
  AlarmCRBlockRes::{
    alarm_cr_type: read_u16_be(data, offset),
    local_alarm_reference: read_u16_be(data, offset + 2),
    max_alarm_data_length: read_u16_be(data, offset + 4),
  }
}

///|
/// Alarm notification PDU — parsed from an alarm Ethernet frame payload.
pub(all) struct AlarmNotification {
  dst_endpoint : Int
  src_endpoint : Int
  pdu_version : Int
  pdu_type : Byte
  add_flags : Byte
  send_seq_num : Int
  ack_seq_num : Int
  alarm_data_length : Int
  alarm_type : Int
  api : Int
  slot_number : Int
  subslot_number : Int
  module_ident_number : Int
  submodule_ident_number : Int
} derive(Eq, Debug)

///|
pub(all) struct RtaPduHeader {
  dst_endpoint : Int
  src_endpoint : Int
  pdu_version : Int
  pdu_type : Int
  pdu_type_raw : Byte
  add_flags : Byte
  send_seq_num : Int
  ack_seq_num : Int
  var_part_len : Int
} derive(Eq, Debug)

///|
pub(all) struct RtaError {
  header : RtaPduHeader
  pnio_status : Int
} derive(Eq, Debug)

///|
pub(all) struct AlarmAck {
  dst_endpoint : Int
  src_endpoint : Int
  add_flags : Byte
  send_seq_num : Int
  ack_seq_num : Int
  status : Int
} derive(Eq, Debug)

///|
pub(all) struct AlarmResponderState {
  phase : String
  expected_send_seq : Int
  last_send_seq : Int
  pending_ack_seq : Int
  notification_count : Int
  duplicate_count : Int
  out_of_sequence_count : Int
  ack_sent_count : Int
  ack_fail_count : Int
} derive(Eq, Debug)

///|
pub(all) struct AlarmResponderDecision {
  phase_before : String
  phase_after : String
  notification_send_seq : Int
  notification_ack_seq : Int
  expected_send_seq : Int
  next_expected_send_seq : Int
  duplicate : Bool
  in_sequence : Bool
  ack_required : Bool
} derive(Eq, Debug)

///|
pub fn AlarmResponderState::default() -> AlarmResponderState {
  AlarmResponderState::{
    phase: "W-Notify",
    expected_send_seq: -1,
    last_send_seq: -1,
    pending_ack_seq: -1,
    notification_count: 0,
    duplicate_count: 0,
    out_of_sequence_count: 0,
    ack_sent_count: 0,
    ack_fail_count: 0,
  }
}

///|
pub fn alarm_sequence_next(seq : Int) -> Int {
  (seq + 1) & 0xFFFF
}

///|
pub fn alarm_responder_on_notification(
  state : AlarmResponderState,
  notif : AlarmNotification,
) -> (AlarmResponderState, AlarmResponderDecision, AlarmAck) {
  let expected = if state.expected_send_seq < 0 {
    notif.send_seq_num
  } else {
    state.expected_send_seq
  }
  let duplicate = state.last_send_seq >= 0 &&
    notif.send_seq_num == state.last_send_seq
  let in_sequence = duplicate ||
    state.expected_send_seq < 0 ||
    notif.send_seq_num == state.expected_send_seq
  let next_expected = if duplicate {
    state.expected_send_seq
  } else {
    alarm_sequence_next(notif.send_seq_num)
  }
  let duplicate_delta = if duplicate { 1 } else { 0 }
  let out_of_sequence_delta = if in_sequence { 0 } else { 1 }
  let next_state = AlarmResponderState::{
    phase: "W-User-Ack",
    expected_send_seq: next_expected,
    last_send_seq: notif.send_seq_num,
    pending_ack_seq: notif.send_seq_num,
    notification_count: state.notification_count + 1,
    duplicate_count: state.duplicate_count + duplicate_delta,
    out_of_sequence_count: state.out_of_sequence_count + out_of_sequence_delta,
    ack_sent_count: state.ack_sent_count,
    ack_fail_count: state.ack_fail_count,
  }
  let decision = AlarmResponderDecision::{
    phase_before: state.phase,
    phase_after: next_state.phase,
    notification_send_seq: notif.send_seq_num,
    notification_ack_seq: notif.ack_seq_num,
    expected_send_seq: expected,
    next_expected_send_seq: next_expected,
    duplicate,
    in_sequence,
    ack_required: true,
  }
  (next_state, decision, AlarmAck::from_notification(notif))
}

///|
pub fn alarm_responder_on_ack_sent(
  state : AlarmResponderState,
  sent : Int,
) -> AlarmResponderState {
  if sent > 0 {
    AlarmResponderState::{
      phase: "W-Notify",
      expected_send_seq: state.expected_send_seq,
      last_send_seq: state.last_send_seq,
      pending_ack_seq: -1,
      notification_count: state.notification_count,
      duplicate_count: state.duplicate_count,
      out_of_sequence_count: state.out_of_sequence_count,
      ack_sent_count: state.ack_sent_count + 1,
      ack_fail_count: state.ack_fail_count,
    }
  } else {
    AlarmResponderState::{
      phase: "W-User-Ack",
      expected_send_seq: state.expected_send_seq,
      last_send_seq: state.last_send_seq,
      pending_ack_seq: state.pending_ack_seq,
      notification_count: state.notification_count,
      duplicate_count: state.duplicate_count,
      out_of_sequence_count: state.out_of_sequence_count,
      ack_sent_count: state.ack_sent_count,
      ack_fail_count: state.ack_fail_count + 1,
    }
  }
}

///|
pub fn format_alarm_responder_decision(
  prefix : String,
  decision : AlarmResponderDecision,
) -> String {
  let lines : Array[String] = []
  lines.push(prefix + "_phase_before=" + decision.phase_before)
  lines.push(prefix + "_phase_after=" + decision.phase_after)
  lines.push(
    prefix +
    "_notification_send_seq=" +
    decision.notification_send_seq.to_string(),
  )
  lines.push(
    prefix +
    "_notification_ack_seq=" +
    decision.notification_ack_seq.to_string(),
  )
  lines.push(
    prefix + "_expected_send_seq=" + decision.expected_send_seq.to_string(),
  )
  lines.push(
    prefix +
    "_next_expected_send_seq=" +
    decision.next_expected_send_seq.to_string(),
  )
  lines.push(prefix + "_duplicate=" + decision.duplicate.to_string())
  lines.push(prefix + "_in_sequence=" + decision.in_sequence.to_string())
  lines.push(prefix + "_ack_required=" + decision.ack_required.to_string())
  lines.join("\n")
}

///|
pub fn format_alarm_responder_state(
  prefix : String,
  state : AlarmResponderState,
) -> String {
  let lines : Array[String] = []
  lines.push(prefix + "_phase=" + state.phase)
  lines.push(
    prefix + "_expected_send_seq=" + state.expected_send_seq.to_string(),
  )
  lines.push(prefix + "_last_send_seq=" + state.last_send_seq.to_string())
  lines.push(prefix + "_pending_ack_seq=" + state.pending_ack_seq.to_string())
  lines.push(
    prefix + "_notification_count=" + state.notification_count.to_string(),
  )
  lines.push(prefix + "_duplicate_count=" + state.duplicate_count.to_string())
  lines.push(
    prefix + "_out_of_sequence_count=" + state.out_of_sequence_count.to_string(),
  )
  lines.push(prefix + "_ack_sent_count=" + state.ack_sent_count.to_string())
  lines.push(prefix + "_ack_fail_count=" + state.ack_fail_count.to_string())
  lines.join("\n")
}

///|
pub fn parse_rta_pdu_header(
  data : Bytes,
  offset : Int,
) -> RtaPduHeader raise @frame.FrameError {
  guard data.length() >= offset + 12 else {
    raise @frame.FrameError::InvalidMacLength(data.length())
  }
  let pdu_type_raw = data[offset + 4]
  RtaPduHeader::{
    dst_endpoint: read_u16_be(data, offset),
    src_endpoint: read_u16_be(data, offset + 2),
    pdu_version: (pdu_type_raw.to_int() >> 4) & 0x0F,
    pdu_type: pdu_type_raw.to_int() & 0x0F,
    pdu_type_raw,
    add_flags: data[offset + 5],
    send_seq_num: read_u16_be(data, offset + 6),
    ack_seq_num: read_u16_be(data, offset + 8),
    var_part_len: read_u16_be(data, offset + 10),
  }
}

///|
pub fn parse_rta_error(
  data : Bytes,
  offset : Int,
) -> RtaError raise @frame.FrameError {
  let header = parse_rta_pdu_header(data, offset)
  guard header.pdu_type == rta_pdu_type_err && header.var_part_len >= 4 else {
    raise @frame.FrameError::InvalidMacLength(data.length())
  }
  guard data.length() >= offset + 16 else {
    raise @frame.FrameError::InvalidMacLength(data.length())
  }
  RtaError::{ header, pnio_status: read_u32_be(data, offset + 12) }
}

///|
pub fn AlarmAck::from_notification(notif : AlarmNotification) -> AlarmAck {
  AlarmAck::{
    dst_endpoint: notif.src_endpoint,
    src_endpoint: notif.dst_endpoint,
    add_flags: b'\x00',
    send_seq_num: notif.ack_seq_num,
    ack_seq_num: notif.send_seq_num,
    status: 0,
  }
}

///|
/// Encode the RTA transport ACK body that follows the 2-byte FrameID.
pub fn encode_alarm_ack(ack : AlarmAck) -> Bytes {
  let output : Array[Byte] = []
  push_u16_be(output, ack.dst_endpoint)
  push_u16_be(output, ack.src_endpoint)
  output.push(alarm_pdu_type_ack)
  output.push(ack.add_flags)
  push_u16_be(output, ack.send_seq_num)
  push_u16_be(output, ack.ack_seq_num)
  push_u16_be(output, 0)
  Bytes::from_array(output)
}

///|
fn push_eth_header(
  output : Array[Byte],
  destination : @frame.MacAddress,
  source : @frame.MacAddress,
  vlan_tci : Int,
) -> Unit {
  for byte in destination.octets() {
    output.push(byte)
  }
  for byte in source.octets() {
    output.push(byte)
  }
  if vlan_tci >= 0 {
    push_u16_be(output, 0x8100)
    push_u16_be(output, vlan_tci)
  }
  push_u16_be(output, alarm_lt)
}

///|
fn pad_ethernet_min_frame(output : Array[Byte]) -> Unit {
  while output.length() < 60 {
    output.push(b'\x00')
  }
}

///|
/// Encode a full Ethernet RTA transport ACK frame.
pub fn encode_alarm_ack_frame(
  destination : @frame.MacAddress,
  source : @frame.MacAddress,
  frame_id : Int,
  ack : AlarmAck,
  vlan_tci? : Int = -1,
) -> Bytes {
  let output : Array[Byte] = []
  push_eth_header(output, destination, source, vlan_tci)
  push_u16_be(output, frame_id)
  for byte in encode_alarm_ack(ack) {
    output.push(byte)
  }
  pad_ethernet_min_frame(output)
  Bytes::from_array(output)
}

///|
/// Parse an AlarmNotification from Ethernet frame payload.
/// The payload starts after Ethernet header (14 bytes) + FrameID (2 bytes).
pub fn parse_alarm_notification(
  data : Bytes,
  offset : Int,
) -> AlarmNotification raise @frame.FrameError {
  guard data.length() >= offset + 30 else {
    raise @frame.FrameError::InvalidMacLength(data.length())
  }
  let header = parse_rta_pdu_header(data, offset)
  AlarmNotification::{
    dst_endpoint: header.dst_endpoint,
    src_endpoint: header.src_endpoint,
    pdu_version: header.pdu_version,
    pdu_type: (header.pdu_type & 0xFF).to_byte(),
    add_flags: header.add_flags,
    send_seq_num: header.send_seq_num,
    ack_seq_num: header.ack_seq_num,
    alarm_data_length: header.var_part_len,
    alarm_type: read_u16_be(data, offset + 12),
    api: read_u32_be(data, offset + 14),
    slot_number: read_u16_be(data, offset + 18),
    subslot_number: read_u16_be(data, offset + 20),
    module_ident_number: read_u32_be(data, offset + 22),
    submodule_ident_number: read_u32_be(data, offset + 26),
  }
}

///|
/// Format alarm notification as human-readable summary.
pub fn format_alarm_notification(notif : AlarmNotification) -> String {
  let lines : Array[String] = []
  lines.push("pdu_version=" + notif.pdu_version.to_string())
  lines.push("pdu_type=" + rta_pdu_type_label(notif.pdu_type.to_int()))
  lines.push("add_flags=0x" + @frame.uint16_to_hex(notif.add_flags.to_int()))
  lines.push("alarm_type=" + alarm_type_label(notif.alarm_type))
  lines.push("api=" + notif.api.to_string())
  lines.push("slot=" + notif.slot_number.to_string())
  lines.push("subslot=" + notif.subslot_number.to_string())
  lines.push(
    "module_ident=0x" + @frame.uint32_to_hex(notif.module_ident_number),
  )
  lines.push(
    "submodule_ident=0x" + @frame.uint32_to_hex(notif.submodule_ident_number),
  )
  lines.push("send_seq=" + notif.send_seq_num.to_string())
  lines.push("ack_seq=" + notif.ack_seq_num.to_string())
  lines.push("dst_endpoint=" + notif.dst_endpoint.to_string())
  lines.push("src_endpoint=" + notif.src_endpoint.to_string())
  lines.join("\n")
}

///|
pub fn rta_pdu_type_label(pdu_type : Int) -> String {
  match pdu_type {
    1 => "DATA"
    2 => "NACK"
    3 => "ACK"
    4 => "ERR"
    _ => "Unknown(" + pdu_type.to_string() + ")"
  }
}

///|
pub fn format_rta_error(err : RtaError) -> String {
  let lines : Array[String] = []
  lines.push("rta_pdu_version=" + err.header.pdu_version.to_string())
  lines.push("rta_pdu_type=" + rta_pdu_type_label(err.header.pdu_type))
  lines.push(
    "rta_pdu_type_raw=0x" +
    @frame.uint16_to_hex(err.header.pdu_type_raw.to_int()),
  )
  lines.push(
    "rta_add_flags=0x" + @frame.uint16_to_hex(err.header.add_flags.to_int()),
  )
  lines.push("rta_send_seq=" + err.header.send_seq_num.to_string())
  lines.push("rta_ack_seq=" + err.header.ack_seq_num.to_string())
  lines.push("rta_var_part_len=" + err.header.var_part_len.to_string())
  lines.push("rta_pnio_status=0x" + @frame.uint32_to_hex(err.pnio_status))
  lines.push("rta_pnio_status_hint=" + rta_pnio_status_hint(err.pnio_status))
  lines.join("\n")
}

///|
pub fn rta_pnio_status_hint(status : Int) -> String {
  match status {
    0xCF81FD02 => "rta_abort_instance_closed"
    0xCF81FD05 => "rta_abort_ar_consumer_dht_expired"
    _ => "unknown"
  }
}

///|
pub fn format_alarm_ack(ack : AlarmAck) -> String {
  let lines : Array[String] = []
  lines.push("ack_dst_endpoint=" + ack.dst_endpoint.to_string())
  lines.push("ack_src_endpoint=" + ack.src_endpoint.to_string())
  lines.push("ack_pdu_type=ACK")
  lines.push("ack_send_seq=" + ack.send_seq_num.to_string())
  lines.push("ack_ack_seq=" + ack.ack_seq_num.to_string())
  lines.join("\n")
}