///|
/// Request bookkeeping helpers for datagram servers.
///
/// RADIUS uses an identifier plus request authenticator to correlate
/// retransmissions.  This module does not decide policy; it gives an
/// application a bounded, deterministic way to classify repeated requests.
pub enum RequestDisposition {
  New
  Retransmission
  Conflict
} derive(Eq, Debug)

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

///|
pub extend RequestDisposition with @moonbitlang/core/debug.Debug::{to_repr}

///|
pub struct RequestKey {
  identifier : Byte
  authenticator : Bytes
}

///|
pub fn RequestKey::from_packet(packet : Packet) -> RequestKey {
  RequestKey::{
    identifier: packet.header.identifier,
    authenticator: packet.header.authenticator,
  }
}

///|
pub fn RequestKey::same(self : RequestKey, other : RequestKey) -> Bool {
  self.identifier == other.identifier &&
  byte_eq(self.authenticator, other.authenticator)
}

///|
/// A bounded replay table suitable for one shared-secret endpoint.
pub struct ReplayTable {
  entries : Array[RequestKey]
  capacity : Int
}

///|
pub fn ReplayTable::new(capacity : Int) -> ReplayTable {
  ReplayTable::{
    entries: [],
    capacity: if capacity < 1 {
      1
    } else {
      capacity
    },
  }
}

///|
pub fn ReplayTable::size(self : ReplayTable) -> Int {
  self.entries.length()
}

///|
pub fn ReplayTable::classify(
  self : ReplayTable,
  packet : Packet,
) -> RequestDisposition {
  let key = RequestKey::from_packet(packet)
  for entry in self.entries {
    if entry.identifier == key.identifier {
      if entry.same(key) {
        return Retransmission
      } else {
        return Conflict
      }
    }
  }
  New
}

///|
/// Record a new request, evicting the oldest key at capacity.
pub fn ReplayTable::record(
  self : ReplayTable,
  packet : Packet,
) -> RequestDisposition {
  let disposition = self.classify(packet)
  match disposition {
    New => {
      if self.entries.length() >= self.capacity {
        let _ = self.entries.remove(0)
      }
      self.entries.push(RequestKey::from_packet(packet))
    }
    Retransmission => ()
    Conflict => ()
  }
  disposition
}

///|
/// Decide whether a packet is a legal response to a request code.
pub fn response_matches(request : Packet, response : Packet) -> Bool {
  match request.header.code {
    AccessRequest =>
      response.header.code == AccessAccept ||
      response.header.code == AccessReject ||
      response.header.code == AccessChallenge
    AccountingRequest => response.header.code == AccountingResponse
    _ => false
  }
}

///|
/// Ensure identifier correlation is preserved across a response.
pub fn response_identifier_matches(request : Packet, response : Packet) -> Bool {
  request.header.identifier == response.header.identifier
}

///|
/// Return a compact packet summary for diagnostics and logs.
pub fn packet_summary(packet : Packet) -> String {
  let code = match packet.header.code {
    AccessRequest => "Access-Request"
    AccessAccept => "Access-Accept"
    AccessReject => "Access-Reject"
    AccountingRequest => "Accounting-Request"
    AccountingResponse => "Accounting-Response"
    AccessChallenge => "Access-Challenge"
    StatusServer => "Status-Server"
    StatusClient => "Status-Client"
    Unknown(_) => "Unknown"
  }
  code +
  " id=" +
  packet.header.identifier.to_int().to_string() +
  " attrs=" +
  packet.attributes.length().to_string()
}

///|
/// Collect basic counters without inspecting sensitive attribute values.
pub struct PacketStats {
  attributes : Int
  vendor_attributes : Int
  total_value_bytes : Int
}

///|
pub fn PacketStats::from_packet(packet : Packet) -> PacketStats {
  let mut vendors = 0
  let mut values = 0
  for attribute in packet.attributes {
    if attribute.kind == Vendor_Specific {
      vendors = vendors + 1
    }
    values = values + attribute.value.length()
  }
  PacketStats::{
    attributes: packet.attributes.length(),
    vendor_attributes: vendors,
    total_value_bytes: values,
  }
}