// The XRPC error taxonomy.
//
// An unsuccessful XRPC response has a standard shape -- `{"error": "Name",
// "message": "..."}` -- where `error` is a machine-readable constant and
// `message` is for a person. Each Lexicon declares the error names its endpoint
// can return, so `error_name` is the thing to branch on and the generated
// packages carry the declared names as constants.
//
// Rate limiting is its own variant rather than `Status(429)`, because it is the
// one status a caller is expected to handle by waiting rather than by giving
// up, and burying it in a status code makes that easy to miss.
//
// As in `@slack/client`, nothing here sleeps. `should_retry` and
// `retry_after_millis` compute; the caller waits. Sleeping needs a runtime, and
// depending on one would make this package native-only.

///|
pub(all) suberror XrpcError {
  /// No response at all: DNS, connection, TLS, timeout. Carries the
  /// transport's description of where it was pointed.
  RequestFailed(String)
  /// A non-2xx response.
  Status(
    status~ : Int,
    error~ : String,
    message~ : String?,
    headers~ : Map[String, String]
  )
  /// 429, or a 4xx whose error name says the same thing.
  RateLimited(retry_after_millis~ : Int64?, error~ : String, message~ : String?)
  /// A 2xx whose body did not match the Lexicon.
  Decode(@data.DecodeError)
  /// A request that could not be built -- an argument that will not encode.
  Encode(String)
} derive(Debug)

///|
/// The machine-readable name: the `error` field of the body, or a name derived
/// from the status when the server did not send one.
pub fn XrpcError::error_name(self : Self) -> String {
  match self {
    RequestFailed(_) => "RequestFailed"
    Status(error~, ..) => error
    RateLimited(error~, ..) => error
    Decode(_) => "InvalidResponse"
    Encode(_) => "InvalidRequest"
  }
}

///|
pub fn XrpcError::status(self : Self) -> Int? {
  match self {
    Status(status~, ..) => Some(status)
    RateLimited(..) => Some(429)
    _ => None
  }
}

///|
pub fn XrpcError::message(self : Self) -> String? {
  match self {
    Status(message~, ..) => message
    RateLimited(message~, ..) => message
    _ => None
  }
}

///|
/// The access token has expired and a refresh should be attempted.
///
/// The condition is upstream's and it is not just "401": a PDS may answer 400
/// with `ExpiredToken`, and a client that only checked the status would log the
/// user out instead of refreshing.
pub fn XrpcError::is_expired_token(self : Self) -> Bool {
  match self {
    Status(status=401, ..) => true
    Status(status=400, error~, ..) => error == "ExpiredToken"
    _ => false
  }
}

///|
/// Whether the same request is worth sending again.
///
/// The status set is the reference implementation's
/// `RETRYABLE_HTTP_STATUS_CODES`. A transport failure is retryable because it
/// may never have reached the server; a 4xx other than these is not, because
/// sending it again produces the same answer.
pub fn XrpcError::should_retry(self : Self) -> Bool {
  match self {
    RequestFailed(_) => true
    RateLimited(..) => true
    Status(status~, ..) =>
      status == 408 ||
      status == 425 ||
      status == 429 ||
      status == 500 ||
      status == 502 ||
      status == 503 ||
      status == 504 ||
      status == 522 ||
      status == 524
    _ => false
  }
}

///|
/// How long to wait before retrying, if the server said.
///
/// `None` means it did not, and the caller should pick its own backoff -- the
/// reference implementation uses `min(30s, 500ms * 2^attempt)`.
pub fn XrpcError::retry_after_millis(self : Self) -> Int64? {
  match self {
    RateLimited(retry_after_millis~, ..) => retry_after_millis
    Status(headers~, ..) => retry_after_of(headers, None)
    _ => None
  }
}

///|
pub fn XrpcError::describe_error(self : Self) -> String {
  match self {
    RequestFailed(detail) => "request failed: \{detail}"
    Status(status~, error~, message~, ..) =>
      match message {
        Some(text) => "\{status} \{error}: \{text}"
        None => "\{status} \{error}"
      }
    RateLimited(retry_after_millis~, error~, message~) => {
      let detail = match message {
        Some(text) => ": \{text}"
        None => ""
      }
      match retry_after_millis {
        Some(millis) => "429 \{error} (retry after \{millis}ms)\{detail}"
        None => "429 \{error}\{detail}"
      }
    }
    Decode(e) => "invalid response: \{e.describe_error()}"
    Encode(detail) => "invalid request: \{detail}"
  }
}

///|
pub impl Show for XrpcError with fn output(self, logger) {
  logger.write_string(self.describe_error())
}

///|
/// Narrows a caught `Error` back to this taxonomy.
///
/// Needed because variant patterns are the only way to match an error value --
/// the suberror TYPE name is not a pattern -- and they are only in scope inside
/// this package. Anything from elsewhere becomes a transport failure, which is
/// the honest reading: it came from the code that does the sending.
pub fn to_xrpc_error(e : Error) -> XrpcError {
  match e {
    RequestFailed(_) as t => t
    Status(..) as t => t
    RateLimited(..) as t => t
    Decode(_) as t => t
    Encode(_) as t => t
    _ => RequestFailed(e.to_string())
  }
}

///|
/// `Retry-After` in seconds, then `ratelimit-reset` as an epoch second.
///
/// The HTTP-date form of `Retry-After` is not read: it would need a clock to
/// turn into a duration, and this package does not have one. A server that
/// sends only a date gets `None` and the caller's own backoff, which is the
/// right degradation.
fn retry_after_of(
  headers : Map[String, String],
  now_seconds : Int64?,
) -> Int64? {
  if headers.get("retry-after") is Some(value) {
    if parse_uint(value.trim().to_owned()) is Some(seconds) {
      return Some(seconds * 1000L)
    }
  }
  if headers.get("ratelimit-reset") is Some(value) && now_seconds is Some(now) {
    if parse_uint(value.trim().to_owned()) is Some(resets_at) {
      let wait = (resets_at - now) * 1000L
      if wait > 0L {
        return Some(wait)
      }
    }
  }
  None
}

///|
/// Digits only. Deliberately not a general number parser: `Retry-After` is
/// either a count of seconds or an HTTP date, and anything else is a header
/// this code should decline to interpret.
fn parse_uint(text : String) -> Int64? {
  guard text.length() > 0 && text.length() <= 18 else { return None }
  let mut acc = 0L
  for i = 0; i < text.length(); i = i + 1 {
    let digit = text[i].to_int() - '0'.to_int()
    guard digit >= 0 && digit <= 9 else { return None }
    acc = acc * 10L + digit.to_int64()
  }
  Some(acc)
}