///|
/// Sends a generated request and decodes the successful response.
///
/// The two arguments are the matched halves of one generated operation, so a
/// call reads as a single line:
///
///     let repo = gh.call(
///       @gen.repos_get_request("gaato", "mbt-sdk"),
///       @gen.repos_get_decode,
///     )
///
/// Failures arrive as `@runtime.SdkError`; `api_error` reads GitHub's error
/// body out of one.
pub async fn[T] GitHub::call(
  self : GitHub,
  request : @http.Request,
  decode : (@http.Response) -> T raise @runtime.SdkError,
) -> T raise @runtime.SdkError {
  decode(self.send(request))
}

///|
/// Sends a generated request and returns the successful response undecoded.
///
/// This is for the operations whose body a decoder cannot describe: `204` and
/// `304` replies, and the representations requested through a non-JSON `accept`
/// such as `application/vnd.github.diff`.
pub async fn GitHub::send(
  self : GitHub,
  request : @http.Request,
) -> @http.Response raise @runtime.SdkError {
  self.client.send(
    request,
    bucket=rate_limit_bucket(request.http_method, request.url),
  )
}

///|
/// The rate limit bucket a request belongs to, by method and URL.
///
/// GitHub meters search separately from the rest of the REST API, and the two
/// windows are far apart (30 requests per minute against 5,000 per hour), so
/// they must not share a limiter bucket. GraphQL is a third window, budgeted in
/// points rather than requests: the vendored specification lists `core`,
/// `graphql`, `search` and the rest as separate resources of
/// `/rate_limit`, and a GraphQL response names its own with
/// `x-ratelimit-resource: graphql`.
///
/// Writes get buckets of their own so they can be paced (`github_pace`):
/// any method but `GET` and `HEAD` is `"core:write"`, or
/// `"core:notifications"` on a path that sends notifications
/// (`triggers_notification`). Their state is still the `core` window, because
/// the limiter follows `x-ratelimit-resource`. `GitHub::graphql` names its
/// own bucket (`"graphql"` or `"graphql:mutation"`); a request sent to the
/// endpoint through `send` is `"graphql"`.
///
/// The URL is either the relative path a generated operation produced or the
/// absolute `Link` target of a following page, so the scheme and authority are
/// stripped first, along with the `/api/v3` mount a GitHub Enterprise Server
/// deployment puts in front of every path. GraphQL is not under that mount —
/// Enterprise Server serves it from `/api/graphql` — so both spellings appear
/// here.
fn rate_limit_bucket(http_method : String, url : String) -> String {
  let path = request_path(url)
  let read = http_method.to_upper() is ("GET" | "HEAD")
  if path == "/graphql" || path == "/api/graphql" {
    graphql_bucket
  } else if read {
    if path.has_prefix("/search/") {
      "search"
    } else {
      "core"
    }
  } else if triggers_notification(path) {
    "core:notifications"
  } else {
    "core:write"
  }
}

///|
/// How far apart the requests of a bucket start, after octokit's
/// `plugin-throttling` and docs.github.com's "wait at least one second between
/// each request" for mutative requests: writes and every GraphQL request
/// (which may mutate) at least 1 s apart, writes that send notifications 3 s,
/// and searches 2 s. All writes and GraphQL share the `"write"` group, so a
/// notification-sending write also holds the next plain write for 3 s, where
/// octokit keeps a separate 3 s queue for them; the stricter spacing meets
/// both of its limits. Reads are not paced.
fn github_pace(bucket : String) -> @runtime.Pace {
  match bucket {
    "core:write" | "graphql" | "graphql:mutation" =>
      { group: "write", min_interval_ms: 1000, }
    "core:notifications" => { group: "write", min_interval_ms: 3000, }
    "search" => { group: "search", min_interval_ms: 2000, }
    _ => { group: bucket, min_interval_ms: 0, }
  }
}

///|
/// The REST paths whose writes send notifications, from octokit's
/// `plugin-throttling` (`src/generated/triggers-notification-paths.ts` at
/// eb4215edcd97f20ade800b18d964bf798e0d70b7), split into segments.
let notification_paths : Array[Array[String]] = [
  "/orgs/{org}/invitations", "/orgs/{org}/invitations/{invitation_id}", "/orgs/{org}/teams/{team_slug}/discussions",
  "/orgs/{org}/teams/{team_slug}/discussions/{discussion_number}/comments", "/repos/{owner}/{repo}/collaborators/{username}",
  "/repos/{owner}/{repo}/commits/{commit_sha}/comments", "/repos/{owner}/{repo}/issues",
  "/repos/{owner}/{repo}/issues/{issue_number}/comments", "/repos/{owner}/{repo}/issues/{issue_number}/sub_issue",
  "/repos/{owner}/{repo}/issues/{issue_number}/sub_issues/priority", "/repos/{owner}/{repo}/pulls",
  "/repos/{owner}/{repo}/pulls/{pull_number}/comments", "/repos/{owner}/{repo}/pulls/{pull_number}/comments/{comment_id}/replies",
  "/repos/{owner}/{repo}/pulls/{pull_number}/merge", "/repos/{owner}/{repo}/pulls/{pull_number}/requested_reviewers",
  "/repos/{owner}/{repo}/pulls/{pull_number}/reviews", "/repos/{owner}/{repo}/releases",
  "/teams/{team_id}/discussions", "/teams/{team_id}/discussions/{discussion_number}/comments",
].map(path_segments)

///|
/// Whether a write to `path` sends notifications. A `{parameter}` matches one
/// non-empty segment and a literal segment matches regardless of case.
/// Octokit's own matcher is a looser regular expression (a parameter may span
/// slashes, and anything without a slash may follow the last segment); since
/// no owner, repository or number contains a slash, matching whole segments
/// gives the same answer for every real path without its accidental matches.
fn triggers_notification(path : String) -> Bool {
  let segments = path_segments(path)
  notification_paths
  .iter()
  .any(pattern => {
    pattern.length() == segments.length() &&
    pattern
    .iter()
    .zip(segments.iter())
    .all(pair => {
      let (expected, actual) = pair
      if expected.has_prefix("{") {
        !actual.is_empty()
      } else {
        expected.to_lower() == actual.to_lower()
      }
    })
  })
}

///|
/// The segments of a path, without the empty one before its leading slash.
fn path_segments(path : String) -> Array[String] {
  let segments = path.split("/").map(segment => segment.to_owned()).collect()
  if segments.length() > 0 && segments[0].is_empty() {
    segments.remove(0) |> ignore
  }
  segments
}

///|
/// Extracts the path of an absolute or relative request URL.
fn request_path(url : String) -> String {
  let mut rest = url
  let mut absolute = true
  if url.has_prefix("https://") {
    rest = url[8:].to_owned()
  } else if url.has_prefix("http://") {
    rest = url[7:].to_owned()
  } else if url.has_prefix("//") {
    rest = url[2:].to_owned()
  } else {
    absolute = false
  }
  if absolute {
    rest = match rest.find("/") {
      Some(index) => rest[index:].to_owned()
      None => "/"
    }
  }
  if rest.find("#") is Some(index) {
    rest = rest[:index].to_owned()
  }
  if rest.find("?") is Some(index) {
    rest = rest[:index].to_owned()
  }
  if !rest.has_prefix("/") {
    rest = "/" + rest
  }
  if rest.has_prefix("/api/v3/") {
    rest = rest[7:].to_owned()
  }
  rest
}