#!/usr/bin/env bash
# agentbox `gh` shim — translates a strict subset of `gh` subcommands into
# `agentbox-ctl gh ...` so the host's authenticated `gh` runs the operation
# and only the result crosses back into the box. The in-box agent never sees
# a GitHub token.
#
# This shim ships only what Claude Code's PR badge and our documented agent
# flows need. Anything outside the subset below is rejected with a clear
# error — better safe than compatible. Add ops deliberately, not by default.

set -euo pipefail

# Paths are constants in production; env overrides exist purely to let unit
# tests substitute a stub `agentbox-ctl` on PATH without rewriting the shim.
CTL="${AGENTBOX_CTL_PATH:-/usr/local/bin/agentbox-ctl}"
REAL_GIT="${AGENTBOX_REAL_GIT_PATH:-/usr/bin/git}"

die() {
  printf 'agentbox gh shim: %s\n' "$*" >&2
  exit 2
}

# Resolve the in-box current branch once. Used to inject the right ref into
# `gh pr` commands so the host's `gh` (which runs in the host main repo, not
# the box's worktree) doesn't fall back to the host's HEAD.
box_branch() {
  "$REAL_GIT" -C "$PWD" rev-parse --abbrev-ref HEAD 2>/dev/null || true
}

# Returns 0 if any element of "$@" equals "$1" (the needle).
needle_present() {
  local needle="$1"; shift
  local arg
  for arg in "$@"; do
    if [ "$arg" = "$needle" ]; then return 0; fi
  done
  return 1
}

# Walk argv: if any arg starts with `-` and isn't in the allowed set, die.
# Doesn't try to validate flag _values_ (e.g. `--json number,url`); that's
# real gh's job — we only block flags we don't expect to see.
strict_flags() {
  local subcmd="$1"; shift
  local allowed="$1"; shift
  local re="^(${allowed})$"
  local arg
  for arg in "$@"; do
    case "$arg" in
      --) ;;
      -*)
        if ! [[ "$arg" =~ $re ]]; then
          die "unsupported flag '$arg' for '$subcmd'. Allowed: ${allowed//|/, }"
        fi
        ;;
    esac
  done
}

# Returns the first true positional arg of "$@" (skipping flags AND their
# values), or '' if none. $1 is a `|`-separated list of value-taking flags
# for the current subcommand — e.g. "--json|--title|--body|--base" means the
# token after any of those flags is a value, not a positional. Without this
# we'd treat `--json number,url`'s field list as the positional and miss
# branch injection (real bug: PR-badge lookups returned "no PR for main"
# because the JSON field list looked positional).
first_positional() {
  local value_taking="$1"; shift
  local re="^(${value_taking})$"
  local arg
  local skip_value=0
  for arg in "$@"; do
    if [ "$skip_value" = "1" ]; then
      skip_value=0
      continue
    fi
    case "$arg" in
      --) ;;
      -*)
        if [[ -n "$value_taking" && "$arg" =~ $re ]]; then
          skip_value=1
        fi
        ;;
      *) printf '%s\n' "$arg"; return 0 ;;
    esac
  done
}

handle_pr() {
  local op="${1-}"; shift || true
  if [ -z "$op" ]; then
    die "missing subcommand for 'gh pr'. Supported: view, list, create, comment, review, merge, checkout, close, reopen"
  fi
  local branch
  branch="$(box_branch)"

  case "$op" in
    view)
      strict_flags "gh pr view" "--json" "$@"
      if [ -z "$(first_positional "--json" "$@")" ] && [ -n "$branch" ]; then
        set -- "$branch" "$@"
      fi
      exec "$CTL" gh pr view -- "$@"
      ;;
    list)
      strict_flags "gh pr list" "--json|--state" "$@"
      if ! needle_present "--head" "$@" && [ -n "$branch" ]; then
        set -- "$@" "--head" "$branch"
      fi
      exec "$CTL" gh pr list -- "$@"
      ;;
    create)
      strict_flags "gh pr create" "--fill|--draft|--title|--body|--base" "$@"
      if ! needle_present "--head" "$@" && [ -n "$branch" ]; then
        set -- "$@" "--head" "$branch"
      fi
      exec "$CTL" gh pr create -- "$@"
      ;;
    comment)
      strict_flags "gh pr comment" "--body" "$@"
      if [ -z "$(first_positional "--body" "$@")" ] && [ -n "$branch" ]; then
        set -- "$branch" "$@"
      fi
      exec "$CTL" gh pr comment -- "$@"
      ;;
    review)
      strict_flags "gh pr review" "--approve|--request-changes|--comment|--body" "$@"
      if [ -z "$(first_positional "--body" "$@")" ] && [ -n "$branch" ]; then
        set -- "$branch" "$@"
      fi
      exec "$CTL" gh pr review -- "$@"
      ;;
    merge)
      strict_flags "gh pr merge" "--squash|--merge|--rebase|--delete-branch" "$@"
      if [ -z "$(first_positional "" "$@")" ] && [ -n "$branch" ]; then
        set -- "$branch" "$@"
      fi
      exec "$CTL" gh pr merge -- "$@"
      ;;
    close)
      strict_flags "gh pr close" "--delete-branch" "$@"
      if [ -z "$(first_positional "" "$@")" ] && [ -n "$branch" ]; then
        set -- "$branch" "$@"
      fi
      exec "$CTL" gh pr close -- "$@"
      ;;
    reopen)
      strict_flags "gh pr reopen" "" "$@"
      if [ -z "$(first_positional "" "$@")" ] && [ -n "$branch" ]; then
        set -- "$branch" "$@"
      fi
      exec "$CTL" gh pr reopen -- "$@"
      ;;
    checkout)
      # gh pr checkout takes a required ref (number / URL / branch). No
      # auto-inject — the relay also refuses checkout by default
      # (AGENTBOX_GH_PR_CHECKOUT=allow opt-in).
      strict_flags "gh pr checkout" "" "$@"
      if [ -z "$(first_positional "" "$@")" ]; then
        die "'gh pr checkout' requires a positional ref (PR number, URL, or branch)"
      fi
      exec "$CTL" gh pr checkout -- "$@"
      ;;
    *)
      die "'gh pr $op' is not proxied (supported: view, list, create, comment, review, merge, checkout, close, reopen)"
      ;;
  esac
}

handle_repo() {
  local op="${1-}"; shift || true
  if [ "$op" != "clone" ]; then
    die "'gh repo $op' is not proxied (supported: clone)"
  fi
  # gh repo clone <repo> [<dir>] [--branch <n>] [--depth <n>]
  strict_flags "gh repo clone" "--branch|--depth" "$@"
  # Split argv into (repo, dir, flags). Pass positionals BEFORE options to the
  # ctl so commander parses --branch/--depth as real options; using a `--`
  # separator would force commander to treat them as extra positionals (the
  # ctl `gh repo clone` command doesn't allowExcessArguments).
  local repo='' dir=''
  local -a flags=()
  local pos=0
  local skip_value=0
  local arg
  for arg in "$@"; do
    if [ "$skip_value" = "1" ]; then
      flags+=("$arg")
      skip_value=0
      continue
    fi
    case "$arg" in
      --branch|--depth)
        flags+=("$arg")
        skip_value=1
        ;;
      -*)
        flags+=("$arg")
        ;;
      *)
        if [ $pos -eq 0 ]; then repo="$arg"
        elif [ $pos -eq 1 ]; then dir="$arg"
        else die "too many positionals for 'gh repo clone' (got '$arg' after repo + dir)"
        fi
        pos=$((pos+1))
        ;;
    esac
  done
  if [ -z "$repo" ]; then
    die "'gh repo clone' requires a positional <repo> (owner/name or full URL)"
  fi
  if [ -n "$dir" ]; then
    exec "$CTL" gh repo clone "$repo" "$dir" ${flags[@]+"${flags[@]}"}
  else
    exec "$CTL" gh repo clone "$repo" ${flags[@]+"${flags[@]}"}
  fi
}

# Top-level dispatch.
if [ $# -eq 0 ]; then
  die "no subcommand. Supported: pr {view,list,create,comment,review,merge,checkout,close,reopen}, repo clone, auth status, --version"
fi

case "$1" in
  --version|-v)
    # Real gh prints something like:
    #   gh version 2.40.0 (2023-10-26)
    #   https://github.com/cli/cli/releases/tag/v2.40.0
    # Tools that sniff "gh version" succeed with our shim line too.
    printf 'gh version 2.0.0 (agentbox-shim)\n'
    printf 'https://github.com/cli/cli\n'
    ;;
  --help|-h)
    printf 'agentbox gh shim — strict subset.\n' >&2
    printf 'Supported: pr {view,list,create,comment,review,merge,checkout,close,reopen}, repo clone, auth status, --version\n' >&2
    printf 'Anything else is rejected. Run host `gh --help` for full upstream docs.\n' >&2
    ;;
  auth)
    shift
    case "${1-}" in
      status)
        # Real auth state is verified host-side on the next real RPC (relay's
        # assertGhReady returns exit 4 if the host is logged out). We don't
        # round-trip on every refresh-driven poll Claude Code does.
        printf 'agentbox gh shim: logged in to github.com (via agentbox host relay)\n' >&2
        ;;
      *)
        die "'gh auth ${1-}' is not proxied (supported: status)"
        ;;
    esac
    ;;
  pr)
    shift
    handle_pr "$@"
    ;;
  repo)
    shift
    handle_repo "$@"
    ;;
  *)
    die "'gh $1' is not proxied (supported: pr {…}, repo clone, auth status, --version)"
    ;;
esac
