///|
/// SNMP v1 standard UDP port.
pub let snmp_port : Int = 161

///|
/// SNMP v1 PDU types (ASN.1 constructed, context-specific).
pub let pdu_type_get : Byte = b'\xA0'

///|
pub let pdu_type_get_next : Byte = b'\xA1'

///|
pub let pdu_type_response : Byte = b'\xA2'

///|
pub let pdu_type_set : Byte = b'\xA3'

///|
/// ASN.1 tag constants.
pub let asn1_sequence : Byte = b'\x30'

///|
pub let asn1_integer : Byte = b'\x02'

///|
pub let asn1_octet_string : Byte = b'\x04'

///|
pub let asn1_null : Byte = b'\x05'

///|
pub let asn1_oid : Byte = b'\x06'

///|
/// Well-known PROFINET SNMP OIDs.
pub let oid_sys_descr : String = "1.3.6.1.2.1.1.1.0"

///|
pub let oid_sys_name : String = "1.3.6.1.2.1.1.5.0"

///|
pub let oid_sys_contact : String = "1.3.6.1.2.1.1.4.0"

///|
pub let oid_sys_location : String = "1.3.6.1.2.1.1.6.0"

///|
pub let oid_if_number : String = "1.3.6.1.2.1.2.1.0"

///|
/// LLDP MIB OIDs.
pub let oid_lldp_rem_sys_name : String = "1.0.8802.1.1.2.1.4.1.1.9"

///|
pub let oid_lldp_rem_port_id : String = "1.0.8802.1.1.2.1.4.1.1.7"

///|
/// SNMP v1 message model.
pub(all) struct SnmpMessage {
  version : Int
  community : String
  pdu_type : Byte
  request_id : Int
  error_status : Int
  error_index : Int
  varbinds : Array[VarBind]
} derive(Eq, Debug)

///|
/// SNMP variable binding.
pub(all) struct VarBind {
  oid : String
  value : SnmpValue
} derive(Eq, Debug)

///|
/// SNMP value types (simplified).
pub(all) enum SnmpValue {
  Null
  IntVal(Int)
  OctetString(Bytes)
  OidVal(String)
} derive(Eq, Debug)

///|
struct Asn1Tlv {
  tag : Byte
  value_start : Int
  value_end : Int
} derive(Eq, Debug)

///|
/// Encode OID string to ASN.1 bytes.
pub fn encode_oid(oid_str : String) -> Bytes {
  let parts = oid_str.split(".").to_array()
  let output : Array[Byte] = []
  if parts.length() >= 2 {
    let first = parse_oid_part(parts[0].to_owned())
    let second = parse_oid_part(parts[1].to_owned())
    output.push((first * 40 + second).to_byte())
    for i = 2; i < parts.length(); i = i + 1 {
      let value = parse_oid_part(parts[i].to_owned())
      encode_oid_component(output, value)
    }
  }
  Bytes::from_array(output)
}

///|
fn read_asn1_tlv(
  data : Bytes,
  offset : Int,
  limit : Int,
) -> Asn1Tlv raise @frame.FrameError {
  guard offset + 2 <= limit && limit <= data.length() else {
    raise @frame.FrameError::InvalidMacLength(data.length())
  }
  let tag = data[offset]
  let first_len = data[offset + 1].to_int()
  let mut length = 0
  let mut value_start = offset + 2
  if (first_len & 0x80) == 0 {
    length = first_len
  } else {
    let len_len = first_len & 0x7F
    guard len_len > 0 && len_len <= 4 && value_start + len_len <= limit else {
      raise @frame.FrameError::InvalidMacLength(data.length())
    }
    for i = 0; i < len_len; i = i + 1 {
      length = (length << 8) + data[value_start + i].to_int()
    }
    value_start = value_start + len_len
  }
  let value_end = value_start + length
  guard value_end <= limit else {
    raise @frame.FrameError::InvalidMacLength(data.length())
  }
  Asn1Tlv::{ tag, value_start, value_end }
}

///|
fn parse_asn1_integer(data : Bytes, tlv : Asn1Tlv) -> Int {
  let mut value = 0
  for i = tlv.value_start; i < tlv.value_end; i = i + 1 {
    value = (value << 8) + data[i].to_int()
  }
  value
}

///|
fn parse_asn1_string(data : Bytes, tlv : Asn1Tlv) -> String {
  let mut s = ""
  for i = tlv.value_start; i < tlv.value_end; i = i + 1 {
    if data[i].to_int() == 0 {
      break
    }
    s = s + data[i].to_int().unsafe_to_char().to_string()
  }
  s
}

///|
fn parse_asn1_oid(
  data : Bytes,
  tlv : Asn1Tlv,
) -> String raise @frame.FrameError {
  guard tlv.value_start < tlv.value_end else {
    raise @frame.FrameError::InvalidMacLength(data.length())
  }
  let first = data[tlv.value_start].to_int()
  let first_component = if first >= 80 { 2 } else { first / 40 }
  let second_component = if first >= 80 { first - 80 } else { first % 40 }
  let components : Array[Int] = [first_component, second_component]
  let mut component = 0
  let mut complete = true
  for i = tlv.value_start + 1; i < tlv.value_end; i = i + 1 {
    let byte = data[i].to_int()
    component = (component << 7) | (byte & 0x7F)
    complete = (byte & 0x80) == 0
    if complete {
      components.push(component)
      component = 0
    }
  }
  guard complete else {
    raise @frame.FrameError::InvalidMacLength(data.length())
  }
  let parts : Array[String] = []
  for component in components {
    parts.push(component.to_string())
  }
  parts.join(".")
}

///|
fn parse_asn1_value(
  data : Bytes,
  tlv : Asn1Tlv,
) -> SnmpValue raise @frame.FrameError {
  if tlv.tag == asn1_null {
    Null
  } else if tlv.tag == asn1_integer {
    IntVal(parse_asn1_integer(data, tlv))
  } else if tlv.tag == asn1_octet_string {
    OctetString(data[tlv.value_start:tlv.value_end].to_owned())
  } else if tlv.tag == asn1_oid {
    OidVal(parse_asn1_oid(data, tlv))
  } else {
    OctetString(data[tlv.value_start:tlv.value_end].to_owned())
  }
}

///|
/// Parse an SNMP v1 message from BER bytes.
pub fn parse_snmp_message(data : Bytes) -> SnmpMessage raise @frame.FrameError {
  let top = read_asn1_tlv(data, 0, data.length())
  guard top.tag == asn1_sequence && top.value_end == data.length() else {
    raise @frame.FrameError::InvalidMacLength(data.length())
  }
  let version_tlv = read_asn1_tlv(data, top.value_start, top.value_end)
  guard version_tlv.tag == asn1_integer else {
    raise @frame.FrameError::InvalidMacLength(data.length())
  }
  let community_tlv = read_asn1_tlv(data, version_tlv.value_end, top.value_end)
  guard community_tlv.tag == asn1_octet_string else {
    raise @frame.FrameError::InvalidMacLength(data.length())
  }
  let pdu_tlv = read_asn1_tlv(data, community_tlv.value_end, top.value_end)
  let request_tlv = read_asn1_tlv(data, pdu_tlv.value_start, pdu_tlv.value_end)
  guard request_tlv.tag == asn1_integer else {
    raise @frame.FrameError::InvalidMacLength(data.length())
  }
  let error_status_tlv = read_asn1_tlv(
    data,
    request_tlv.value_end,
    pdu_tlv.value_end,
  )
  guard error_status_tlv.tag == asn1_integer else {
    raise @frame.FrameError::InvalidMacLength(data.length())
  }
  let error_index_tlv = read_asn1_tlv(
    data,
    error_status_tlv.value_end,
    pdu_tlv.value_end,
  )
  guard error_index_tlv.tag == asn1_integer else {
    raise @frame.FrameError::InvalidMacLength(data.length())
  }
  let varbind_list_tlv = read_asn1_tlv(
    data,
    error_index_tlv.value_end,
    pdu_tlv.value_end,
  )
  guard varbind_list_tlv.tag == asn1_sequence else {
    raise @frame.FrameError::InvalidMacLength(data.length())
  }
  let varbinds : Array[VarBind] = []
  let mut pos = varbind_list_tlv.value_start
  while pos < varbind_list_tlv.value_end {
    let varbind_tlv = read_asn1_tlv(data, pos, varbind_list_tlv.value_end)
    guard varbind_tlv.tag == asn1_sequence else {
      raise @frame.FrameError::InvalidMacLength(data.length())
    }
    let oid_tlv = read_asn1_tlv(
      data,
      varbind_tlv.value_start,
      varbind_tlv.value_end,
    )
    guard oid_tlv.tag == asn1_oid else {
      raise @frame.FrameError::InvalidMacLength(data.length())
    }
    let value_tlv = read_asn1_tlv(
      data,
      oid_tlv.value_end,
      varbind_tlv.value_end,
    )
    let oid = parse_asn1_oid(data, oid_tlv)
    let value = parse_asn1_value(data, value_tlv)
    varbinds.push(VarBind::{ oid, value })
    pos = varbind_tlv.value_end
  }
  SnmpMessage::{
    version: parse_asn1_integer(data, version_tlv),
    community: parse_asn1_string(data, community_tlv),
    pdu_type: pdu_tlv.tag,
    request_id: parse_asn1_integer(data, request_tlv),
    error_status: parse_asn1_integer(data, error_status_tlv),
    error_index: parse_asn1_integer(data, error_index_tlv),
    varbinds,
  }
}

///|
fn parse_oid_part(s : String) -> Int {
  let mut value = 0
  for c in s {
    if c >= '0' && c <= '9' {
      value = value * 10 + (c.to_int() - '0'.to_int())
    }
  }
  value
}

///|
fn encode_oid_component(output : Array[Byte], value : Int) -> Unit {
  if value < 128 {
    output.push(value.to_byte())
  } else if value < 16384 {
    output.push(((value >> 7) | 0x80).to_byte())
    output.push((value & 0x7F).to_byte())
  } else {
    output.push(((value >> 14) | 0x80).to_byte())
    output.push((((value >> 7) & 0x7F) | 0x80).to_byte())
    output.push((value & 0x7F).to_byte())
  }
}

///|
fn encode_asn1_length(output : Array[Byte], length : Int) -> Unit {
  if length < 128 {
    output.push(length.to_byte())
  } else if length < 256 {
    output.push(b'\x81')
    output.push(length.to_byte())
  } else {
    output.push(b'\x82')
    output.push(((length >> 8) & 0xFF).to_byte())
    output.push((length & 0xFF).to_byte())
  }
}

///|
fn encode_asn1_integer(output : Array[Byte], value : Int) -> Unit {
  output.push(asn1_integer)
  if value == 0 {
    output.push(b'\x01')
    output.push(b'\x00')
  } else if value < 128 {
    output.push(b'\x01')
    output.push(value.to_byte())
  } else if value < 32768 {
    output.push(b'\x02')
    output.push(((value >> 8) & 0xFF).to_byte())
    output.push((value & 0xFF).to_byte())
  } else {
    output.push(b'\x04')
    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 encode_asn1_string(output : Array[Byte], s : String) -> Unit {
  output.push(asn1_octet_string)
  let bytes = s.to_array().map(fn(c) { c.to_int().to_byte() })
  encode_asn1_length(output, bytes.length())
  for b in bytes {
    output.push(b)
  }
}

///|
fn encode_asn1_oid_tlv(output : Array[Byte], oid_bytes : Bytes) -> Unit {
  output.push(asn1_oid)
  encode_asn1_length(output, oid_bytes.length())
  for b in oid_bytes {
    output.push(b)
  }
}

///|
fn encode_snmp_value(output : Array[Byte], value : SnmpValue) -> Unit {
  match value {
    Null => {
      output.push(asn1_null)
      output.push(b'\x00')
    }
    IntVal(v) => encode_asn1_integer(output, v)
    OctetString(data) => {
      output.push(asn1_octet_string)
      encode_asn1_length(output, data.length())
      for b in data {
        output.push(b)
      }
    }
    OidVal(oid) => {
      let oid_bytes = encode_oid(oid)
      encode_asn1_oid_tlv(output, oid_bytes)
    }
  }
}

///|
fn encode_varbind(output : Array[Byte], vb : VarBind) -> Unit {
  let inner : Array[Byte] = []
  let oid_bytes = encode_oid(vb.oid)
  encode_asn1_oid_tlv(inner, oid_bytes)
  encode_snmp_value(inner, vb.value)
  output.push(asn1_sequence)
  encode_asn1_length(output, inner.length())
  for b in inner {
    output.push(b)
  }
}

///|
/// Encode an SNMP v1 message.
pub fn encode_snmp_message(msg : SnmpMessage) -> Bytes {
  // Build PDU body
  let pdu_body : Array[Byte] = []
  encode_asn1_integer(pdu_body, msg.request_id)
  encode_asn1_integer(pdu_body, msg.error_status)
  encode_asn1_integer(pdu_body, msg.error_index)
  // varbind list
  let varbinds_body : Array[Byte] = []
  for vb in msg.varbinds {
    encode_varbind(varbinds_body, vb)
  }
  pdu_body.push(asn1_sequence)
  encode_asn1_length(pdu_body, varbinds_body.length())
  for b in varbinds_body {
    pdu_body.push(b)
  }
  // Build message body
  let msg_body : Array[Byte] = []
  encode_asn1_integer(msg_body, msg.version)
  encode_asn1_string(msg_body, msg.community)
  msg_body.push(msg.pdu_type)
  encode_asn1_length(msg_body, pdu_body.length())
  for b in pdu_body {
    msg_body.push(b)
  }
  // Wrap in SEQUENCE
  let result : Array[Byte] = []
  result.push(asn1_sequence)
  encode_asn1_length(result, msg_body.length())
  for b in msg_body {
    result.push(b)
  }
  Bytes::from_array(result)
}

///|
fn is_printable_ascii(data : Bytes) -> Bool {
  for b in data {
    let value = b.to_int()
    if value < 0x20 || value > 0x7E {
      return false
    }
  }
  true
}

///|
fn ascii_bytes_to_string(data : Bytes) -> String {
  let mut s = ""
  for b in data {
    if b.to_int() == 0 {
      break
    }
    s = s + b.to_int().unsafe_to_char().to_string()
  }
  s
}

///|
/// Format a simplified SNMP value for diagnostics.
pub fn format_snmp_value(value : SnmpValue) -> String {
  match value {
    Null => "null"
    IntVal(v) => v.to_string()
    OctetString(data) =>
      if is_printable_ascii(data) {
        "\"" + ascii_bytes_to_string(data) + "\""
      } else {
        "0x" + @frame.bytes_to_hex(data, separator="")
      }
    OidVal(oid) => oid
  }
}

///|
/// Build a simple SNMP GET request for a single OID.
pub fn build_get_request(
  community : String,
  request_id : Int,
  oid : String,
) -> SnmpMessage {
  SnmpMessage::{
    version: 0,
    community,
    pdu_type: pdu_type_get,
    request_id,
    error_status: 0,
    error_index: 0,
    varbinds: [VarBind::{ oid, value: Null }],
  }
}

///|
/// Build a simple SNMP GET-NEXT request for a single OID.
pub fn build_get_next_request(
  community : String,
  request_id : Int,
  oid : String,
) -> SnmpMessage {
  SnmpMessage::{
    version: 0,
    community,
    pdu_type: pdu_type_get_next,
    request_id,
    error_status: 0,
    error_index: 0,
    varbinds: [VarBind::{ oid, value: Null }],
  }
}

///|
/// Build a simple SNMP SET request.
pub fn build_set_request(
  community : String,
  request_id : Int,
  oid : String,
  value : SnmpValue,
) -> SnmpMessage {
  SnmpMessage::{
    version: 0,
    community,
    pdu_type: pdu_type_set,
    request_id,
    error_status: 0,
    error_index: 0,
    varbinds: [VarBind::{ oid, value }],
  }
}

///|
/// Format SNMP message summary.
pub fn format_snmp_message(msg : SnmpMessage) -> String {
  let pdu_label = match msg.pdu_type {
    b'\xA0' => "GET"
    b'\xA1' => "GET-NEXT"
    b'\xA2' => "RESPONSE"
    b'\xA3' => "SET"
    _ => "UNKNOWN"
  }
  let lines : Array[String] = []
  lines.push("version=SNMPv" + (msg.version + 1).to_string())
  lines.push("community=" + msg.community)
  lines.push("pdu_type=" + pdu_label)
  lines.push("request_id=" + msg.request_id.to_string())
  lines.push("error_status=" + msg.error_status.to_string())
  lines.push("varbind_count=" + msg.varbinds.length().to_string())
  for vb in msg.varbinds {
    lines.push("  oid=" + vb.oid + " value=" + format_snmp_value(vb.value))
  }
  lines.join("\n")
}