///|
/// RFC 4648 Base64 URL-safe alphabet (no padding for JWT).
let b64url_chars : Array[Char] = [
  'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
  'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
  'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
  'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_',
]

///|
/// Encode bytes to a Base64URL string (no padding).
pub fn base64url_encode(data : Bytes) -> String {
  let result : Array[Char] = []
  let len = data.length()
  let mut i = 0
  while i + 3 <= len {
    let b0 = data[i].to_int()
    let b1 = data[i + 1].to_int()
    let b2 = data[i + 2].to_int()
    let n = (b0 << 16) | (b1 << 8) | b2
    result.push(b64url_chars[(n >> 18) & 0x3F])
    result.push(b64url_chars[(n >> 12) & 0x3F])
    result.push(b64url_chars[(n >> 6) & 0x3F])
    result.push(b64url_chars[n & 0x3F])
    i += 3
  }
  let rem = len - i
  if rem == 1 {
    let n = data[i].to_int() << 16
    result.push(b64url_chars[(n >> 18) & 0x3F])
    result.push(b64url_chars[(n >> 12) & 0x3F])
  } else if rem == 2 {
    let b0 = data[i].to_int()
    let b1 = data[i + 1].to_int()
    let n = (b0 << 16) | (b1 << 8)
    result.push(b64url_chars[(n >> 18) & 0x3F])
    result.push(b64url_chars[(n >> 12) & 0x3F])
    result.push(b64url_chars[(n >> 6) & 0x3F])
  }
  String::from_array(result)
}

///|
/// Decode a Base64URL string to bytes.
pub fn base64url_decode(s : String) -> Bytes? {
  // Build reverse lookup: char code -> index
  let map : Map[Int, Int] = Map([])
  for i in 0..<64 {
    map[b64url_chars[i].to_int()] = i
  }
  // Count valid (non-padding) chars
  let chars : Array[Char] = []
  for c in s {
    chars.push(c)
  }
  let char_count = chars.length()
  let result : Array[Byte] = []
  let mut i = 0
  while i + 4 <= char_count {
    let mut n = 0
    let mut valid = true
    for j in 0..<4 {
      let idx = i + j
      let c = chars[idx]
      match map.get(c.to_int()) {
        Some(v) => n = (n << 6) | v
        None => {
          valid = false
          break
        }
      }
    }
    if !valid {
      return None
    }
    result.push(((n >> 16) & 0xFF).to_byte())
    if i + 3 <= char_count {
      result.push(((n >> 8) & 0xFF).to_byte())
    }
    if i + 4 <= char_count {
      result.push((n & 0xFF).to_byte())
    }
    i += 4
  }
  // Handle remaining 2 or 3 chars
  let rem = char_count - i
  if rem == 2 {
    let mut n = 0
    let mut valid = true
    for j in 0..<2 {
      match map.get(chars[i + j].to_int()) {
        Some(v) => n = (n << 6) | v
        None => {
          valid = false
          break
        }
      }
    }
    if !valid {
      return None
    }
    result.push(((n >> 4) & 0xFF).to_byte())
  } else if rem == 3 {
    let mut n = 0
    let mut valid = true
    for j in 0..<3 {
      match map.get(chars[i + j].to_int()) {
        Some(v) => n = (n << 6) | v
        None => {
          valid = false
          break
        }
      }
    }
    if !valid {
      return None
    }
    result.push(((n >> 10) & 0xFF).to_byte())
    result.push(((n >> 2) & 0xFF).to_byte())
  }
  Some(Bytes::from_array(result))
}

///|
/// Encode a string to Base64URL.
pub fn base64url_encode_str(s : String) -> String {
  base64url_encode(@utf8.encode(s))
}