///|
/// LLDP EtherType.
pub let ether_type_lldp : Int = 0x88CC

///|
/// LLDP multicast destination MAC.
pub let lldp_multicast_mac : String = "01:80:C2:00:00:0E"

///|
/// LLDP TLV types.
pub let tlv_type_end : Int = 0

///|
pub let tlv_type_chassis_id : Int = 1

///|
pub let tlv_type_port_id : Int = 2

///|
pub let tlv_type_ttl : Int = 3

///|
pub let tlv_type_port_description : Int = 4

///|
pub let tlv_type_system_name : Int = 5

///|
pub let tlv_type_system_description : Int = 6

///|
pub let tlv_type_system_capabilities : Int = 7

///|
pub let tlv_type_management_address : Int = 8

///|
/// PROFINET-specific TLV organization OUI.
pub let oui_profinet : Bytes = Bytes::from_array([b'\x00', b'\x0E', b'\xCF'])

///|
pub let oui_ieee_802_1 : Bytes = Bytes::from_array([b'\x00', b'\x80', b'\xC2'])

///|
pub let oui_ieee_802_3 : Bytes = Bytes::from_array([b'\x30', b'\x00', b'\x00'])

///|
/// LLDP TLV parsed structure.
pub(all) struct LldpTlv {
  tlv_type : Int
  length : Int
  value : Bytes
} derive(Eq, Debug)

///|
/// Parse LLDP TLVs from a frame payload (after Ethernet header).
pub fn parse_lldp_tlvs(
  data : Bytes,
  offset : Int,
) -> Array[LldpTlv] raise @frame.FrameError {
  let tlvs : Array[LldpTlv] = []
  let mut pos = offset
  while pos + 2 <= data.length() {
    let type_len = (data[pos].to_int() << 8) + data[pos + 1].to_int()
    let tlv_type = (type_len >> 9) & 0x7F
    let length = type_len & 0x01FF
    pos = pos + 2
    guard pos + length <= data.length() else {
      raise @frame.FrameError::InvalidMacLength(data.length())
    }
    let value_bytes : Array[Byte] = []
    for i = 0; i < length; i = i + 1 {
      value_bytes.push(data[pos + i])
    }
    tlvs.push(LldpTlv::{
      tlv_type,
      length,
      value: Bytes::from_array(value_bytes),
    })
    if tlv_type == tlv_type_end {
      break
    }
    pos = pos + length
  }
  tlvs
}

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

///|
pub fn tlv_type_label(tlv_type : Int) -> String {
  match tlv_type {
    0 => "End"
    1 => "ChassisID"
    2 => "PortID"
    3 => "TTL"
    4 => "PortDescription"
    5 => "SystemName"
    6 => "SystemDescription"
    7 => "SystemCapabilities"
    8 => "ManagementAddress"
    127 => "OrganizationSpecific"
    _ => "Unknown(" + tlv_type.to_string() + ")"
  }
}

///|
/// LLDP neighbor info extracted from TLVs.
pub(all) struct LldpNeighbor {
  chassis_id : String
  port_id : String
  ttl : Int
  system_name : String
  system_description : String
  port_description : String
  management_address : String
} derive(Eq, Debug)

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

///|
fn ipv4_bytes_to_string(data : Bytes, offset : Int) -> String {
  data[offset].to_int().to_string() +
  "." +
  data[offset + 1].to_int().to_string() +
  "." +
  data[offset + 2].to_int().to_string() +
  "." +
  data[offset + 3].to_int().to_string()
}

///|
fn parse_management_address(value : Bytes) -> String {
  if value.length() < 2 {
    return ""
  }
  let address_len = value[0].to_int()
  if address_len < 2 || value.length() < 1 + address_len {
    return ""
  }
  let subtype = value[1].to_int()
  if subtype == 1 && address_len == 5 {
    ipv4_bytes_to_string(value, 2)
  } else {
    let address_bytes : Array[Byte] = []
    for i = 1; i < 1 + address_len; i = i + 1 {
      address_bytes.push(value[i])
    }
    @frame.bytes_to_hex(Bytes::from_array(address_bytes), separator=":")
  }
}

///|
/// Extract neighbor info from parsed TLVs.
pub fn extract_neighbor(tlvs : Array[LldpTlv]) -> LldpNeighbor {
  let mut chassis_id = ""
  let mut port_id = ""
  let mut ttl = 0
  let mut system_name = ""
  let mut system_description = ""
  let mut port_description = ""
  let mut management_address = ""
  for tlv in tlvs {
    match tlv.tlv_type {
      1 =>
        if tlv.value.length() > 1 {
          // skip subtype byte
          let sub : Array[Byte] = []
          for i = 1; i < tlv.value.length(); i = i + 1 {
            sub.push(tlv.value[i])
          }
          chassis_id = @frame.bytes_to_hex(
            Bytes::from_array(sub),
            separator=":",
          )
        }
      2 =>
        if tlv.value.length() > 1 {
          let sub : Array[Byte] = []
          for i = 1; i < tlv.value.length(); i = i + 1 {
            sub.push(tlv.value[i])
          }
          port_id = ascii_bytes_to_string(Bytes::from_array(sub))
        }
      3 => if tlv.value.length() >= 2 { ttl = read_u16_be(tlv.value, 0) }
      4 => port_description = ascii_bytes_to_string(tlv.value)
      5 => system_name = ascii_bytes_to_string(tlv.value)
      6 => system_description = ascii_bytes_to_string(tlv.value)
      8 => management_address = parse_management_address(tlv.value)
      _ => ()
    }
  }
  LldpNeighbor::{
    chassis_id,
    port_id,
    ttl,
    system_name,
    system_description,
    port_description,
    management_address,
  }
}

///|
/// Format LLDP neighbor summary.
pub fn format_neighbor(n : LldpNeighbor) -> String {
  let lines : Array[String] = []
  lines.push("chassis_id=" + n.chassis_id)
  lines.push("port_id=" + n.port_id)
  lines.push("ttl=" + n.ttl.to_string())
  lines.push("system_name=" + n.system_name)
  lines.push("system_description=" + n.system_description)
  lines.push("port_description=" + n.port_description)
  lines.push("management_address=" + n.management_address)
  lines.join("\n")
}

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

///|
fn append_bytes(output : Array[Byte], data : Bytes) -> Unit {
  for byte in data {
    output.push(byte)
  }
}

///|
/// Encode a single LLDP TLV.
pub fn encode_tlv(tlv_type : Int, value : Bytes) -> Bytes {
  let output : Array[Byte] = []
  let type_len = ((tlv_type & 0x7F) << 9) | (value.length() & 0x01FF)
  push_u16_be(output, type_len)
  append_bytes(output, value)
  Bytes::from_array(output)
}

///|
/// Build a minimal LLDP frame with required TLVs.
pub fn build_lldp_frame(
  chassis_id : Bytes,
  port_id : Bytes,
  ttl : Int,
  system_name? : String = "",
) -> Bytes {
  let output : Array[Byte] = []
  // Chassis ID TLV (subtype 4 = MAC address)
  let chassis_val : Array[Byte] = [b'\x04']
  append_bytes(chassis_val, chassis_id)
  append_bytes(
    output,
    encode_tlv(tlv_type_chassis_id, Bytes::from_array(chassis_val)),
  )
  // Port ID TLV (subtype 7 = locally assigned)
  let port_val : Array[Byte] = [b'\x07']
  for c in port_id {
    port_val.push(c)
  }
  append_bytes(
    output,
    encode_tlv(tlv_type_port_id, Bytes::from_array(port_val)),
  )
  // TTL TLV
  let ttl_bytes : Array[Byte] = []
  push_u16_be(ttl_bytes, ttl)
  append_bytes(output, encode_tlv(tlv_type_ttl, Bytes::from_array(ttl_bytes)))
  // System Name TLV (optional)
  if system_name != "" {
    let name_bytes = system_name.to_array().map(fn(c) { c.to_int().to_byte() })
    append_bytes(
      output,
      encode_tlv(tlv_type_system_name, Bytes::from_array(name_bytes)),
    )
  }
  // End TLV
  append_bytes(output, encode_tlv(tlv_type_end, Bytes::default()))
  Bytes::from_array(output)
}