///|
pub enum Code {
  AccessRequest
  AccessAccept
  AccessReject
  AccountingRequest
  AccountingResponse
  AccessChallenge
  StatusServer
  StatusClient
  Unknown(Int)
} derive(Eq, Debug)

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

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

///|
pub fn Code::from_int(value : Int) -> Code {
  match value {
    1 => AccessRequest
    2 => AccessAccept
    3 => AccessReject
    4 => AccountingRequest
    5 => AccountingResponse
    11 => AccessChallenge
    12 => StatusServer
    13 => StatusClient
    n => Unknown(n)
  }
}

///|
pub fn Code::to_int(self : Code) -> Int {
  match self {
    AccessRequest => 1
    AccessAccept => 2
    AccessReject => 3
    AccountingRequest => 4
    AccountingResponse => 5
    AccessChallenge => 11
    StatusServer => 12
    StatusClient => 13
    Unknown(value) => value
  }
}

///|
pub(all) suberror RadiusError {
  TooShort
  InvalidCode(Int)
  InvalidLength(Int)
  AttributeTruncated(Int)
  AttributeTooShort(Int)
  InvalidAuthenticator
  Unsupported(String)
  InvalidAttribute(String)
}

///|
pub struct Header {
  code : Code
  identifier : Byte
  authenticator : Bytes
}

///|
pub struct Attribute {
  kind : Byte
  value : Bytes
}

///|
pub struct Packet {
  header : Header
  attributes : Array[Attribute]
}

///|
pub fn Header::new(
  code : Code,
  identifier : Byte,
  authenticator : Bytes,
) -> Header {
  Header::{ code, identifier, authenticator, }
}

///|
pub fn Attribute::new(kind : Byte, value : Bytes) -> Attribute {
  Attribute::{ kind, value, }
}

///|
pub fn Packet::new(
  code : Code,
  identifier : Byte,
  authenticator : Bytes,
) -> Packet {
  { header: Header::new(code, identifier, authenticator), attributes: [], }
}

///|
pub fn Packet::with_attribute(self : Packet, attribute : Attribute) -> Packet {
  self.attributes.push(attribute)
  self
}

///|
pub fn Packet::attribute(self : Packet, kind : Byte) -> Attribute? {
  for item in self.attributes {
    if item.kind == kind {
      return Some(item)
    }
  }
  None
}

///|
/// Expose the packet code without requiring callers to inspect the header.
pub fn Packet::code(self : Packet) -> Code {
  self.header.code
}

///|
/// Return the RADIUS identifier used to correlate a response.
pub fn Packet::identifier(self : Packet) -> Byte {
  self.header.identifier
}

///|
/// Return a defensive copy of the request or response authenticator.
pub fn Packet::authenticator(self : Packet) -> Bytes {
  self.header.authenticator
}

///|
/// Return a defensive copy of all attributes in wire order.
pub fn Packet::all_attributes(self : Packet) -> Array[Attribute] {
  self.attributes.copy()
}

///|
/// Standard RADIUS attribute numbers from RFC 2865 and RFC 2866.
pub const User_Name : Byte = 1

///|
pub const User_Password : Byte = 2

///|
pub const CHAP_Password : Byte = 3

///|
pub const NAS_IP_Address : Byte = 4

///|
pub const NAS_Port : Byte = 5

///|
pub const Service_Type : Byte = 6

///|
pub const Framed_Protocol : Byte = 7

///|
pub const Framed_IP_Address : Byte = 8

///|
pub const Framed_IP_Netmask : Byte = 9

///|
pub const Filter_Id : Byte = 11

///|
pub const Reply_Message : Byte = 18

///|
pub const State : Byte = 24

///|
pub const Class : Byte = 25

///|
pub const Vendor_Specific : Byte = 26

///|
pub const Session_Timeout : Byte = 27

///|
pub const Idle_Timeout : Byte = 28

///|
pub const Termination_Action : Byte = 29

///|
pub const Called_Station_Id : Byte = 30

///|
pub const Calling_Station_Id : Byte = 31

///|
pub const NAS_Identifier : Byte = 32

///|
pub const Acct_Status_Type : Byte = 40

///|
pub const Acct_Input_Octets : Byte = 42

///|
pub const Acct_Output_Octets : Byte = 43

///|
pub const Acct_Session_Id : Byte = 44

///|
pub const Acct_Session_Time : Byte = 46

///|
pub const Acct_Input_Packets : Byte = 47

///|
pub const Acct_Output_Packets : Byte = 48

///|
pub const Acct_Terminate_Cause : Byte = 49

///|
pub const Acct_Multi_Session_Id : Byte = 50

///|
pub const Acct_Link_Count : Byte = 51

///|
pub const Acct_Interim_Interval : Byte = 85

///|
/// Accounting status values defined by RFC 2866.
pub const Acct_Start : Int = 1

///|
pub const Acct_Stop : Int = 2

///|
pub const Acct_Interim_Update : Int = 3

///|
pub const Acct_On : Int = 7

///|
pub const Acct_Off : Int = 8

///|
/// Common service types.
pub const Login : Int = 1

///|
pub const Framed : Int = 2

///|
pub const Administrative : Int = 6

///|
pub const NAS_Prompt : Int = 7

///|
/// Construct an attribute from UTF-8 text.
pub fn Attribute::text(kind : Byte, value : String) -> Attribute {
  Attribute::{ kind, value: @utf8.encode(value), }
}

///|
/// Construct an attribute containing a big-endian unsigned integer.
pub fn Attribute::integer(kind : Byte, value : Int) -> Attribute {
  Attribute::{ kind, value: u32_bytes(value), }
}

///|
/// Read a four-byte unsigned integer attribute.
pub fn Attribute::as_integer(self : Attribute) -> Int? {
  if self.value.length() != 4 {
    None
  } else {
    Some(
      (self.value[0].to_int() << 24) |
      (self.value[1].to_int() << 16) |
      (self.value[2].to_int() << 8) |
      self.value[3].to_int(),
    )
  }
}

///|
/// Construct a packet with a zero authenticator, useful for requests whose
/// authenticator is filled by a transport or authentication helper.
pub fn Packet::request(code : Code, identifier : Byte) -> Packet {
  Packet::new(code, identifier, zeros(16))
}

///|
/// Return all attributes with a given type, preserving wire order.
pub fn Packet::attributes_of(self : Packet, kind : Byte) -> Array[Attribute] {
  let result = []
  for item in self.attributes {
    if item.kind == kind {
      result.push(item)
    }
  }
  result
}

///|
/// Add an attribute without replacing existing attributes.
pub fn Packet::push_attribute(self : Packet, attribute : Attribute) -> Packet {
  self.attributes.push(attribute)
  self
}

///|
/// Return whether this packet is an Access-Request.
pub fn Packet::is_access_request(self : Packet) -> Bool {
  self.header.code == AccessRequest
}

///|
/// Return whether this packet is an Accounting-Request.
pub fn Packet::is_accounting_request(self : Packet) -> Bool {
  self.header.code == AccountingRequest
}

///|
/// Return a packet with the same header and a filtered attribute stream.
pub fn Packet::filter_attributes(
  self : Packet,
  predicate : (Attribute) -> Bool,
) -> Packet {
  let attributes = []
  for attribute in self.attributes {
    if predicate(attribute) {
      attributes.push(attribute)
    }
  }
  Packet::{ header: self.header, attributes, }
}

///|
/// Return a packet with one attribute type removed.
pub fn Packet::without_attribute(self : Packet, kind : Byte) -> Packet {
  self.filter_attributes(fn(attribute) { attribute.kind != kind })
}

///|
/// Return a packet with all copies of a type replaced by one value.
pub fn Packet::replace_attribute(
  self : Packet,
  kind : Byte,
  replacement : Attribute,
) -> Packet {
  let filtered = self.without_attribute(kind)
  filtered.with_attribute(replacement)
}

///|
/// Return a stable copy of the packet for response construction.
pub fn Packet::copy(self : Packet) -> Packet {
  Packet::{
    header: Header::{
      code: self.header.code,
      identifier: self.header.identifier,
      authenticator: self.header.authenticator,
    },
    attributes: self.attributes.copy(),
  }
}