#!/usr/bin/env bash

:<<'DOC'
GraphQL request primitives with error handling and cursor-based pagination.

Built on http_request from http.api.sh. Handles the GraphQL-specific concern
that errors are returned in the response body (HTTP 200) rather than via
status codes.
DOC

import "$SHELLOPS_DIR/lib/http.api.sh"

__graphql_api__QUERIES_DIR="$SHELLOPS_DIR/lib/graphql"
__graphql_api__EMPTY_VARS='{}'

:<<'DOC'
Reads a .graphql file from lib/graphql/<service>/<operation>.graphql.

Usage: graphql_load "linear" "get-issue"
DOC
function graphql_load() {
  local service="$1" operation="$2"
  local filepath="$__graphql_api__QUERIES_DIR/$service/$operation.graphql"

  if [[ ! -f "$filepath" ]]; then
    error "GraphQL document not found: $filepath"
    return 1
  fi

  cat "$filepath"
}

:<<'DOC'
Executes a GraphQL query and returns the .data portion of the response.

Checks for .errors in the response body — GraphQL returns HTTP 200 even
on query failures, so http_request alone cannot detect them.

Usage:
  graphql_request "$endpoint" "$http_opts" "$query"
  graphql_request "$endpoint" "$http_opts" "$query" "$variables_json"
DOC
function graphql_request() {
  local endpoint="$1" http_opts="$2" query="$3"
  local variables="${4:-$__graphql_api__EMPTY_VARS}"

  local body result
  body="$(jq_set '{}' \
    '.query' "$query" \
    '.variables' "$variables"
  )"

  result="$(
    http_post "$endpoint" "$body" "$http_opts"
  )" || return 1

  if __graphql_api__has_errors "$result"; then
    __graphql_api__log_errors "$result" "$endpoint"
    return 1
  fi

  jq_get "$result" '.data'
}

:<<'DOC'
Executes a paginated GraphQL query and returns all nodes concatenated.

The query MUST include a pageInfo { hasNextPage endCursor } selection and
accept a $cursor: String variable. The data_path identifies where the
paginated connection lives within .data (e.g. ".organization.repositories").

Usage:
  graphql_paginate "$endpoint" "$http_opts" "$query" "$variables" "$data_path"
DOC
function graphql_paginate() {
  local endpoint="$1" http_opts="$2" query="$3"
  local variables="${4:-$__graphql_api__EMPTY_VARS}" data_path="$5"
  local all_nodes="[]" cursor="null"
  local vars_with_cursor data
  local connection nodes has_next end_cursor

  while true; do
    vars_with_cursor="$(jq_set "$variables" '.cursor' "$cursor")"

    data="$(
      graphql_request "$endpoint" \
        "$http_opts" \
        "$query" \
        "$vars_with_cursor"
    )" || return 1

    connection="$(jq_get "$data" "$data_path")"
    nodes="$(jq_get "$connection" '.nodes' '.edges' '[]')"
    has_next="$(jq_get "$connection" '.pageInfo.hasNextPage')"
    end_cursor="$(jq_get "$connection" '.pageInfo.endCursor')"

    all_nodes="$(jq_concat "$all_nodes" "$nodes")"

    if [[ "$has_next" != "true" ]]; then
      break
    fi

    cursor="$end_cursor"
  done

  echo "$all_nodes"
}

function __graphql_api__has_errors() {
  local result="$1"
  local errors error_count
  errors="$(jq_get "$result" '.errors' '[]')"
  error_count="$(jq_len "$errors")"
  (( error_count > 0 ))
}

function __graphql_api__log_errors() {
  local result="$1" endpoint="$2"
  local errors messages
  errors="$(jq_get "$result" '.errors' '[]')"
  messages="$(jq_join "$(jq_pluck "$errors" '.message')" "; ")"
  error "GraphQL query to $endpoint failed: $messages"
}
