///|
/// Validates every outgoing field, then replaces Content-Length and
/// Transfer-Encoding with the ones `framing` implies. `NoBody` writes
/// `Content-Length: 0` when `empty_length`: where a recipient would
/// otherwise read until close, or where the method anticipates content.
fn wire_fields(
  headers : @mttp_core.Headers,
  framing : Framing,
  v : Version,
  empty_length~ : Bool,
) -> @mttp_core.Headers raise ParseError {
  // Fields replaced below are validated too: a framing override must not
  // hide a malformed value the caller supplied.
  for (name, value) in headers.iter() {
    if !token(name) ||
      !value.iter().all(c => is_field_char(c) && c.to_int() <= 0xff) {
      raise malformed("invalid outgoing field \{name}")
    }
  }
  let headers = headers.remove("content-length").remove("transfer-encoding")
  match framing {
    NoBody if empty_length => headers.add("content-length", "0")
    NoBody => headers
    Length(n) => {
      if n < 0L {
        raise malformed("negative body length")
      }
      headers.add("content-length", n.to_string())
    }
    Chunked => {
      if v == Http10 {
        raise ParseError(kind=Unsupported, detail="HTTP/1.0 cannot be chunked")
      }
      headers.add("transfer-encoding", "chunked")
    }
    UntilClose => headers.set("connection", "close")
  }
}

///|
fn version_text(v : Version) -> String {
  match v {
    Http10 => "HTTP/1.0"
    Http11 => "HTTP/1.1"
  }
}

///|
/// Writes the start line and the fields; values are written as Latin-1
/// octets, the inverse of the parser's decoding.
fn head_bytes(start : String, headers : @mttp_core.Headers) -> Bytes {
  let buffer = Buffer()
  buffer.write_bytes(@mttp_core.encode_utf8(start + "\r\n"))
  for (name, value) in headers.iter() {
    buffer.write_bytes(@mttp_core.encode_utf8(name + ": "))
    for c in value {
      buffer.write_byte(c.to_int().to_byte())
    }
    buffer.write_bytes(b"\r\n")
  }
  buffer.write_bytes(b"\r\n")
  buffer.to_bytes()
}

///|
/// Serializes a response to `meth` with the standard reason phrase,
/// replacing Content-Length and Transfer-Encoding with the ones `framing`
/// implies, so that `response_framing` reads back the same body:
///
/// - `NoBody` on a status that has a body writes `Content-Length: 0`, on
///   HTTP/1.0 too, since a response without either field is read until
///   close. A response that never has a body (to HEAD, a 1xx, 204 or 304, a
///   2xx to CONNECT) gets no framing field.
/// - `Length(n)` and `Chunked` on a response to HEAD or a 304 describe the
///   body a GET would have had (RFC 9110, sections 8.6 and 15.4.5); no body
///   follows either way.
/// - `UntilClose` adds `Connection: close`.
///
/// Refuses a status outside 100 to 599, body framing on a 1xx, 204 or 2xx to
/// CONNECT (RFC 9110, section 9.3.6), a negative length, chunked HTTP/1.0
/// and invalid fields. Values are written as Latin-1 octets, matching the
/// parser; encode text beyond Latin-1 before putting it in a field. No body
/// bytes are written.
///
/// ```mbt check
/// test {
///   let head = @h1.write_response_head(
///     200,
///     @mttp_core.Headers::of([("Content-Type", "text/plain")]),
///     Length(5),
///     Http11,
///   )
///   assert_eq(
///     head, b"HTTP/1.1 200 OK\r\ncontent-type: text/plain\r\ncontent-length: 5\r\n\r\n",
///   )
///   let empty = @h1.write_response_head(
///     200,
///     @mttp_core.Headers::empty(),
///     NoBody,
///     Http11,
///   )
///   assert_eq(empty, b"HTTP/1.1 200 OK\r\ncontent-length: 0\r\n\r\n")
///   let none = @h1.write_response_head(
///     204,
///     @mttp_core.Headers::empty(),
///     NoBody,
///     Http11,
///   )
///   assert_eq(none, b"HTTP/1.1 204 No Content\r\n\r\n")
/// }
/// ```
pub fn write_response_head(
  status : Int32,
  headers : @mttp_core.Headers,
  framing : Framing,
  v : Version,
  meth~ : @mttp_core.Method = Get,
) -> Bytes raise ParseError {
  if status < 100 || status > 599 {
    raise malformed("invalid status code")
  }
  if (
      status / 100 == 1 ||
      status == 204 ||
      (meth == Connect && status / 100 == 2)
    ) &&
    framing != NoBody {
    raise malformed("status prohibits body framing")
  }
  let headers = wire_fields(
    headers,
    framing,
    v,
    empty_length=!has_no_body(status, meth),
  )
  head_bytes(
    "\{version_text(v)} \{status} \{@mttp_core.reason_phrase(status)}",
    headers,
  )
}

///|
/// Serializes a request head, replacing Content-Length and Transfer-Encoding
/// with the ones `framing` implies, and keeping duplicate fields. `NoBody`
/// writes `Content-Length: 0` for POST, PUT and PATCH, whose semantics
/// anticipate content, and no framing field for other methods (RFC 9110,
/// section 8.6); pass `Length(0)` to send it for another method. The result
/// passes the same checks as `parse_request_head` (target form, Host), and a
/// request cannot be `UntilClose`. No body bytes are written.
///
/// ```mbt check
/// test {
///   let head = @h1.write_request_head(
///     Post,
///     "/items",
///     @mttp_core.Headers::of([("Host", "example.com")]),
///     Chunked,
///     Http11,
///   )
///   assert_eq(
///     head, b"POST /items HTTP/1.1\r\nhost: example.com\r\ntransfer-encoding: chunked\r\n\r\n",
///   )
/// }
/// ```
pub fn write_request_head(
  meth : @mttp_core.Method,
  target : String,
  headers : @mttp_core.Headers,
  framing : Framing,
  v : Version,
) -> Bytes raise ParseError {
  if framing == UntilClose {
    raise malformed("a request cannot be close-delimited")
  }
  let headers = wire_fields(
    headers,
    framing,
    v,
    empty_length=meth is (Post | Put | Patch),
  )
  let head = head_bytes("\{meth} \{target} \{version_text(v)}", headers)
  let _ = parse_request_head(head, limits={
    ..Limits::default(),
    max_head_bytes: head.length(),
    max_headers: headers.length(),
  })
  head
}

///|
/// Encodes one chunk of the chunked coding; an empty `bytes` encodes the
/// last chunk with an empty trailer section, which ends the body.
///
/// ```mbt check
/// test {
///   assert_eq(@h1.encode_chunk(b"Wiki"), b"4\r\nWiki\r\n")
///   assert_eq(@h1.encode_chunk(b""), b"0\r\n\r\n")
/// }
/// ```
pub fn encode_chunk(bytes : BytesView) -> Bytes {
  let buffer = Buffer()
  buffer.write_bytes(
    @mttp_core.encode_utf8(bytes.length().to_string(radix=16) + "\r\n"),
  )
  buffer.write_bytesview(bytes)
  buffer.write_bytes(b"\r\n")
  buffer.to_bytes()
}