// MoonDNS Domain Name Codec with Robust Pointer Defense (RFC 1035 Section 4.1.4)

///|
/// Convert ASCII uppercase letters to lowercase
pub fn to_ascii_lower(s : String) -> String {
  let sb = StringBuilder()
  for c in s {
    if c >= 'A' && c <= 'Z' {
      let code = c.to_int() + 32
      sb.write_char(Int::unsafe_to_char(code))
    } else {
      sb.write_char(c)
    }
  }
  sb.to_string()
}

///|
/// Normalize domain name: lowercase ASCII and strip trailing dot
pub fn normalize_domain(name : String) -> String {
  let lower = to_ascii_lower(name)
  let trimmed = lower.trim().to_owned()
  if trimmed == "" || trimmed == "." {
    ""
  } else if trimmed.has_suffix(".") {
    match trimmed.chop_suffix(".") {
      Some(v) => v.to_owned()
      None => trimmed
    }
  } else {
    trimmed
  }
}

///|
/// RFC 1035 Case-insensitive domain equality
pub fn domain_equal(a : String, b : String) -> Bool {
  normalize_domain(a) == normalize_domain(b)
}

///|
/// Split a normalized domain into labels
pub fn split_labels(domain : String) -> Array[String] {
  let norm = normalize_domain(domain)
  if norm == "" {
    return []
  }
  let result : Array[String] = []
  let mut curr = StringBuilder()
  for c in norm {
    if c == '.' {
      result.push(curr.to_string())
      curr = StringBuilder()
    } else {
      curr.write_char(c)
    }
  }
  result.push(curr.to_string())
  result
}

///|
/// Compression dictionary for domain name serialization
pub(all) struct NameCompressor {
  entries : Array[(String, Int)]
}

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

///|
pub fn NameCompressor::find(self : NameCompressor, suffix : String) -> Option[Int] {
  let norm = normalize_domain(suffix)
  for entry in self.entries {
    if entry.0 == norm {
      return Some(entry.1)
    }
  }
  None
}

///|
pub fn NameCompressor::add(
  self : NameCompressor,
  suffix : String,
  offset : Int,
) -> Unit {
  let norm = normalize_domain(suffix)
  // Store only if within 14-bit pointer reach
  if offset < 0x4000 {
    self.entries.push((norm, offset))
  }
}

///|
/// Encode domain name into DnsBuffer, optionally using compression
pub fn encode_name(
  name : String,
  buf : DnsBuffer,
  comp : NameCompressor?,
  compress : Bool,
) -> Unit {
  let norm = normalize_domain(name)
  if norm == "" {
    buf.write_byte(b'\x00')
    return
  }

  let labels = split_labels(norm)
  let n = labels.length()

  for i in 0.. i {
        suffix_sb.write_char('.')
      }
      suffix_sb.write_string(labels[j])
    }
    let suffix = suffix_sb.to_string()

    if compress {
      match comp {
        Some(c) =>
          match c.find(suffix) {
            Some(target_offset) => {
              // Write 2-byte compression pointer: 11xxxxxx xxxxxxxx
              let p0 = (0xC0 | ((target_offset >> 8) & 0x3F)).to_byte()
              let p1 = (target_offset & 0xFF).to_byte()
              buf.write_byte(p0)
              buf.write_byte(p1)
              return
            }
            None =>
              if buf.length() < 0x4000 {
                c.add(suffix, buf.length())
              }
          }
        None => ()
      }
    }

    let label = labels[i]
    let len = label.length()
    buf.write_byte(len.to_byte())
    for c in label {
      buf.write_byte(c.to_int().to_byte())
    }
  }

  // Terminating root label
  buf.write_byte(b'\x00')
}

///|
/// Parse domain name from bytes with full security guards:
/// - Guard 1: Loop detection (A -> A, A -> B -> A)
/// - Guard 2: Forward pointer / self pointer defense (target >= ptr_pos)
/// - Guard 3: Out of bounds defense (target >= bytes.length)
/// - Guard 4: Jump limit defense (<= 128 jumps)
/// - Guard 5: Label length <= 63 bytes
/// - Guard 6: Total name length <= 255 bytes
pub fn parse_name(
  bytes : Bytes,
  start_offset : Int,
) -> Result[(String, Int), DnsError] {
  let mut curr = start_offset
  let mut next_pos = -1
  let mut jumps = 0
  let visited_pointers : Array[Int] = []
  let labels : Array[String] = []
  let mut total_len = 0

  for ;; {
    if curr >= bytes.length() {
      return Err(DnsError::BufferUnderflow)
    }

    let b0 = bytes[curr].to_int()
    if b0 == 0 {
      // Root label (end of name)
      if next_pos < 0 {
        next_pos = curr + 1
      }
      break
    }

    let flag = b0 & 0xC0
    if flag == 0xC0 {
      // Compression pointer (2 bytes)
      if curr + 1 >= bytes.length() {
        return Err(DnsError::BufferUnderflow)
      }
      let ptr_pos = curr
      let target_offset = ((b0 & 0x3F) << 8) | bytes[curr + 1].to_int()

      if next_pos < 0 {
        next_pos = curr + 2
      }

      // Guard 2: Target must be strictly prior to current pointer position
      if target_offset >= ptr_pos {
        return Err(
          DnsError::PointerOutOfBounds(
            "Target offset \{target_offset} >= pointer position \{ptr_pos}",
          ),
        )
      }

      // Guard 3: Target offset must not exceed total message length
      if target_offset >= bytes.length() {
        return Err(
          DnsError::PointerOutOfBounds(
            "Target offset \{target_offset} >= packet length \{bytes.length()}",
          ),
        )
      }

      // Guard 1: Loop detection across visited pointer targets
      for v in visited_pointers {
        if v == target_offset {
          return Err(
            DnsError::PointerLoop("Loop detected at offset \{target_offset}"),
          )
        }
      }
      visited_pointers.push(target_offset)

      // Guard 4: Jump count limit
      jumps += 1
      if jumps > 128 {
        return Err(DnsError::PointerJumpLimitExceeded)
      }

      curr = target_offset
    } else if flag == 0 {
      // Standard uncompressed label
      let label_len = b0
      if label_len > 63 {
        return Err(DnsError::InvalidLabelLength(label_len))
      }
      if curr + 1 + label_len > bytes.length() {
        return Err(DnsError::BufferUnderflow)
      }

      total_len += label_len + 1
      if total_len > 255 {
        return Err(DnsError::InvalidDomainName("Domain exceeds 255 bytes"))
      }

      let sb = StringBuilder()
      for i in 0.. 0 {
        sb.write_char('.')
      }
      sb.write_string(labels[i])
    }
    sb.to_string()
  }

  let consumed_pos = if next_pos < 0 { curr + 1 } else { next_pos }
  Ok((final_name, consumed_pos))
}