// The answers that are shaped by their method rather than by an entity.
//
// @model holds what Slack sends in many places -- a `User` is a `User` on six
// methods. What is here is the opposite: a handful of methods answer a bag of
// fields nothing else uses, and giving each one a name is better than handing
// back a `Json` or inventing an entity that occurs once.
//
// They are `pub(all)` so a test can build one, and they carry no `extra`: a
// method-specific answer has no forward-compatibility story to tell, and
// `ApiResponse.raw` is right there through `Api::client`.

///|
/// Who a token is: `auth.test`, which is the first call most apps make.
///
/// The five required fields are the ones Slack answers unconditionally for any
/// valid token. `bot_id` is present for a bot token and absent for a user one,
/// which is the cheapest way to tell them apart.
pub(all) struct Identity {
  url : String
  team : String
  user : String
  team_id : String
  user_id : String
  bot_id : String?
  is_enterprise_install : Bool?
  enterprise_id : String?
  app_id : String?
  app_name : String?
  expires_in : Int?
} derive(Eq, @debug.Debug)

///|
/// Whether this token belongs to a bot.
pub fn Identity::is_bot(self : Identity) -> Bool {
  self.bot_id is Some(_)
}

///|
/// A message queued by `chat.scheduleMessage`.
///
/// `scheduled_message_id` is what `chat.deleteScheduledMessage` cancels it
/// with, and the only handle you get -- there is no `ts` until it is sent.
pub(all) struct Scheduled {
  scheduled_message_id : String
  channel : String
  /// Epoch seconds, as an `Int64` so this still works after 2038.
  post_at : Int64
  message : @model.Message?
} derive(Eq, @debug.Debug)

///|
/// A user's presence: `users.getPresence`.
///
/// Everything but `presence` needs the `users:read` scope on your own user, so
/// the rest is optional and usually absent for anyone else.
pub(all) struct Presence {
  /// `"active"` or `"away"`. The only field Slack always answers.
  presence : String
  online : Bool?
  auto_away : Bool?
  manual_away : Bool?
  connection_count : Int?
  /// Epoch seconds.
  last_activity : Int64?
} derive(Eq, @debug.Debug)

///|
/// Where to PUT a file's bytes: `files.getUploadURLExternal`.
///
/// The upload itself is an ordinary HTTP request to `upload_url` and not a Web
/// API call, so it is not something this library does -- see "Not included" in
/// the library's README. `file_id` is what `files.completeUploadExternal`
/// finishes with.
pub(all) struct Upload {
  upload_url : String
  file_id : String
} derive(Eq, @debug.Debug)

///|
/// `auth.test`, as who you are.
fn identity_of(
  response : @api.ApiResponse,
  api_method : String,
) -> Identity raise TypedError {
  {
    url: entity(response, api_method, "url", id_of),
    team: entity(response, api_method, "team", id_of),
    user: entity(response, api_method, "user", id_of),
    team_id: entity(response, api_method, "team_id", id_of),
    user_id: entity(response, api_method, "user_id", id_of),
    bot_id: response.get_str("bot_id"),
    is_enterprise_install: bool_of(response, "is_enterprise_install"),
    enterprise_id: response.get_str("enterprise_id"),
    app_id: response.get_str("app_id"),
    app_name: response.get_str("app_name"),
    expires_in: int_of(response, "expires_in"),
  }
}

///|
/// An optional boolean field of a response.
fn bool_of(response : @api.ApiResponse, key : String) -> Bool? {
  match response.get(key) {
    Some(True) => Some(true)
    Some(False) => Some(false)
    _ => None
  }
}

///|
/// An optional whole-number field, at `Int` width.
fn int_of(response : @api.ApiResponse, key : String) -> Int? {
  match response.get(key) {
    Some(Number(n, ..)) => {
      let i = n.to_int()
      if i.to_double() == n {
        Some(i)
      } else {
        None
      }
    }
    _ => None
  }
}

///|
/// The same at `Int64` width, for the epoch-second fields.
fn int64_of(response : @api.ApiResponse, key : String) -> Int64? {
  match response.get(key) {
    Some(Number(n, ..)) => {
      let i = n.to_int64()
      if i.to_double() == n {
        Some(i)
      } else {
        None
      }
    }
    _ => None
  }
}

///|
/// A required `Int64`, for the one place a schedule time is not optional.
fn int64_at(
  response : @api.ApiResponse,
  api_method : String,
  key : String,
) -> Int64 raise TypedError {
  guard int64_of(response, key) is Some(value) else {
    raise ResponseShapeError(api_method~, key~)
  }
  value
}