///|
pub fn ApiCompatibility::media_type(self : ApiCompatibility) -> String {
  match self {
    Default => "application/json"
    CompatibleWith8 => "application/vnd.elasticsearch+json; compatible-with=8"
  }
}

///|
fn Auth::authorization_header(self : Auth) -> String? {
  match self {
    NoAuth => None
    ApiKey(value) => Some("ApiKey \{value}")
    BearerToken(token) => Some("Bearer \{token}")
    Basic(username, password) => {
      let raw = "\{username}:\{password}"
      Some("Basic \{@base64.encode(@utf8.encode(raw))}")
    }
  }
}

///|
fn HttpMethod::to_http_method(self : HttpMethod) -> @http.RequestMethod {
  match self {
    Get => Get
    Head => Head
    Post => Post
    Put => Put
    Delete => Delete
    Patch => Patch
  }
}

///|
pub fn RequestBody::content_type(self : RequestBody) -> String? {
  match self {
    NoBody => None
    JsonBody(_) => Some("application/json")
    TextBody(_) => Some("text/plain")
    BinaryBody(_) => Some("application/octet-stream")
    Ndjson(_) => Some("application/x-ndjson")
  }
}

///|
fn ClientConfig::request_headers(
  self : ClientConfig,
  request : EsRequest,
) -> Map[String, String] {
  let headers = self.default_headers.copy()
  let media_type = self.compatibility.media_type()
  headers["Accept"] = media_type
  match request.body.content_type() {
    Some(content_type) => headers["Content-Type"] = content_type
    None => ()
  }
  match self.auth.authorization_header() {
    Some(value) => headers["Authorization"] = value
    None => ()
  }
  for key, value in request.headers {
    headers[key] = value
  }
  headers
}

///|
async fn write_request_body(client : @http.Client, body : RequestBody) -> Unit {
  match body {
    NoBody => ()
    JsonBody(json) => client.write(json.stringify())
    TextBody(text) => client.write(text)
    BinaryBody(bytes) => client.write(bytes)
    Ndjson(items) => client.write(encode_ndjson(items))
  }
}

///|
fn parse_response_json(data : &@io.Data) -> Json raise EsError {
  let text = data.text() catch { err => raise Decode(err.to_string()) }
  if text.is_empty() {
    Json::null()
  } else {
    @json.parse(text) catch {
      _ => Json::string(text)
    }
  }
}

///|
pub async fn Client::new(config : ClientConfig) -> Client {
  let inner = @http.Client::Client(config.base_url, headers=Map([]))
  { config, inner: Some(inner) }
}

///|
pub async fn new(
  base_url : String,
  auth? : Auth = NoAuth,
  compatibility? : ApiCompatibility = Default,
  headers? : Map[String, String] = Map([]),
) -> Client {
  Client::new(ClientConfig::new(base_url, auth~, compatibility~, headers~))
}

///|
pub fn Client::config(self : Client) -> ClientConfig {
  self.config
}

///|
pub fn Client::is_closed(self : Client) -> Bool {
  self.inner is None
}

///|
pub fn Client::close(self : Client) -> Unit {
  match self.inner {
    Some(inner) => {
      inner.close()
      self.inner = None
    }
    None => ()
  }
}

///|
pub async fn Client::perform_json(
  self : Client,
  request : EsRequest,
) -> EsResponse[Json] {
  let inner = match self.inner {
    Some(inner) => inner
    None => raise ClosedClient
  }
  let target = append_query(request.path, request.query)
  let headers = self.config.request_headers(request)
  inner.request(request.meth.to_http_method(), target, extra_headers=headers)
  write_request_body(inner, request.body)
  let response = inner.end_request()
  let body = parse_response_json(inner.read_all())
  if response.code >= 200 && response.code < 300 {
    { status: response.code, headers: response.headers, body }
  } else {
    raise Http(status=response.code, reason=response.reason, body~)
  }
}

///|
pub async fn[T : FromJson] Client::perform(
  self : Client,
  request : EsRequest,
) -> EsResponse[T] {
  let response = self.perform_json(request)
  let body : T = @json.from_json(response.body) catch {
    err => raise Decode(err.to_string())
  }
  { status: response.status, headers: response.headers, body }
}