#!/bin/sh
# gh wrapper for the ambient GitHub path (github-broker.ts).
#
# Fetches a fresh short-lived installation token from the loopback broker
# and execs the real gh with GH_TOKEN set, so `gh pr create`, `gh pr checks`,
# `gh run view`, `gh api`, etc. authenticate as the channel's GitHub App.
# The broker responds with two lines (token, then UNIX-epoch expiry) — the
# wrapper takes line 1 only; per-invocation fetch means expiry never
# matters here. GH_TOKEN lives ONLY in the child's env (the server scrubs
# any inherited GitHub tokens at start). Without a broker (channel without
# the github connector) gh runs unauthenticated — public reads still work,
# writes fail loudly.
#
# REPO SCOPING: when the repository in play is knowable — an explicit GH_REPO,
# else the checkout's github.com origin, else (fallbacks, 2026-08-30) a
# `-R`/`--repo` flag or the `repos/<owner>/<name>/...` prefix of a `gh api`
# endpoint — it is passed to the broker as `repo=owner/name` so the minted
# token covers that repo ONLY, not every repo the channel can reach. A
# repo-less READ (`gh api /orgs/...`, `gh auth status`) sends nothing and
# gets the channel-wide token, as before.
#
# REPO-LESS WRITES are REFUSED (2026-08-30): a write served the channel-wide
# token could touch every granted repo, not just the one in play. Only this
# wrapper sees the verb — the broker sees a credential request — so the
# wrapper classifies the invocation (`gh api` with a mutating -X/--method,
# or body fields which imply POST; or a mutating subcommand) and, when no
# repository could be resolved, declares it to the broker (`write=1`, which
# refuses and logs it) and exits with a "set GH_REPO" error instead of
# running the command. NOTE: `gh api graphql` counts as a write (it is a
# POST and can mutate) — set GH_REPO to use it.
#
# An explicit GH_TOKEN in the environment wins (never overridden).

# Overridable ONLY so the refusal/scoping paths can be tested against a stub
# broker and a stub gh binary (dev-runner has the same seam); production
# always uses the loopback default and the image's real gh.
# This instance has no GitHub at all: the `github` capability is off, or no GitHub
# App secret is configured. Say so once, plainly, instead of letting every
# command fail as an obscure auth error.
if [ "${FOUNDATION_GITHUB_DISABLED:-}" = "1" ]; then
  echo "GitHub is not connected on this instance." >&2
  exit 1
fi

BROKER_URL="${FOUNDATION_GH_BROKER_URL:-http://127.0.0.1:7791/git-credential}"
GH_REAL="${FOUNDATION_GH_REAL:-/opt/sky/bin/gh-real}"

# First positional token of a `gh api` invocation ($1 is "api"): the
# endpoint. Flags that take a value are skipped WITH their value so an
# argument like `-f state=closed` is never mistaken for the endpoint.
api_endpoint() {
  shift # the leading "api"
  while [ $# -gt 0 ]; do
    case "$1" in
      -X|--method|-f|-F|--field|--raw-field|-H|--header|-q|--jq|-t|--template|--input|--cache|--hostname|-p|--preview)
        shift
        [ $# -gt 0 ] && shift
        ;;
      -*) shift ;;
      *) printf '%s' "$1"; return 0 ;;
    esac
  done
  return 1
}

if [ -z "$GH_TOKEN" ] && [ -z "$GITHUB_TOKEN" ]; then
  cmd="$1"

  # --- classify write intent (only the wrapper can: the broker never sees
  # the verb). Unknown/new subcommands classify as reads — the broker's
  # grant intersection and GitHub's own permission checks still bound them.
  is_write=""
  if [ "$cmd" = "api" ]; then
    method=""
    has_body=""
    want_method=""
    for arg in "$@"; do
      if [ -n "$want_method" ]; then method="$arg"; want_method=""; continue; fi
      case "$arg" in
        -X|--method) want_method=1 ;;
        --method=*) method="${arg#--method=}" ;;
        -X?*) method="${arg#-X}"; method="${method#=}" ;;
        -f|-F|--field|--raw-field|--input|--field=*|--raw-field=*|--input=*|-f?*|-F?*) has_body=1 ;;
      esac
    done
    if [ -n "$method" ]; then
      case "$method" in
        [Gg][Ee][Tt]|[Hh][Ee][Aa][Dd]) ;;
        *) is_write=1 ;;
      esac
    elif [ -n "$has_body" ]; then
      is_write=1 # gh api implies POST when body fields are supplied
    fi
  else
    case "$cmd" in
      ''|-*|auth|browse|search|status|config|alias|completion|help|version) ;;
      *)
        case "$2" in
          create|delete|edit|merge|close|reopen|comment|review|ready|lock|unlock|transfer|develop|pin|unpin|upload|delete-asset|set|remove|rename|archive|unarchive|sync|enable|disable|cancel|rerun|run|dismiss|approve|update-branch|fork|link|unlink|mark-template|deploy) is_write=1 ;;
        esac
        ;;
    esac
  fi

  # --- resolve the repository in play. GH_REPO, then the checkout's origin
  # (both as before — a repo resolved yesterday is scoped identically today),
  # then the fallbacks: -R/--repo, and a `repos/<owner>/<name>` api endpoint.
  repo="$GH_REPO"
  if [ -z "$repo" ]; then
    origin="$(git config --get remote.origin.url 2>/dev/null)"
    case "$origin" in
      https://github.com/*) repo="${origin#https://github.com/}" ;;
      git@github.com:*) repo="${origin#git@github.com:}" ;;
      ssh://git@github.com/*) repo="${origin#ssh://git@github.com/}" ;;
    esac
    repo="${repo%.git}"
  fi
  if [ -z "$repo" ]; then
    flag_repo=""
    want_repo=""
    for arg in "$@"; do
      if [ -n "$want_repo" ]; then flag_repo="$arg"; want_repo=""; continue; fi
      case "$arg" in
        -R|--repo) want_repo=1 ;;
        --repo=*) flag_repo="${arg#--repo=}" ;;
        -R?*) flag_repo="${arg#-R}"; flag_repo="${flag_repo#=}" ;;
      esac
    done
    case "$flag_repo" in
      https://github.com/*) flag_repo="${flag_repo#https://github.com/}" ;;
      github.com/*) flag_repo="${flag_repo#github.com/}" ;;
    esac
    flag_repo="${flag_repo%.git}"
    case "$flag_repo" in
      */*/*) ;; # HOST/OWNER/NAME on some other host — not brokered
      */*) repo="$flag_repo" ;;
    esac
  fi
  if [ -z "$repo" ] && [ "$cmd" = "api" ]; then
    endpoint="$(api_endpoint "$@")"
    endpoint="${endpoint%%\?*}"
    endpoint="${endpoint#/}"
    case "$endpoint" in
      repos/*/*)
        rest="${endpoint#repos/}"
        owner="${rest%%/*}"
        rest="${rest#*/}"
        name="${rest%%/*}"
        repo="$owner/$name"
        ;;
    esac
  fi

  query=""
  case "$repo" in
    '' | *[!A-Za-z0-9._/-]*) ;;
    */*) query="?repo=$repo" ;;
  esac

  # FAIL CLOSED on a repo-less write: the channel-wide token must never back
  # a mutation. The probe (`write=1`) lets the broker log and refuse it
  # server-side; only connection-refused (curl rc=7: no broker = channel
  # without the github connector) falls through to anonymous gh, whose
  # writes fail at GitHub exactly as before.
  if [ -n "$is_write" ] && [ -z "$query" ]; then
    curl -fsS --max-time 25 --retry 2 --retry-connrefused "$BROKER_URL?write=1" >/dev/null 2>&1
    if [ $? -ne 7 ]; then
      echo "gh: refusing a GitHub write with no resolved repository — set GH_REPO=owner/name (or pass -R owner/name, or run inside the repository checkout) so the credential can be scoped to that one repo" >&2
      exit 1
    fi
    exec "$GH_REAL" "$@"
  fi

  # FAIL CLOSED when the broker exists but misbehaves (2026-08-16): an
  # unauthenticated fallthrough turns a broker hiccup into a misleading
  # GitHub 404 ("PR not found") that models mistake for a missing PR or a
  # sandbox limitation. Only connection-refused (curl rc=7: no broker =
  # channel without the github connector) falls through to anonymous.
  response="$(curl -fsS --max-time 25 --retry 2 --retry-connrefused "$BROKER_URL$query" 2>/dev/null)"
  rc=$?
  if [ $rc -eq 0 ]; then
    token="$(printf '%s\n' "$response" | head -n 1)"
    if [ -n "$token" ]; then
      GH_TOKEN="$token" exec "$GH_REAL" "$@"
    fi
    echo "gh: credential broker returned an empty token — transient; retry shortly" >&2
    exit 1
  elif [ $rc -ne 7 ]; then
    echo "gh: could not obtain GitHub credentials from broker (curl exit $rc) — transient; retry shortly" >&2
    exit 1
  fi
fi
exec "$GH_REAL" "$@"
