// The seam between this library and whatever sends bytes.
//
// Taken from `slack/api`'s `Transport` unchanged, because the shape is right:
// one async method that sends, one sync method that says where it points. No
// retries, no rate limiting, no JSON. Those are decided above, where they can
// be tested without a socket.

///|
/// One outbound call, already reduced to bytes.
pub(all) struct HttpRequest {
  url : String
  /// Spelled `http_method` because `method` is a reserved word.
  http_method : String
  headers : Map[String, String]
  body : Bytes
} derive(Eq, Debug)

///|
/// The UTF-8 byte count, which is what `Content-Length` must be -- and is not
/// the string length, because MoonBit strings are UTF-16.
pub fn HttpRequest::content_length(self : Self) -> Int {
  self.body.length()
}

///|
/// The body as text, for logging and tests. Lossy by design: a blob upload is
/// not text and printing it should not fail.
pub fn HttpRequest::body_text(self : Self) -> String {
  @utf8.decode_lossy(self.body[:])
}

///|
pub(all) struct HttpResponse {
  status : Int
  /// Header names MUST be lower-cased by the transport. Everything that reads
  /// one -- `retry-after`, `ratelimit-reset`, `content-type` -- assumes it.
  headers : Map[String, String]
  body : Bytes
} derive(Eq, Debug)

///|
pub fn HttpResponse::header(self : Self, name : String) -> String? {
  self.headers.get(name)
}

///|
/// The body as text. Lossy, because a malformed body must still be reportable
/// rather than turning into a second, less useful error.
pub fn HttpResponse::body_text(self : Self) -> String {
  @utf8.decode_lossy(self.body[:])
}

///|
/// The media type, without parameters -- `application/json` from
/// `application/json; charset=utf-8`.
pub fn HttpResponse::content_type(self : Self) -> String? {
  guard self.header("content-type") is Some(value) else { return None }
  let media = match value.split_once(";") {
    Some((head, _)) => head.to_owned()
    None => value
  }
  Some(media.trim().to_owned().to_lower())
}

///|
/// A thing that can send an XRPC request and return what came back.
///
/// Deliberately dumb. An implementation has exactly three obligations, and all
/// three are things a hand-written one gets wrong:
///
///   - Lower-case the response header names.
///   - Do not forward `content-length`; most HTTP clients compute their own,
///     and sending both risks two conflicting headers on the wire.
///   - Do not let the underlying client's errors escape. Translate them, or a
///     caller's `catch` sees a type from a library it never imported.
pub(open) trait Transport {
  async fn send(Self, HttpRequest) -> HttpResponse
  /// Human-readable target, for error messages. A transport that cannot say
  /// where it was pointed makes a connection failure much harder to place.
  fn describe(Self) -> String
}