///|
fn request_content_length(request : @http.Request) -> Int? {
  match
    @httputil.get_header_case_insensitive(request.headers, "content-length") {
    Some(value) => Some(@string.parse_int(value) catch { _ => return None })
    None => None
  }
}

///|
priv enum RequestBodyReadOutcome {
  Body(Bytes)
  RequestRejected
}

///|
fn next_native_request_body_chunk_size(limit : Int, total : Int) -> Int {
  let remaining = limit - total
  if remaining >= 1024 {
    1024
  } else {
    remaining + 1
  }
}

///|
test "next_native_request_body_chunk_size returns 1024 when remaining >= 1024" {
  debug_inspect(next_native_request_body_chunk_size(2048, 0), content="1024")
  debug_inspect(next_native_request_body_chunk_size(2048, 1024), content="1024")
  debug_inspect(next_native_request_body_chunk_size(4096, 2000), content="1024")
}

///|
test "next_native_request_body_chunk_size returns remaining + 1 when remaining < 1024" {
  debug_inspect(next_native_request_body_chunk_size(1000, 0), content="1001")
  debug_inspect(next_native_request_body_chunk_size(2048, 1500), content="549")
  debug_inspect(next_native_request_body_chunk_size(100, 99), content="2")
  debug_inspect(next_native_request_body_chunk_size(100, 100), content="1")
}

///|
test "native request body limit helpers handle int max boundaries" {
  let max_int = 2_147_483_647
  debug_inspect(next_native_request_body_chunk_size(max_int, 0), content="1024")
  debug_inspect(
    native_request_body_chunk_exceeds_limit(max_int, max_int, 1),
    content="true",
  )
  debug_inspect(
    native_request_body_chunk_exceeds_limit(max_int, max_int - 1, 1),
    content="false",
  )
}

///|
fn native_request_body_chunk_exceeds_limit(
  limit : Int,
  total : Int,
  chunk_length : Int,
) -> Bool {
  chunk_length > limit - total
}

///|
test "native_request_body_chunk_exceeds_limit returns true when chunk exceeds remaining" {
  debug_inspect(
    native_request_body_chunk_exceeds_limit(100, 90, 11),
    content="true",
  )
  debug_inspect(
    native_request_body_chunk_exceeds_limit(100, 100, 1),
    content="true",
  )
}

///|
test "native_request_body_chunk_exceeds_limit returns false when chunk fits" {
  debug_inspect(
    native_request_body_chunk_exceeds_limit(100, 90, 10),
    content="false",
  )
  debug_inspect(
    native_request_body_chunk_exceeds_limit(100, 0, 100),
    content="false",
  )
  debug_inspect(
    native_request_body_chunk_exceeds_limit(100, 50, 1),
    content="false",
  )
}

///|
async fn read_request_body_async(
  request : @http.Request,
  body_reader : &@io.Reader,
  conn : @http.ServerConnection,
  max_request_body_bytes : Int?,
) -> RequestBodyReadOutcome {
  match max_request_body_bytes {
    None => Body(body_reader.read_all().binary())
    Some(limit) => {
      match request_content_length(request) {
        Some(content_length) if content_length > limit => {
          send_request_entity_too_large_async(request, conn)
          return RequestRejected
        }
        _ => ()
      }
      let buffer = Buffer()
      let mut total = 0
      for ;; {
        let next_chunk_size = next_native_request_body_chunk_size(limit, total)
        guard body_reader.read_some(max_len=next_chunk_size) is Some(chunk) else {
          return Body(buffer.contents())
        }
        if native_request_body_chunk_exceeds_limit(limit, total, chunk.length()) {
          send_request_entity_too_large_async(request, conn)
          return RequestRejected
        }
        total += chunk.length()
        buffer.write_bytes(chunk)
      }
    }
  }
}

///|
async fn read_request_body_with_policy_async(
  request : @http.Request,
  body_reader : &@io.Reader,
  conn : @http.ServerConnection,
  max_request_body_bytes : Int?,
  request_body_read_timeout_ms : Int?,
) -> RequestBodyReadOutcome {
  match request_body_read_timeout_ms {
    Some(request_body_read_timeout_ms) =>
      match
        @async.with_timeout_opt(request_body_read_timeout_ms, () => {
          read_request_body_async(
            request, body_reader, conn, max_request_body_bytes,
          )
        }) {
        Some(outcome) => outcome
        None => {
          send_request_timeout_async(request, conn)
          RequestRejected
        }
      }
    None =>
      read_request_body_async(
        request, body_reader, conn, max_request_body_bytes,
      )
  }
}