///|
extern "js" fn ffi_fetch_text(
  meth : String,
  url : String,
  headers_json : String,
  body : String?,
  timeout_ms : Int,
) -> @js_async.Promise[FetchResponse] =
  #| async (meth, url, headersJson, body, timeoutMs) => {
  #|   const headers = JSON.parse(headersJson);
  #|   const controller = new AbortController();
  #|   const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
  #|   try {
  #|     const response = await fetch(url, {
  #|       method: meth,
  #|       headers,
  #|       body: body ?? undefined,
  #|       signal: controller.signal,
  #|     });
  #|     const text = await response.text();
  #|     return {
  #|       ok: response.ok,
  #|       status: response.status,
  #|       content_type: response.headers.get("content-type") ?? undefined,
  #|       body: text,
  #|     };
  #|   } finally {
  #|     clearTimeout(timeoutId);
  #|   }
  #| }

///|
pub struct RestOptions {
  api : String
  headers : Map[String, String]
  timeout_ms : Int
}

///|
pub fn default_rest_headers() -> Map[String, String] {
  { "User-Agent": default_user_agent }
}

///|
pub fn default_rest_options() -> RestOptions {
  {
    api: api_base,
    headers: default_rest_headers(),
    timeout_ms: default_timeout_ms,
  }
}

///|
pub fn rest_options(
  api? : String = api_base,
  headers? : Map[String, String] = default_rest_headers(),
  timeout_ms? : Int = default_timeout_ms,
) -> RestOptions {
  { api, headers, timeout_ms }
}

///|
pub struct RestClient {
  options : RestOptions
  token : String?
}

///|
pub fn RestClient::new(
  options? : RestOptions = default_rest_options(),
) -> RestClient {
  { options, token: None }
}

///|
pub fn RestClient::with_token(self : RestClient, token : String) -> RestClient {
  { options: self.options, token: Some(token) }
}

///|
pub async fn RestClient::get(
  self : RestClient,
  route : String,
  options? : RequestOptions = request_options(),
) -> Result[Json, RestError] {
  self.request(MethodGet, route, options)
}

///|
pub async fn RestClient::post(
  self : RestClient,
  route : String,
  options? : RequestOptions = request_options(),
) -> Result[Json, RestError] {
  self.request(MethodPost, route, options)
}

///|
pub async fn RestClient::put(
  self : RestClient,
  route : String,
  options? : RequestOptions = request_options(),
) -> Result[Json, RestError] {
  self.request(MethodPut, route, options)
}

///|
pub async fn RestClient::patch(
  self : RestClient,
  route : String,
  options? : RequestOptions = request_options(),
) -> Result[Json, RestError] {
  self.request(MethodPatch, route, options)
}

///|
pub async fn RestClient::delete(
  self : RestClient,
  route : String,
  options? : RequestOptions = request_options(),
) -> Result[Json, RestError] {
  self.request(MethodDelete, route, options)
}

///|
pub fn RestClient::prepare_request(
  self : RestClient,
  meth : RequestMethod,
  route : String,
  options : RequestOptions,
) -> PreparedRequest {
  let query = encode_query(options.query)
  let url = if query == "" {
    "\{self.options.api}\{route}"
  } else {
    let separator = if route.contains("?") { "&" } else { "?" }
    "\{self.options.api}\{route}\{separator}\{query}"
  }
  let headers = self.options.headers.copy()
  for key, value in options.headers {
    headers.set(key, value)
  }
  match options.auth_mode {
    UseClientToken =>
      match self.token {
        Some(token) => headers.set("Authorization", "Bot \{token}")
        None => ()
      }
    UseToken(token) => headers.set("Authorization", "Bot \{token}")
    SkipAuth => ()
  }
  let body = match options.body {
    Some(json) => {
      headers.set("Content-Type", "application/json")
      Some(json.stringify())
    }
    None => None
  }
  { meth, url, headers, body }
}

///|
pub async fn RestClient::request(
  self : RestClient,
  meth : RequestMethod,
  route : String,
  options : RequestOptions,
) -> Result[Json, RestError] {
  let prepared = self.prepare_request(meth, route, options)
  let headers_json = headers_to_json_string(prepared.headers)
  let fetch_result = try? ffi_fetch_text(
    request_method_to_string(prepared.meth),
    prepared.url,
    headers_json,
    prepared.body,
    self.options.timeout_ms,
  ).wait()
  match fetch_result {
    Err(err) => Err(Network(err.to_string()))
    Ok(response) => decode_fetch_response(response)
  }
}

///|
fn request_method_to_string(meth : RequestMethod) -> String {
  match meth {
    MethodGet => "GET"
    MethodPost => "POST"
    MethodPut => "PUT"
    MethodPatch => "PATCH"
    MethodDelete => "DELETE"
  }
}

///|
fn headers_to_json_string(headers : Map[String, String]) -> String {
  let values = headers.map((_, value) => Json::string(value))
  Json::object(values).stringify()
}

///|
fn parse_error_message(body : String) -> String {
  if body == "" {
    return "Unknown error"
  }
  let parsed = try? @json.parse(body)
  match parsed {
    Ok(Object(obj)) =>
      match obj.get("message") {
        Some(String(message)) => message
        _ => body
      }
    _ => body
  }
}

///|
fn decode_success_response(response : FetchResponse) -> Result[Json, RestError] {
  if response.body == "" {
    return Ok(Json::null())
  }
  let is_json = match response.content_type {
    Some(content_type) => content_type.contains("application/json")
    None => false
  }
  if !(is_json) {
    return Ok(Json::string(response.body))
  }
  let parsed = try? @json.parse(response.body)
  match parsed {
    Ok(json) => Ok(json)
    Err(err) => Err(DecodeError(err.to_string()))
  }
}

///|
fn decode_fetch_response(response : FetchResponse) -> Result[Json, RestError] {
  if response.ok {
    decode_success_response(response)
  } else {
    Err(ApiError(response.status, parse_error_message(response.body)))
  }
}