///|
/// One entry of a `NO_PROXY` list, as curl reads it.
priv enum NoProxyRule {
  /// `*`: every host.
  Everything
  /// A host name, lower-case without a leading or trailing dot: the host
  /// itself and every subdomain, on `port` or any port.
  Domain(name~ : String, port~ : Int32?)
  /// An IP address, or with `/prefix` a network: the addresses whose first
  /// `prefix` bits match `address` (4 bytes for IPv4, 16 for IPv6), on
  /// `port` or any port.
  Network(address~ : Array[Int32], prefix~ : Int32, port~ : Int32?)
}

///|
/// The rules of a `NO_PROXY` value, by curl's reading: comma-separated
/// entries, spaces around them ignored; `*` for every host; a host name,
/// with or without a leading dot, for itself and its subdomains; an IPv4 or
/// IPv6 address, bracketed or not, optionally a `/prefix` network (curl
/// 7.86, Go, reqwest). `host:port` and `[v6]:port` restrict an entry to that
/// port, as httpx, Go and Python do. Entries that are none of these are
/// skipped, so a typo bypasses nothing.
fn parse_no_proxy(text : String) -> Array[NoProxyRule] {
  let rules = []
  for entry in text.split(",") {
    let entry = entry.trim().to_owned()
    if entry.is_empty() {
      continue
    }
    if no_proxy_rule(entry) is Some(rule) {
      rules.push(rule)
    }
  }
  rules
}

///|
fn no_proxy_rule(entry : String) -> NoProxyRule? {
  if entry == "*" {
    return Some(Everything)
  }
  // `[v6]`, `[v6]:port`, `[v6/prefix]`
  if entry.has_prefix("[") {
    guard entry.find("]") is Some(close) else { return None }
    let after = entry[close + 1:].to_owned()
    let port = if after.is_empty() {
      None
    } else if after.has_prefix(":") {
      guard port_number(after[1:].to_owned()) is Some(port) else { return None }
      Some(port)
    } else {
      return None
    }
    return network_rule(entry[1:close].to_owned(), port~)
  }
  if entry.contains("/") {
    return network_rule(entry, port=None)
  }
  let colons = entry.iter().filter(c => c == ':').count()
  if colons >= 2 {
    return network_rule(entry, port=None)
  }
  let (host, port) = if entry.rev_find(":") is Some(colon) {
    guard port_number(entry[colon + 1:].to_owned()) is Some(port) else {
      return None
    }
    (entry[:colon].to_owned(), Some(port))
  } else {
    (entry, None)
  }
  if ipv4_bytes(host) is Some(address) {
    return Some(Network(address~, prefix=32, port~))
  }
  let mut name = host.to_lower()
  if name.has_prefix(".") {
    name = name[1:].to_owned()
  }
  if name.has_suffix(".") {
    name = name[:name.length() - 1].to_owned()
  }
  guard !name.is_empty() &&
    name.iter().all(c => c is ('a'..<='z' | '0'..<='9' | '-' | '.' | '_')) else {
    return None
  }
  Some(Domain(name~, port~))
}

///|
/// An address or `address/prefix` rule, IPv4 or IPv6.
fn network_rule(text : String, port~ : Int32?) -> NoProxyRule? {
  let (address_text, prefix_text) = match text.find("/") {
    Some(slash) => (text[:slash].to_owned(), Some(text[slash + 1:].to_owned()))
    None => (text, None)
  }
  let address = match ipv4_bytes(address_text) {
    Some(bytes) => bytes
    None => {
      guard ipv6_bytes(address_text) is Some(bytes) else { return None }
      bytes
    }
  }
  let bits = address.length() * 8
  let prefix = match prefix_text {
    None => bits
    Some(digits) => {
      guard !digits.is_empty() &&
        digits.length() <= 3 &&
        digits.iter().all(c => c.is_ascii_digit()) else {
        return None
      }
      let prefix = digits
        .iter()
        .fold(init=0, (n, c) => n * 10 + (c.to_int() - '0'.to_int()))
      guard prefix <= bits else { return None }
      prefix
    }
  }
  Some(Network(address~, prefix~, port~))
}

///|
/// Whether a request to `host` on `port` skips the proxy. `host` is as a
/// canonical origin spells it: lower-case, an IPv6 literal without brackets.
/// Host names are compared as text and never resolved, so an address rule
/// matches only a URL written with that address.
fn bypasses_proxy(
  rules : Array[NoProxyRule],
  host : String,
  port : Int32,
) -> Bool {
  let host = if host.has_suffix(".") {
    host[:host.length() - 1].to_owned()
  } else {
    host
  }
  let address = match ipv4_bytes(host) {
    Some(bytes) => Some(bytes)
    None => if host.contains(":") { ipv6_bytes(host) } else { None }
  }
  rules.any(rule => {
    match rule {
      Everything => true
      Domain(name~, port=rule_port) =>
        address is None &&
        port_matches(rule_port, port) &&
        (host == name || host.has_suffix("." + name))
      Network(address=network, prefix~, port=rule_port) =>
        port_matches(rule_port, port) &&
        address is Some(bytes) &&
        in_network(bytes, network, prefix)
    }
  })
}

///|
fn port_matches(rule : Int32?, port : Int32) -> Bool {
  match rule {
    None => true
    Some(wanted) => wanted == port
  }
}

///|
/// Whether `address` shares its first `prefix` bits with `network`, both of
/// one family.
fn in_network(
  address : Array[Int32],
  network : Array[Int32],
  prefix : Int32,
) -> Bool {
  guard address.length() == network.length() else { return false }
  for i in 0.. Int32? {
  guard !text.is_empty() &&
    text.length() <= 5 &&
    text.iter().all(c => c.is_ascii_digit()) else {
    return None
  }
  let port = text
    .iter()
    .fold(init=0, (n, c) => n * 10 + (c.to_int() - '0'.to_int()))
  if port >= 1 && port <= 65535 {
    Some(port)
  } else {
    None
  }
}

///|
/// The four bytes of a dotted-decimal IPv4 address.
fn ipv4_bytes(text : String) -> Array[Int32]? {
  let parts = text.split(".").to_array()
  guard parts.length() == 4 else { return None }
  let bytes = []
  for part in parts {
    guard !part.is_empty() &&
      part.length() <= 3 &&
      part.iter().all(c => c.is_ascii_digit()) else {
      return None
    }
    let value = part
      .iter()
      .fold(init=0, (n, c) => n * 10 + (c.to_int() - '0'.to_int()))
    guard value <= 255 else { return None }
    bytes.push(value)
  }
  Some(bytes)
}

///|
/// The sixteen bytes of an RFC 4291 IPv6 address: hex groups with at most
/// one `::`, the last 32 bits optionally a dotted quad. Zone identifiers
/// are refused.
fn ipv6_bytes(text : String) -> Array[Int32]? {
  let halves = text.split("::").to_array()
  guard halves.length() <= 2 else { return None }
  let parsed : Array[Array[Int32]] = []
  for h, half in halves {
    let groups = []
    if !half.is_empty() {
      let parts = half.split(":").to_array()
      for p, part in parts {
        let last = h == halves.length() - 1 && p == parts.length() - 1
        if last && part.contains(".") {
          guard ipv4_bytes(part.to_owned()) is Some(quad) else { return None }
          groups.push(quad[0] * 256 + quad[1])
          groups.push(quad[2] * 256 + quad[3])
        } else {
          guard part.length() >= 1 &&
            part.length() <= 4 &&
            part.iter().all(c => c.is_ascii_hexdigit()) else {
            return None
          }
          groups.push(
            part.iter().fold(init=0, (n, c) => n * 16 + hex_digit_value(c)),
          )
        }
      }
    }
    parsed.push(groups)
  }
  let groups = if parsed.length() == 2 {
    let missing = 8 - parsed[0].length() - parsed[1].length()
    guard missing >= 1 else { return None }
    [..parsed[0], ..Array::make(missing, 0), ..parsed[1]]
  } else {
    guard parsed[0].length() == 8 else { return None }
    parsed[0]
  }
  let bytes = []
  for group in groups {
    bytes.push(group >> 8)
    bytes.push(group & 0xff)
  }
  Some(bytes)
}

///|
fn hex_digit_value(c : Char) -> Int32 {
  match c {
    '0'..<='9' => c.to_int() - '0'.to_int()
    'a'..<='f' => c.to_int() - 'a'.to_int() + 10
    _ => c.to_int() - 'A'.to_int() + 10
  }
}