///|
/// Lightweight URI checks for the WebFinger scope.
///
/// This is deliberately NOT a complete RFC 3986 URI resolver. It covers
/// only what WebFinger needs (RFC 7033 Sections 2, 4.1, 4.4):
///
/// * a WebFinger `resource` must be an absolute URI — RFC 7033 Section 2
///   says relative references are not used with WebFinger;
/// * a URI must not contain spaces or control characters;
/// * every `%` must begin a well-formed `%XX` escape;
/// * non-ASCII characters are rejected (they must be percent-encoded;
///   full IDNA/IRI handling is out of scope, see `docs/limitations.md`);
/// * the scheme must match RFC 3986 `scheme = ALPHA *( ALPHA / DIGIT /
///   "+" / "-" / "." )`.
///
/// `check_absolute_uri` accepts any scheme: RFC 7033 Section 4.5 is
/// neutral about the scheme of the query target (`acct:`, `mailto:`,
/// `http:`, `https:`, `urn:`, `tag:` and others are all fine).

///|
/// Internal: RFC 3986 scheme character check (first position).
fn is_scheme_start_u16(u : UInt16) -> Bool {
  is_ascii_alpha_u16(u)
}

///|
/// Internal: RFC 3986 scheme character check (subsequent positions).
fn is_scheme_char_u16(u : UInt16) -> Bool {
  is_ascii_alpha_u16(u) ||
  is_ascii_digit_u16(u) ||
  u == 43 ||
  u == 45 ||
  u == 46
}

///|
/// Internal: whether a code unit is a C0/C1 control or a space, i.e.
/// never legal unescaped in a URI.
fn is_uri_forbidden_u16(u : UInt16) -> Bool {
  u < 0x20 || u == 0x20 || (u >= 0x7F && u <= 0x9F)
}

///|
/// Internal: scan a string for raw percent escapes; every `%` must be
/// followed by two ASCII hex digits. Returns the offending offset or
/// `None` when the string is well-formed.
fn malformed_percent_offset(s : String) -> Int? {
  let mut i = 0
  while i < s.length() {
    let u = s[i]
    if u == 37 {
      if i + 2 >= s.length() {
        return Some(i)
      }
      match (hex_value_u16(s[i + 1]), hex_value_u16(s[i + 2])) {
        (Some(_), Some(_)) => i = i + 3
        _ => return Some(i)
      }
    } else {
      i = i + 1
    }
  }
  None
}

///|
/// Internal: find the scheme terminator `:`; returns the index of `:`
/// or `None`.
fn scheme_colon_offset(s : String) -> Int? {
  let mut i = 0
  while i < s.length() {
    let u = s[i]
    if u == 58 {
      return Some(i)
    }
    if !is_scheme_char_u16(u) {
      return None
    }
    i = i + 1
  }
  None
}

///|
/// Check that `s` is an absolute URI within the WebFinger scope.
/// Errors use stage `Uri` and kind `InvalidUri` (or `InvalidPercentEncoding`
/// for bad escapes).
pub fn check_absolute_uri(s : String) -> Result[Unit, WebFingerError] {
  try {
    check_absolute_uri_inner(s)
    Ok(())
  } catch {
    e => Err(unwrap_webfinger_error(e))
  }
}

///|
/// Internal raise-based version of `check_absolute_uri`.
fn check_absolute_uri_inner(s : String) -> Unit raise {
  if s.length() == 0 {
    raise WebFingerError(Uri, InvalidUri, None, "empty string is not a URI")
  }
  let mut i = 0
  let mut saw_forbidden = false
  let mut saw_non_ascii = false
  while i < s.length() {
    let u = s[i]
    if is_uri_forbidden_u16(u) {
      saw_forbidden = true
    }
    if u > 0x7F {
      saw_non_ascii = true
    }
    i = i + 1
  }
  if saw_forbidden {
    raise WebFingerError(
      Uri,
      InvalidUri,
      None,
      "URI must not contain spaces or control characters",
    )
  }
  if saw_non_ascii {
    raise WebFingerError(
      Uri,
      InvalidUri,
      None,
      "URI must be ASCII; percent-encode non-ASCII characters",
    )
  }
  match malformed_percent_offset(s) {
    Some(off) =>
      raise WebFingerError(
        Uri,
        InvalidPercentEncoding,
        Some(off),
        "malformed percent escape",
      )
    None => ()
  }
  // Scheme: the first character must start a scheme and a ':' must
  // terminate it. Anything else is a relative reference (not used with
  // WebFinger) or not a URI at all.
  if !is_scheme_start_u16(s[0]) {
    raise WebFingerError(
      Uri,
      InvalidUri,
      Some(0),
      "not an absolute URI: missing scheme",
    )
  }
  match scheme_colon_offset(s) {
    Some(colon) =>
      if colon <= 0 {
        raise WebFingerError(Uri, InvalidUri, None, "empty URI scheme")
      }
    None =>
      raise WebFingerError(
        Uri,
        InvalidUri,
        None,
        "not an absolute URI: missing ':' after scheme",
      )
  }
}

///|
/// Extract the scheme of a URI-shaped string (the part before `:`),
/// lowercased, or `None` when no valid scheme is present. Does not
/// validate the rest of the string.
pub fn uri_scheme(s : String) -> String? {
  match scheme_colon_offset(s) {
    Some(0) => None
    Some(n) =>
      if is_scheme_start_u16(s[0]) {
        Some(s[0:n].to_lower().to_owned())
      } else {
        None
      }
    None => None
  }
}

///|
/// Case-insensitive scheme comparison.
pub fn scheme_is(s : String, scheme : String) -> Bool {
  match uri_scheme(s) {
    Some(actual) => actual == scheme.to_lower()
    None => false
  }
}

///|
/// Internal: whether a string is a plausible DNS domain name within the
/// WebFinger scope: ASCII letters, digits, hyphens and dots; non-empty
/// labels; no label may start or end with a hyphen; the final label must
/// not be all digits (an IPv4 address literal is not a domain name);
/// port and userinfo are rejected. IDN (U-labels) is out of scope:
/// internationalized hosts must be given as A-labels.
fn is_domain_name(s : String) -> Bool {
  if s.length() == 0 || s.length() > 253 {
    return false
  }
  let mut label_len = 0
  let mut i = 0
  while i < s.length() {
    let u = s[i]
    if u == 46 {
      if label_len == 0 {
        return false
      }
      label_len = 0
    } else if is_ascii_alpha_u16(u) || is_ascii_digit_u16(u) || u == 45 {
      if u == 45 && label_len == 0 {
        return false
      }
      if u == 45 && i + 1 >= s.length() {
        return false
      }
      if u == 45 && i + 1 < s.length() && s[i + 1] == 46 {
        return false
      }
      label_len = label_len + 1
    } else {
      return false
    }
    i = i + 1
  }
  if label_len == 0 {
    return false
  }
  // The final label must not be all digits: that is an IPv4 literal,
  // not a domain name (RFC 7565 wants a DNS domain name here).
  let mut last_dot = -1
  let mut k = 0
  while k < s.length() {
    if s[k] == 46 {
      last_dot = k
    }
    k = k + 1
  }
  let mut all_digits = true
  let mut m = last_dot + 1
  while m < s.length() {
    if !is_ascii_digit_u16(s[m]) {
      all_digits = false
    }
    m = m + 1
  }
  if all_digits {
    false
  } else {
    true
  }
}

///|
/// Check that `host` is a plausible DNS domain name for use in `acct`
/// URIs and WebFinger origins. Advisory-level strictness: this is a
/// conservative syntactic heuristic, not a resolver.
pub fn check_hostname(host : String) -> Result[Unit, WebFingerError] {
  if host.length() == 0 {
    return Err(WebFingerError(Uri, InvalidUri, None, "empty host"))
  }
  if host.contains("[") || host.contains("]") || host.contains(":") {
    return Err(
      WebFingerError(
        Uri,
        InvalidUri,
        None,
        "host must be a domain name, not an IP literal or host:port",
      ),
    )
  }
  if !is_domain_name(host) {
    return Err(WebFingerError(Uri, InvalidUri, None, "invalid domain name"))
  }
  Ok(())
}