///|
/// Typed helpers for standard and vendor-specific attributes.
pub struct VendorAttribute {
  vendor_id : Int
  kind : Byte
  value : Bytes
}

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

///|
/// Encode the value portion of a Vendor-Specific attribute.
pub fn VendorAttribute::encode(
  self : VendorAttribute,
) -> Bytes raise RadiusError {
  if self.vendor_id < 0 {
    raise InvalidAttribute("vendor id is outside uint32 range")
  }
  if self.value.length() > 249 {
    raise InvalidAttribute("vendor attribute value exceeds 249 bytes")
  }
  join_bytes(
    u32_bytes(self.vendor_id),
    join_bytes(
      Bytes::makei(2, fn(index) {
        if index == 0 {
          self.kind
        } else {
          (self.value.length() + 2).to_byte()
        }
      }),
      self.value,
    ),
  )
}

///|
/// Decode the value of a Vendor-Specific attribute.
pub fn decode_vendor_attribute(
  attribute : Attribute,
) -> VendorAttribute? raise RadiusError {
  if attribute.kind != Vendor_Specific || attribute.value.length() < 6 {
    None
  } else {
    Some(VendorAttribute::{
      vendor_id: (attribute.value[0].to_int() << 24) |
      (attribute.value[1].to_int() << 16) |
      (attribute.value[2].to_int() << 8) |
      attribute.value[3].to_int(),
      kind: attribute.value[4],
      value: slice_bytes(attribute.value, 6, attribute.value.length() - 6),
    })
  }
}

///|
/// Add a Vendor-Specific attribute to a packet.
pub fn Packet::with_vendor_attribute(
  self : Packet,
  vendor : VendorAttribute,
) -> Packet raise RadiusError {
  self.with_attribute(Attribute::new(Vendor_Specific, vendor.encode()))
}

///|
/// A small dictionary entry suitable for vendor extensions.
pub struct DictionaryEntry {
  name : String
  code : Byte
  kind : String
}

///|
pub fn DictionaryEntry::new(
  name : String,
  code : Byte,
  kind : String,
) -> DictionaryEntry {
  DictionaryEntry::{ name, code, kind, }
}

///|
/// An immutable dictionary assembled from standard and vendor entries.
pub struct Dictionary {
  entries : Array[DictionaryEntry]
}

///|
pub fn Dictionary::new() -> Dictionary {
  Dictionary::{ entries: [], }
}

///|
pub fn Dictionary::standard() -> Dictionary {
  let dictionary = Dictionary::new()
  let dictionary = dictionary.add(
    DictionaryEntry::new("User-Name", User_Name, "string"),
  )
  let dictionary = dictionary.add(
    DictionaryEntry::new("User-Password", User_Password, "password"),
  )
  let dictionary = dictionary.add(
    DictionaryEntry::new("CHAP-Password", CHAP_Password, "chap"),
  )
  let dictionary = dictionary.add(
    DictionaryEntry::new("NAS-IP-Address", NAS_IP_Address, "ipv4"),
  )
  let dictionary = dictionary.add(
    DictionaryEntry::new("NAS-Port", NAS_Port, "integer"),
  )
  let dictionary = dictionary.add(
    DictionaryEntry::new("Service-Type", Service_Type, "integer"),
  )
  let dictionary = dictionary.add(
    DictionaryEntry::new("Reply-Message", Reply_Message, "string"),
  )
  let dictionary = dictionary.add(
    DictionaryEntry::new("State", State, "octets"),
  )
  let dictionary = dictionary.add(
    DictionaryEntry::new("Class", Class, "octets"),
  )
  let dictionary = dictionary.add(
    DictionaryEntry::new("Acct-Status-Type", Acct_Status_Type, "integer"),
  )
  let dictionary = dictionary.add(
    DictionaryEntry::new("Acct-Session-Id", Acct_Session_Id, "string"),
  )
  dictionary
}

///|
pub fn Dictionary::add(
  self : Dictionary,
  entry : DictionaryEntry,
) -> Dictionary {
  self.entries.push(entry)
  self
}

///|
pub fn Dictionary::lookup_code(
  self : Dictionary,
  code : Byte,
) -> DictionaryEntry? {
  for entry in self.entries {
    if entry.code == code {
      return Some(entry)
    }
  }
  None
}

///|
pub fn Dictionary::lookup_name(
  self : Dictionary,
  name : String,
) -> DictionaryEntry? {
  for entry in self.entries {
    if entry.name == name {
      return Some(entry)
    }
  }
  None
}

///|
/// Validate attribute lengths and required payload shapes.
pub fn validate_attribute(attribute : Attribute) -> String? {
  if attribute.value.length() > 253 {
    Some("attribute payload exceeds 253 bytes")
  } else if attribute.kind == CHAP_Password && attribute.value.length() != 17 {
    Some("CHAP-Password must contain one identifier and sixteen digest bytes")
  } else if attribute.kind == Acct_Status_Type && attribute.value.length() != 4 {
    Some("Acct-Status-Type must be a four-byte integer")
  } else {
    None
  }
}

///|
/// Return all validation messages without dropping the original attributes.
pub fn validate_packet(packet : Packet) -> Array[String] {
  let errors = []
  if packet.header.authenticator.length() != 16 {
    errors.push("authenticator must be exactly 16 bytes")
  }
  for attribute in packet.attributes {
    match validate_attribute(attribute) {
      Some(message) => errors.push(message)
      None => ()
    }
  }
  errors
}