///|
/// Percent-encoding and -decoding for WebFinger query components.
///
/// RFC 7033 Section 4.1 requires that every WebFinger request parameter
/// value be percent-encoded per RFC 3986 Section 2.1, with the addition
/// that `=` and `&` inside parameter values are also percent-encoded.
/// This module implements exactly that:
///
/// * `percent_encode_component` — keeps RFC 3986 unreserved characters
///   (`A-Z a-z 0-9 - . _ ~`) and percent-encodes everything else,
///   including `=`, `&` and all non-ASCII characters (UTF-8 bytes).
/// * `percent_decode` — strict decoder: rejects malformed `%` escapes and
///   byte sequences that are not valid UTF-8.
///
/// UTF-8 encoding/decoding is implemented here directly (a tiny, fully
/// tested RFC 3629 subset) so the codec has no hidden failure modes.

///|
/// Internal: which characters are RFC 3986 unreserved.
fn is_unreserved(c : Char) -> Bool {
  c.is_ascii_alphabetic() ||
  c.is_ascii_digit() ||
  c == '-' ||
  c == '.' ||
  c == '_' ||
  c == '~'
}

///|
/// Internal: RFC 3986 sub-delims (`! $ & ' ( ) * + , ; =`).
fn is_sub_delim(c : Char) -> Bool {
  c == '!' ||
  c == '$' ||
  c == '&' ||
  c == '\'' ||
  c == '(' ||
  c == ')' ||
  c == '*' ||
  c == '+' ||
  c == ',' ||
  c == ';' ||
  c == '='
}

///|
/// Internal: ASCII classification helpers over UTF-16 code units, used by
/// the index-based scanners in this package (URI, percent and acct checks
/// only ever inspect ASCII positions, which are single code units).
fn is_ascii_alpha_u16(u : UInt16) -> Bool {
  (u >= 65 && u <= 90) || (u >= 97 && u <= 122)
}

///|
fn is_ascii_digit_u16(u : UInt16) -> Bool {
  u >= 48 && u <= 57
}

///|
fn is_alnum_u16(u : UInt16) -> Bool {
  is_ascii_alpha_u16(u) || is_ascii_digit_u16(u)
}

///|
fn is_unreserved_u16(u : UInt16) -> Bool {
  is_alnum_u16(u) || u == 45 || u == 46 || u == 95 || u == 126
}

///|
fn is_sub_delim_u16(u : UInt16) -> Bool {
  u == 33 ||
  u == 36 ||
  u == 38 ||
  u == 39 ||
  u == 40 ||
  u == 41 ||
  u == 42 ||
  u == 43 ||
  u == 44 ||
  u == 59 ||
  u == 61
}

///|
/// Internal: numeric value of an ASCII hex digit given as a UTF-16 code
/// unit, or `None`.
fn hex_value_u16(u : UInt16) -> Int? {
  if is_ascii_digit_u16(u) {
    Some(u.to_int() - 48)
  } else if u >= 97 && u <= 102 {
    Some(u.to_int() - 87)
  } else if u >= 65 && u <= 70 {
    Some(u.to_int() - 55)
  } else {
    None
  }
}

///|
/// Internal: uppercase hex digit for a byte value 0..15.
fn hex_digit_upper(n : Int) -> Char {
  if n < 10 {
    (48 + n).to_char().unwrap()
  } else {
    (55 + n).to_char().unwrap()
  }
}

///|
/// Internal: UTF-8 bytes of a Unicode code point (RFC 3629). The code
/// point must be a valid Unicode scalar value; callers guarantee that by
/// iterating `Char` values.
fn utf8_bytes_of_code_point(cp : Int) -> Array[Byte] {
  let bytes : Array[Byte] = []
  if cp < 0x80 {
    bytes.push(cp.to_byte())
  } else if cp < 0x800 {
    bytes.push((0xC0 | (cp >> 6)).to_byte())
    bytes.push((0x80 | (cp & 0x3F)).to_byte())
  } else if cp < 0x10000 {
    bytes.push((0xE0 | (cp >> 12)).to_byte())
    bytes.push((0x80 | ((cp >> 6) & 0x3F)).to_byte())
    bytes.push((0x80 | (cp & 0x3F)).to_byte())
  } else {
    bytes.push((0xF0 | (cp >> 18)).to_byte())
    bytes.push((0x80 | ((cp >> 12) & 0x3F)).to_byte())
    bytes.push((0x80 | ((cp >> 6) & 0x3F)).to_byte())
    bytes.push((0x80 | (cp & 0x3F)).to_byte())
  }
  bytes
}

///|
/// Internal: append `%XX` for one byte to a builder.
fn write_percent_byte(sb : StringBuilder, b : Byte) -> Unit {
  let n = b.to_int()
  sb.write_char('%')
  sb.write_char(hex_digit_upper(n / 16))
  sb.write_char(hex_digit_upper(n % 16))
}

///|
/// Internal: percent-encode with a caller-supplied pass-through set.
/// Non-ASCII characters are always percent-encoded byte-wise as UTF-8.
fn encode_with(input : String, keep : (Char) -> Bool) -> String {
  let sb = StringBuilder::new(size_hint=input.length() * 3)
  for c in input {
    if keep(c) {
      sb.write_char(c)
    } else if c.is_ascii() {
      write_percent_byte(sb, c.to_int().to_byte())
    } else {
      let bytes = utf8_bytes_of_code_point(c.to_int())
      for b in bytes {
        write_percent_byte(sb, b)
      }
    }
  }
  sb.to_string()
}

///|
/// Percent-encode a query component value per RFC 7033 Section 4.1:
/// unreserved characters pass through, everything else (including `=`,
/// `&`, and non-ASCII text) is percent-encoded.
pub fn percent_encode_component(input : String) -> String {
  encode_with(input, is_unreserved)
}

///|
/// Percent-encode the localpart of an `acct` URI (RFC 7565 Section 7):
/// unreserved characters and sub-delims pass through.
pub fn percent_encode_acct_localpart(input : String) -> String {
  encode_with(input, fn(c) { is_unreserved(c) || is_sub_delim(c) })
}

///|
/// Internal: strict UTF-8 decoding of a byte array into a string.
/// Rejects truncated sequences, overlong encodings, surrogate code points
/// (U+D800..U+DFFF) and values above U+10FFFF. Returns a
/// `WebFingerError` on any violation.
fn decode_utf8_strict(bytes : Array[Byte]) -> String raise {
  let sb = StringBuilder::new(size_hint=bytes.length())
  let mut i = 0
  while i < bytes.length() {
    let b0 = bytes[i].to_int()
    if b0 < 0x80 {
      sb.write_char(b0.to_char().unwrap())
      i = i + 1
    } else if b0 < 0xC2 {
      // Continuation byte or overlong 2-byte lead.
      raise WebFingerError(
        Uri,
        InvalidPercentEncoding,
        None,
        "invalid UTF-8: bad leading byte",
      )
    } else if b0 < 0xE0 {
      if i + 1 >= bytes.length() {
        raise WebFingerError(
          Uri,
          InvalidPercentEncoding,
          None,
          "invalid UTF-8: truncated 2-byte sequence",
        )
      }
      let b1 = bytes[i + 1].to_int()
      if b1 < 0x80 || b1 > 0xBF {
        raise WebFingerError(
          Uri,
          InvalidPercentEncoding,
          None,
          "invalid UTF-8: bad continuation byte",
        )
      }
      let cp = ((b0 - 0xC0) << 6) | (b1 - 0x80)
      sb.write_char(cp.to_char().unwrap())
      i = i + 2
    } else if b0 < 0xF0 {
      if i + 2 >= bytes.length() {
        raise WebFingerError(
          Uri,
          InvalidPercentEncoding,
          None,
          "invalid UTF-8: truncated 3-byte sequence",
        )
      }
      let b1 = bytes[i + 1].to_int()
      let b2 = bytes[i + 2].to_int()
      if b1 < 0x80 || b1 > 0xBF || b2 < 0x80 || b2 > 0xBF {
        raise WebFingerError(
          Uri,
          InvalidPercentEncoding,
          None,
          "invalid UTF-8: bad continuation byte",
        )
      }
      let cp = ((b0 - 0xE0) << 12) | ((b1 - 0x80) << 6) | (b2 - 0x80)
      if cp < 0x800 {
        raise WebFingerError(
          Uri,
          InvalidPercentEncoding,
          None,
          "invalid UTF-8: overlong encoding",
        )
      }
      if cp >= 0xD800 && cp <= 0xDFFF {
        raise WebFingerError(
          Uri,
          InvalidPercentEncoding,
          None,
          "invalid UTF-8: surrogate code point",
        )
      }
      sb.write_char(cp.to_char().unwrap())
      i = i + 3
    } else if b0 < 0xF5 {
      if i + 3 >= bytes.length() {
        raise WebFingerError(
          Uri,
          InvalidPercentEncoding,
          None,
          "invalid UTF-8: truncated 4-byte sequence",
        )
      }
      let b1 = bytes[i + 1].to_int()
      let b2 = bytes[i + 2].to_int()
      let b3 = bytes[i + 3].to_int()
      if b1 < 0x80 ||
        b1 > 0xBF ||
        b2 < 0x80 ||
        b2 > 0xBF ||
        b3 < 0x80 ||
        b3 > 0xBF {
        raise WebFingerError(
          Uri,
          InvalidPercentEncoding,
          None,
          "invalid UTF-8: bad continuation byte",
        )
      }
      let cp = ((b0 - 0xF0) << 18) |
        ((b1 - 0x80) << 12) |
        ((b2 - 0x80) << 6) |
        (b3 - 0x80)
      if cp < 0x10000 {
        raise WebFingerError(
          Uri,
          InvalidPercentEncoding,
          None,
          "invalid UTF-8: overlong encoding",
        )
      }
      if cp > 0x10FFFF {
        raise WebFingerError(
          Uri,
          InvalidPercentEncoding,
          None,
          "invalid UTF-8: above U+10FFFF",
        )
      }
      sb.write_char(cp.to_char().unwrap())
      i = i + 4
    } else {
      raise WebFingerError(
        Uri,
        InvalidPercentEncoding,
        None,
        "invalid UTF-8: invalid leading byte",
      )
    }
  }
  sb.to_string()
}

///|
/// Strictly percent-decode a string: every `%` must start a `%XX` escape
/// with two hex digits; raw non-ASCII characters are rejected (URIs are
/// ASCII; non-ASCII text must be percent-encoded); the decoded byte
/// stream must be valid UTF-8.
pub fn percent_decode(input : String) -> Result[String, WebFingerError] {
  try {
    let bytes : Array[Byte] = []
    let mut i = 0
    while i < input.length() {
      let u = input[i]
      if u == 37 {
        if i + 2 >= input.length() {
          raise WebFingerError(
            Uri,
            InvalidPercentEncoding,
            None,
            "malformed percent escape at end of input",
          )
        }
        match (hex_value_u16(input[i + 1]), hex_value_u16(input[i + 2])) {
          (Some(v1), Some(v2)) => {
            bytes.push((v1 * 16 + v2).to_byte())
            i = i + 3
          }
          _ =>
            raise WebFingerError(
              Uri,
              InvalidPercentEncoding,
              Some(i),
              "malformed percent escape",
            )
        }
      } else if u > 0x7F {
        raise WebFingerError(
          Uri,
          InvalidPercentEncoding,
          Some(i),
          "non-ASCII character must be percent-encoded",
        )
      } else {
        bytes.push(u.to_byte())
        i = i + 1
      }
    }
    Ok(decode_utf8_strict(bytes))
  } catch {
    e => Err(unwrap_webfinger_error(e))
  }
}