///|
extern "js" fn fetch_ffi(url : String, options : @js.Value) -> @js.Promise =
  #| (url, options) => fetch(url, options)

///|
extern "js" fn fetch_response_status(resp : @js.Value) -> Int =
  #| (resp) => resp.status

///|
extern "js" fn fetch_response_bytes(resp : @js.Value) -> @js.Promise =
  #| async (resp) => {
  #|   const ab = await resp.arrayBuffer();
  #|   return Array.from(new Uint8Array(ab));
  #| }

///|
extern "js" fn fetch_response_headers_json(resp : @js.Value) -> String =
  #| (resp) => {
  #|   const out = {};
  #|   resp.headers.forEach((value, key) => {
  #|     out[key] = value;
  #|   });
  #|   return JSON.stringify(out);
  #| }

///|
fn parse_headers_json(
  json_str : String,
) -> Map[StringView, StringView] raise Error {
  let headers : Map[StringView, StringView] = {}
  match @json.parse(json_str) {
    Object(obj) => {
      obj.each(fn(k, v) { if v is String(s) { headers.set(k, s) } })
      headers
    }
    _ =>
      raise FetchError::RequestFailed("response headers is not a JSON object")
  }
}

///|
pub async fn fetch(
  url : String,
  body? : String,
  http_method : HttpMethod,
  data? : &Responder,
  headers? : Map[String, String],
  credentials? : FetchCredentials,
  mode? : FetchMode,
) -> HttpResponse raise Error {
  let (req_body, req_headers) = prepare_fetch_payload(body, data, headers)
  let options = @js.Object::new()
  options["method"] = http_method.to_string()
  if req_body is Some(body_str) {
    options["body"] = body_str
  }
  if not(req_headers.is_empty()) {
    let headers_obj = @js.Object::new()
    req_headers.each(fn(k, v) { headers_obj[k] = v })
    options["headers"] = headers_obj.to_value()
  }
  ignore(credentials)
  ignore(mode)

  let resp = fetch_ffi(url, options.to_value()).wait() catch {
    e => raise FetchError::RequestFailed("js fetch failed: \{e}")
  }
  let status = fetch_response_status(resp)
  let body_arr : Array[Byte] = fetch_response_bytes(resp).wait().cast() catch {
      e => raise FetchError::RequestFailed("read response body failed: \{e}")
    }
  let response_headers = parse_headers_json(fetch_response_headers_json(resp))

  HttpResponse::new(
    StatusCode::from_int(status),
    headers=response_headers,
    raw_body=Bytes::from_array(body_arr),
  )
}