// Building a request and reading a response.
//
// The XRPC wire format, in full:
//
//   path      /xrpc/, always
//   method    query -> GET, procedure -> POST
//   params    query string, on BOTH kinds
//   input     the request body, for procedures only
//   output    the response body
//
// That is the whole protocol. What makes a client non-trivial is not the shape
// but the edges: which encoding a body has, what a non-2xx means, and when a
// 400 is really an expired token.

///|
/// A query reads and a procedure writes, and that is the only thing that
/// decides the HTTP method.
pub(all) enum Method {
  Query
  Procedure
} derive(Eq, Debug)

///|
pub fn Method::http_method(self : Self) -> String {
  match self {
    Query => "GET"
    Procedure => "POST"
  }
}

///|
/// What goes in the request body.
pub(all) enum Body {
  /// No body. Every query, and the procedures that take no input.
  Empty
  /// `application/json`, encoded through the lex codec so `$link` and `$bytes`
  /// come out right.
  Json(@data.LexValue)
  /// Raw bytes with a declared media type: `uploadBlob`, and the video
  /// endpoints. The mime type is the caller's, not guessed from the content.
  Blob(bytes~ : Bytes, mime_type~ : String)
} derive(Eq, Debug)

///|
/// How the caller proves who it is.
pub(all) enum Credential {
  Anonymous
  /// `Authorization: Bearer `. Both the access and the refresh token are
  /// sent this way -- `refreshSession` is the one call that uses the latter.
  Bearer(String)
} derive(Eq, Debug)

///|
pub const USER_AGENT : String = "marianoguerra-atproto-mb"

///|
/// Assembles a request. Pure, and the reason almost all of this package can be
/// tested without a transport.
///
/// `service` is the origin to send to -- `https://bsky.social`, or the PDS from
/// the account's DID document. A trailing slash on it is tolerated, because
/// callers paste these from configuration and one showing up should not produce
/// `//xrpc/...`.
pub fn build_request(
  service : String,
  nsid : @syntax.Nsid,
  // Spelled `kind` because `method` is a reserved word.
  kind : Method,
  params? : Params = Params::new(),
  body? : Body = Empty,
  credential? : Credential = Anonymous,
  accept? : String,
  extra_headers? : Map[String, String] = Map([]),
) -> HttpRequest {
  let origin = if service.has_suffix("/") {
    service[:service.length() - 1].to_owned()
  } else {
    service
  }
  let query = params.encode()
  let url = if query == "" {
    "\{origin}/xrpc/\{nsid}"
  } else {
    "\{origin}/xrpc/\{nsid}?\{query}"
  }
  let headers : Map[String, String] = Map([])
  headers["user-agent"] = USER_AGENT
  if accept is Some(value) {
    headers["accept"] = value
  }
  let bytes = match body {
    Empty => b""
    Json(value) => {
      headers["content-type"] = "application/json"
      @utf8.encode(value.stringify())
    }
    Blob(bytes~, mime_type~) => {
      headers["content-type"] = mime_type
      bytes
    }
  }
  // Set even though most transports recompute it, so a caller inspecting a
  // request can see what it would send. `@xrpc.Transport` implementations are
  // told to drop it rather than forward it.
  if bytes.length() > 0 {
    headers["content-length"] = bytes.length().to_string()
  }
  if credential is Bearer(token) {
    headers["authorization"] = "Bearer \{token}"
  }
  for name, value in extra_headers {
    headers[name.to_lower()] = value
  }
  { url, http_method: kind.http_method(), headers, body: bytes }
}

///|
/// Turns a response into the decoded body, or into the right error.
///
/// The order matters. A 429 is a rate limit whatever its body says; a non-2xx
/// is an error even if the body happens to parse; and only then is a 2xx body
/// worth decoding.
pub fn interpret(response : HttpResponse) -> @data.LexValue raise XrpcError {
  if response.status == 429 {
    let (error, message) = error_body_of(response)
    raise RateLimited(
      retry_after_millis=retry_after_of(response.headers, None),
      error=if error == "" { "RateLimitExceeded" } else { error },
      message~,
    )
  }
  if response.status < 200 || response.status >= 300 {
    let (error, message) = error_body_of(response)
    raise Status(
      status=response.status,
      error=if error == "" { status_error_name(response.status) } else { error },
      message~,
      headers=response.headers,
    )
  }
  // A 2xx with an empty body is a successful call that returns nothing --
  // `deleteSession`, `updateHandle`. Null is the honest value for it.
  if response.body.length() == 0 {
    return Null
  }
  @data.LexValue::parse(response.body_text()) catch {
    e => raise Decode(e)
  }
}

///|
/// A 2xx whose body is not JSON: `getBlob`, `getRepo`, the video endpoints.
/// Returns the bytes and lets the caller decide what they are.
pub fn interpret_bytes(response : HttpResponse) -> Bytes raise XrpcError {
  if response.status == 429 {
    let (error, message) = error_body_of(response)
    raise RateLimited(
      retry_after_millis=retry_after_of(response.headers, None),
      error=if error == "" { "RateLimitExceeded" } else { error },
      message~,
    )
  }
  if response.status < 200 || response.status >= 300 {
    let (error, message) = error_body_of(response)
    raise Status(
      status=response.status,
      error=if error == "" { status_error_name(response.status) } else { error },
      message~,
      headers=response.headers,
    )
  }
  response.body
}

///|
/// Reads `{"error": ..., "message": ...}` out of an error response.
///
/// Never raises. A server that is failing may also be returning HTML from a
/// proxy, and turning "502 with an nginx page" into a JSON parse error would
/// hide the status -- which is the only useful thing in it.
fn error_body_of(response : HttpResponse) -> (String, String?) {
  if response.body.length() == 0 {
    return ("", None)
  }
  let parsed = decode_or_none(response.body_text())
  guard parsed is Some(value) else { return ("", None) }
  let error = match value.get("error") {
    Some(Str(name)) => name
    _ => ""
  }
  let message = match value.get("message") {
    Some(Str(text)) => Some(text)
    _ => None
  }
  (error, message)
}

///|
fn decode_or_none(text : String) -> @data.LexValue? {
  Some(@data.LexValue::parse(text)) catch {
    _ => None
  }
}

///|
/// The name to use when the server sent a status but no `error` field.
///
/// The table is the reference implementation's `ResponseType`. Anything
/// unlisted collapses to `InvalidRequest` below 500 and `InternalServerError`
/// at or above, which is what upstream's fallback does.
pub fn status_error_name(status : Int) -> String {
  match status {
    400 => "InvalidRequest"
    401 => "AuthenticationRequired"
    403 => "Forbidden"
    404 => "XRPCNotSupported"
    406 => "NotAcceptable"
    413 => "PayloadTooLarge"
    415 => "UnsupportedMediaType"
    429 => "RateLimitExceeded"
    500 => "InternalServerError"
    501 => "MethodNotImplemented"
    502 => "UpstreamFailure"
    503 => "NotEnoughResources"
    504 => "UpstreamTimeout"
    _ => if status >= 500 { "InternalServerError" } else { "InvalidRequest" }
  }
}