///|
/// A stateful, asynchronously fetched sequence of REST result pages: the
/// `gaato/sdk-runtime` paginator with `DiscordHttpError` as its error.
///
/// Call `next_page`, `each`, or `collect` sequentially; all three consume the
/// same cursor state. Empty and short REST pages both mark the sequence as
/// finished, and the items a `collect` did not use come back first from the
/// next call.
pub type Paginator[T] = @runtime.Paginator[T, DiscordHttpError]

///|
fn validate_page_limit(
  resource : String,
  limit : Int?,
  maximum : Int,
) -> Unit raise DiscordHttpError {
  validate_range("\{resource} limit", limit, min=1, max=maximum)
}

///|
fn[A, B] validate_cursor_pair(
  resource : String,
  before : A?,
  after : B?,
) -> Unit raise DiscordHttpError {
  if before is Some(_) && after is Some(_) {
    raise Validation(
      message="\{resource} pagination cannot use before and after together",
    )
  }
}

///|
fn[T, C : Compare] minimum_cursor(page : Array[T], cursor_of : (T) -> C) -> C? {
  guard page.get(0) is Some(first) else { return None }
  let result = Ref(cursor_of(first))
  for item in page[1:] {
    let cursor = cursor_of(item)
    if cursor < result.val {
      result.val = cursor
    }
  }
  Some(result.val)
}

///|
fn[T, C : Compare] maximum_cursor(page : Array[T], cursor_of : (T) -> C) -> C? {
  guard page.get(0) is Some(first) else { return None }
  let result = Ref(cursor_of(first))
  for item in page[1:] {
    let cursor = cursor_of(item)
    if cursor > result.val {
      result.val = cursor
    }
  }
  Some(result.val)
}

///|
fn[T, C : Compare] maximum_present_cursor(
  page : Array[T],
  cursor_of : (T) -> C?,
) -> C? {
  let result : Ref[C?] = Ref(None)
  for item in page {
    if cursor_of(item) is Some(cursor) {
      match result.val {
        Some(current) if current >= cursor => ()
        _ => result.val = Some(cursor)
      }
    }
  }
  result.val
}

///|
/// Internal constructor shared by real endpoint paginators and fake wbtests.
/// The cursor stays inside the fetch, so any cursor type works and a fetch
/// that raised is retried in place.
fn[T, C] paginator_from_cursor(
  page_size : Int,
  initial_cursor : C?,
  fetch : async (C?, Int) -> Array[T] raise DiscordHttpError,
  next_cursor : (Array[T]) -> C?,
) -> Paginator[T] {
  let cursor = Ref(initial_cursor)
  let finished = Ref(false)
  @runtime.Paginator::new(() => {
    if finished.val {
      return None
    }
    let page = fetch(cursor.val, page_size)
    if page.is_empty() {
      finished.val = true
      return None
    }
    let following = next_cursor(page)
    if page.length() < page_size || following is None {
      finished.val = true
    } else {
      cursor.val = following
    }
    Some(page)
  })
}

///|
/// Page through channel messages. Without a cursor, pagination starts at the
/// newest messages and continues backward. `around` produces one page only.
///
/// Start before or after a known message by supplying one cursor. `before`,
/// `after`, and `around` are mutually exclusive. All paginator methods
/// (`next_page`, `collect`, `each`) consume the same mutable cursor state;
/// do not call them concurrently. Empty pages and pages shorter than the
/// configured page size mark the end.
///
/// ```mbt check
/// test "walk message history newest-first" {
///   async fn print_history(
///     client : @http.Client,
///     channel_id : @model.ChannelId,
///     newest_known : @model.MessageId,
///   ) -> Unit {
///     let pages = client.paginate_messages(channel_id, page_size=100)
///     for ;; {
///       match pages.next_page() {
///         Some(messages) =>
///           for message in messages {
///             println("\{message.id}: \{message.content}")
///           }
///         None => break
///       }
///     }
///     // `collect` bounds the walk instead of draining the full history.
///     let recent = client
///       .paginate_messages(channel_id, before=newest_known, page_size=100)
///       .collect(max=250)
///     ignore(recent)
///   }
///
///   ignore(print_history)
/// }
/// ```
pub fn Client::paginate_messages(
  self : Client,
  channel_id : @model.ChannelId,
  before? : @model.MessageId,
  after? : @model.MessageId,
  around? : @model.MessageId,
  page_size? : Int = 100,
) -> Paginator[@model.Message] raise DiscordHttpError {
  validate_page_limit("message", Some(page_size), 100)
  let cursor_count = (if around is Some(_) { 1 } else { 0 }) +
    (if before is Some(_) { 1 } else { 0 }) +
    (if after is Some(_) { 1 } else { 0 })
  if cursor_count > 1 {
    raise Validation(
      message="message pagination accepts only one of around, before, or after",
    )
  }
  match (around, after) {
    (Some(id), None) =>
      paginator_from_cursor(
        page_size,
        Some(id),
        (cursor, limit) => {
          self.get_channel_messages(channel_id, limit~, around?=cursor)
        },
        _ => None,
      )
    (None, Some(id)) =>
      paginator_from_cursor(
        page_size,
        Some(id),
        (cursor, limit) => {
          self.get_channel_messages(channel_id, limit~, after?=cursor)
        },
        page => maximum_cursor(page, message => message.id),
      )
    (None, None) =>
      paginator_from_cursor(
        page_size,
        before,
        (cursor, limit) => {
          self.get_channel_messages(channel_id, limit~, before?=cursor)
        },
        page => minimum_cursor(page, message => message.id),
      )
    (Some(_), Some(_)) => abort("validated above")
  }
}

///|
/// Page through guild members in ascending user-id order.
///
/// Use `each` when the item callback is synchronous. Available paginator
/// constructors cover messages, guild members, bans, audit-log entries,
/// scheduled-event users, poll-answer voters, and current-user guilds;
/// cursor direction and endpoint maximums are encoded by each constructor.
///
/// ```mbt check
/// test "visit every guild member" {
///   async fn print_members(
///     client : @http.Client,
///     guild_id : @model.GuildId,
///   ) -> Unit {
///     client
///     .paginate_guild_members(guild_id)
///     .each(guild_member => println("\{Repr(guild_member.user)}"))
///   }
///
///   ignore(print_members)
/// }
/// ```
pub fn Client::paginate_guild_members(
  self : Client,
  guild_id : @model.GuildId,
  after? : @model.UserId,
  page_size? : Int = 1000,
) -> Paginator[@model.GuildMember] raise DiscordHttpError {
  validate_page_limit("guild member", Some(page_size), 1000)
  paginator_from_cursor(
    page_size,
    after,
    (cursor, limit) => self.list_guild_members(guild_id, limit~, after?=cursor),
    page => {
      maximum_present_cursor(page, guild_member => {
        guild_member.user.map(user => user.id)
      })
    },
  )
}

///|
/// Page through guild bans using either a before or after cursor.
pub fn Client::paginate_guild_bans(
  self : Client,
  guild_id : @model.GuildId,
  before? : @model.UserId,
  after? : @model.UserId,
  page_size? : Int = 1000,
) -> Paginator[@model.GuildBan] raise DiscordHttpError {
  validate_page_limit("guild ban", Some(page_size), 1000)
  validate_cursor_pair("guild ban", before, after)
  match after {
    Some(id) =>
      paginator_from_cursor(
        page_size,
        Some(id),
        (cursor, limit) => self.get_guild_bans(guild_id, limit~, after?=cursor),
        page => maximum_cursor(page, ban => ban.user.id),
      )
    None =>
      paginator_from_cursor(
        page_size,
        before,
        (cursor, limit) => self.get_guild_bans(guild_id, limit~, before?=cursor),
        page => minimum_cursor(page, ban => ban.user.id),
      )
  }
}

///|
/// Page backward through audit-log entries. Referenced sidecar objects from
/// each audit-log response are intentionally not included in the item stream.
pub fn Client::paginate_guild_audit_log(
  self : Client,
  guild_id : @model.GuildId,
  user_id? : @model.UserId,
  action_type? : @model.AuditLogEvent,
  before? : @model.AuditLogEntryId,
  page_size? : Int = 100,
) -> Paginator[@model.AuditLogEntry] raise DiscordHttpError {
  validate_page_limit("audit log", Some(page_size), 100)
  paginator_from_cursor(
    page_size,
    before,
    (cursor, limit) => {
      let log = self.get_guild_audit_log(
        guild_id,
        user_id?,
        action_type?,
        before?=cursor,
        limit~,
      )
      log.audit_log_entries
    },
    page => minimum_cursor(page, entry => entry.id),
  )
}

///|
/// Page through users interested in a guild scheduled event.
pub fn Client::paginate_guild_scheduled_event_users(
  self : Client,
  guild_id : @model.GuildId,
  event_id : @model.ScheduledEventId,
  with_member? : Bool,
  before? : @model.UserId,
  after? : @model.UserId,
  page_size? : Int = 100,
) -> Paginator[@model.GuildScheduledEventUser] raise DiscordHttpError {
  validate_page_limit("scheduled event user", Some(page_size), 100)
  validate_cursor_pair("scheduled event user", before, after)
  match after {
    Some(id) =>
      paginator_from_cursor(
        page_size,
        Some(id),
        (cursor, limit) => {
          self.list_guild_scheduled_event_users(
            guild_id,
            event_id,
            limit~,
            with_member?,
            after?=cursor,
          )
        },
        page => maximum_cursor(page, event_user => event_user.user.id),
      )
    None =>
      paginator_from_cursor(
        page_size,
        before,
        (cursor, limit) => {
          self.list_guild_scheduled_event_users(
            guild_id,
            event_id,
            limit~,
            with_member?,
            before?=cursor,
          )
        },
        page => minimum_cursor(page, event_user => event_user.user.id),
      )
  }
}

///|
/// Page forward through users who selected one poll answer.
pub fn Client::paginate_poll_answer_voters(
  self : Client,
  channel_id : @model.ChannelId,
  message_id : @model.MessageId,
  answer_id : Int,
  after? : @model.UserId,
  page_size? : Int = 100,
) -> Paginator[@model.User] raise DiscordHttpError {
  validate_page_limit("poll voter", Some(page_size), 100)
  paginator_from_cursor(
    page_size,
    after,
    (cursor, limit) => {
      let response = self.get_poll_answer_voters(
        channel_id,
        message_id,
        answer_id,
        after?=cursor,
        limit~,
      )
      response.users
    },
    page => maximum_cursor(page, user => user.id),
  )
}

///|
/// Page through the current user's partial guild objects.
pub fn Client::paginate_current_user_guilds(
  self : Client,
  before? : @model.GuildId,
  after? : @model.GuildId,
  shard? : Int,
  with_counts? : Bool,
  page_size? : Int = 200,
) -> Paginator[@model.CurrentUserGuild] raise DiscordHttpError {
  validate_page_limit("current user guild", Some(page_size), 200)
  validate_cursor_pair("current user guild", before, after)
  match after {
    Some(id) =>
      paginator_from_cursor(
        page_size,
        Some(id),
        (cursor, limit) => {
          self.get_current_user_guilds(
            limit~,
            after?=cursor,
            shard?,
            with_counts?,
          )
        },
        page => maximum_cursor(page, guild => guild.id),
      )
    None =>
      paginator_from_cursor(
        page_size,
        before,
        (cursor, limit) => {
          self.get_current_user_guilds(
            limit~,
            before?=cursor,
            shard?,
            with_counts?,
          )
        },
        page => minimum_cursor(page, guild => guild.id),
      )
  }
}