///|
/// A GitHub API client backed by the transport-independent SDK runtime.
///
/// REST request and response shapes come from the generated `gen` package,
/// which is produced from the vendored first-party OpenAPI document. This facade
/// does not restate them: it only sends a generated request, follows `Link`
/// pages, and reads GitHub's error bodies.
///
/// GraphQL has no such generated vocabulary — a reply's shape belongs to the
/// caller's query rather than to the schema — so it is served by the `graphql`
/// passthrough over `Json`.
pub struct GitHub {
  priv client : @runtime.Client
  priv graphql_url : String
}

///|
/// Redacted debug representation of a GitHub client.
pub extend GitHub with @debug.Debug::{to_repr}

///|
/// Formats a client without exposing authentication or other runtime internals.
pub impl @debug.Debug for GitHub with fn to_repr(_self) {
  @debug.Repr::ctor("GitHub", [
    (None, @debug.Repr::literal("")),
  ])
}

///|
/// Creates a GitHub client.
///
/// `token` is optional: GitHub serves public resources unauthenticated, so an
/// absent token means `NoAuth` rather than a configuration error. A present
/// token is sent as `Authorization: Bearer`, which covers personal access
/// tokens, installation tokens, and `GITHUB_TOKEN`.
///
/// `base_url` is the API root. GitHub Enterprise Server mounts the REST API
/// under `/api/v3`, so pass `https://ghe.example.com/api/v3` there.
///
/// The default headers are `accept: application/vnd.github+json`,
/// `x-github-api-version`, and `user-agent`, which GitHub requires. A generated
/// operation that needs a different representation — `application/vnd.github.diff`,
/// `.patch`, `.sarif` — sets `accept` on its own request, and the runtime keeps
/// request headers ahead of client defaults.
///
/// `retry` defaults to `RetryPolicy::conservative().with_max_retries(3)` and
/// is wrapped in `GitHubRetry`, which also waits out GitHub's primary and
/// secondary rate limits and retries a transient GraphQL failure of a query.
///
/// Unless `limiter` says otherwise, responses feed a `WindowLimiter` reading
/// `x-ratelimit-remaining` and `x-ratelimit-reset`, keyed by
/// `x-ratelimit-resource` so that a write shares the `core` window. It spaces
/// writes and GraphQL requests at least 1 s apart, writes that send
/// notifications 3 s and searches 2 s, as octokit does. `gate` makes the
/// requests of each of those groups serial, as docs.github.com asks ("make
/// requests serially instead of concurrently"): pass
/// `gate=() => @http_async.SemaphoreGate::new()` from `gaato/http-async`.
/// Without it concurrent requests are only spaced, not serialised. `gate` is
/// ignored when `limiter` is given. Pass `limiter=@runtime.NoLimiter::new()`
/// to opt out of both.
///
/// `timeouts` limits each attempt and each call (`@runtime.Timeouts`); without
/// it only the transport's own timeouts apply.
///
/// `graphql_url` is the GraphQL endpoint, which is not under the REST root:
/// GitHub Enterprise Server serves REST from `/api/v3` and GraphQL from
/// `/api/graphql`. It is derived from `base_url` and only needs passing for a
/// deployment that puts the two somewhere else.
pub fn GitHub::new(
  transport : &@http.Transport,
  clock : &@clock.Clock,
  token? : String,
  base_url~ : String = "https://api.github.com",
  api_version~ : String = "2022-11-28",
  user_agent~ : String = "gaato-mbt-sdk/0.1.0",
  retry? : @runtime.RetryPolicy,
  limiter? : &@runtime.RateLimiter,
  graphql_url? : String,
  gate? : () -> &@sync.Gate,
  timeouts? : @runtime.Timeouts,
) -> GitHub {
  let auth = match token {
    Some(token) => @runtime.Bearer(token)
    None => @runtime.NoAuth
  }
  let limiter : &@runtime.RateLimiter = match limiter {
    Some(limiter) => limiter
    None => default_limiter(clock, gate?)
  }
  let graphql_url = match graphql_url {
    Some(url) => url
    None => default_graphql_url(base_url)
  }
  let retry = GitHubRetry::{
    policy: retry.unwrap_or_else(() => {
      @runtime.RetryPolicy::conservative().with_max_retries(3)
    }),
  }
  {
    client: @runtime.Client::new(
      transport,
      clock,
      base_url~,
      default_headers=default_headers(api_version~, user_agent~),
      auth~,
      retry~,
      limiter~,
      timeouts?,
    ),
    graphql_url,
  }
}

///|
/// Wraps a preconfigured runtime client.
///
/// This is the escape hatch for deployments this facade does not model — a
/// proxy that authenticates differently, a GitHub Enterprise Server instance
/// with extra headers — and for tests that build the client themselves.
///
/// A runtime client keeps its `base_url` to itself, so the GraphQL endpoint
/// cannot be derived from it here and defaults to GitHub's own. Pass
/// `graphql_url` for anything else.
pub fn GitHub::from_client(
  client : @runtime.Client,
  graphql_url~ : String = "https://api.github.com/graphql",
) -> GitHub {
  { client, graphql_url, }
}

///|
/// The headers every request carries unless the request sets them itself.
fn default_headers(
  api_version~ : String,
  user_agent~ : String,
) -> @http.Headers {
  @http.Headers::from_array([
    ("accept", "application/vnd.github+json"),
    ("x-github-api-version", api_version),
    ("user-agent", user_agent),
  ])
}

///|
/// The GraphQL endpoint that goes with a REST root.
///
/// GraphQL is not mounted under the REST root. GitHub Enterprise Server serves
/// REST from `http(s)://HOSTNAME/api/v3` and GraphQL from
/// `http(s)://HOSTNAME/api/graphql`, so appending `/graphql` to a `/api/v3`
/// root would address an endpoint that does not exist. On GitHub.com the two
/// share a host and appending is right.
fn default_graphql_url(base_url : String) -> String {
  let root = if base_url.has_suffix("/") {
    base_url[:base_url.length() - 1].to_owned()
  } else {
    base_url
  }
  if root.has_suffix("/api/v3") {
    // Keep everything up to and including "/api/", then name the other API.
    root[:root.length() - 2].to_owned() + "graphql"
  } else {
    root + "/graphql"
  }
}

///|
/// The rate limit window GitHub advertises on every response, recorded under
/// the resource the response names, with GitHub's pacing and the caller's
/// gate.
fn default_limiter(
  clock : &@clock.Clock,
  gate? : () -> &@sync.Gate,
) -> @runtime.WindowLimiter {
  @runtime.WindowLimiter::new(
    clock,
    @runtime.rate_limit_headers(
      remaining="x-ratelimit-remaining",
      reset_unix_seconds="x-ratelimit-reset",
    ),
    gate?,
    pace=github_pace,
    server_key=(_, headers) => headers.get("x-ratelimit-resource"),
  )
}