// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

///|
/// Which field section a list of fields is, for the rules of RFC 9113 §8.
priv enum Section {
  Request
  // The request a PUSH_PROMISE carries, which must also be safe and cacheable.
  Promise
  Response
  Trailers
}

///|
/// Why `fields` is not a valid section of its kind, or `None` (RFC 9113 §8.2, §8.3,
/// RFC 8441 §4).
///
/// `extended` is whether the receiver of the request has turned on extended CONNECT,
/// which is what lets a request carry `:protocol`.
fn fault(
  fields : Array[@header.Header],
  section : Section,
  extended~ : Bool = false,
) -> String? {
  let seen : Array[String] = []
  let mut regular = false
  let mut verb = ""
  let mut path = ""
  let mut status = b""
  let mut length : Int32? = None
  for f in fields {
    if f.name.length() == 0 {
      return Some("an empty field name")
    }
    let name = text(f.name)
    if f.name[0] == b':' {
      if regular {
        return Some("a pseudo-header after a regular field")
      }
      let allowed = match section {
        Request | Promise =>
          name is (":method" | ":scheme" | ":authority" | ":path") ||
          (name == ":protocol" && extended)
        Response => name == ":status"
        Trailers => false
      }
      if !allowed {
        return Some("the pseudo-header \{name} where it may not be")
      }
      if seen.contains(name) {
        return Some("the pseudo-header \{name} twice")
      }
      seen.push(name)
      match name {
        ":method" => verb = text(f.value)
        ":path" => path = text(f.value)
        ":status" => status = f.value
        _ => ()
      }
    } else {
      regular = true
      for b in f.name {
        let c = b.to_int()
        if c <= 0x20 || (c >= 0x41 && c <= 0x5a) || c >= 0x7f || c == 0x3a {
          return Some("a field name with a character it may not have")
        }
      }
      if name
        is ("connection"
        | "proxy-connection"
        | "keep-alive"
        | "transfer-encoding"
        | "upgrade") {
        return Some("the connection-specific field \{name}")
      }
      if name == "te" && text(f.value) != "trailers" {
        return Some("a te field other than trailers")
      }
      if name == "content-length" {
        let n = digits(f.value)
        if n < 0 || (length is Some(m) && m != n) {
          return Some("a content-length that is not one number")
        }
        length = Some(n)
      }
    }
    let v = f.value
    for b in v {
      if b == b'\x00' || b == b'\n' || b == b'\r' {
        return Some("a field value with NUL, CR or LF")
      }
    }
    if v.length() > 0 &&
      (
        v[0] == b' ' ||
        v[0] == b'\t' ||
        v[v.length() - 1] == b' ' ||
        v[v.length() - 1] == b'\t'
      ) {
      return Some("a field value with surrounding whitespace")
    }
  }
  match section {
    Request | Promise => {
      if verb == "" {
        return Some("a request with no :method")
      }
      if verb == "CONNECT" && !seen.contains(":protocol") {
        if seen.contains(":scheme") || seen.contains(":path") {
          return Some("a CONNECT with :scheme or :path")
        }
        if !seen.contains(":authority") {
          return Some("a CONNECT with no :authority")
        }
      } else {
        if !seen.contains(":scheme") || !seen.contains(":path") {
          return Some("a request with no :scheme or :path")
        }
        if path == "" {
          return Some("an empty :path")
        }
        if seen.contains(":protocol") && verb != "CONNECT" {
          return Some(":protocol on a request that is not CONNECT")
        }
      }
      if section is Promise && !(verb is ("GET" | "HEAD")) {
        return Some("a promised request that is not safe and cacheable")
      }
    }
    Response =>
      if status.length() != 3 || digits(status) < 100 {
        return Some("a response with no three-digit :status")
      }
    Trailers => ()
  }
  None
}

///|
/// The value of the first field named `name`.
fn find(fields : Array[@header.Header], name : String) -> Bytes? {
  for f in fields {
    if text(f.name) == name {
      return Some(f.value)
    }
  }
  None
}

///|
/// The `:status` of a response that [`fault`] has passed.
fn status(fields : Array[@header.Header]) -> Int32 {
  find(fields, ":status").map(digits).unwrap_or(0)
}

///|
/// The declared `content-length`, once [`fault`] has passed the fields.
fn length(fields : Array[@header.Header]) -> Int32? {
  find(fields, "content-length").map(digits)
}

///|
/// Octets as a non-negative number, or `-1` when they are not one.
fn digits(v : Bytes) -> Int32 {
  if v.length() == 0 {
    return -1
  }
  let mut out = 0
  for b in v {
    let c = b.to_int()
    if c < 0x30 || c > 0x39 {
      return -1
    }
    out = out * 10 + (c - 0x30)
    if out < 0 {
      return -1
    }
  }
  out
}

///|
/// Octets as the text they spell, one code unit per octet.
fn text(b : Bytes) -> String {
  let out = StringBuilder()
  for c in b {
    out.write_char(c.to_int().unsafe_to_char())
  }
  out.to_string()
}