#!/usr/bin/env bash
# SPDX-License-Identifier: MIT
# SPDX-FileCopyrightText: 2025-2026 Marcus Quinn
# =============================================================================
# gh shim — auto-inject signature footer on write commands (t2685)
#            + GraphQL→REST read rewriting under low budget (t3037)
# =============================================================================
#
# Intercepts GitHub content writes (`gh issue/pr create|edit|comment|close|review`
# and selected `gh api` REST write endpoints), calls
# gh-signature-helper.sh footer, and appends the footer to --body / --body-file
# / issue-close --comment
# when the canonical HTML marker `<!-- aidevops:sig -->` is missing. Every
# other gh subcommand is passed straight through with minimal overhead.
#
# Why this exists
# ---------------
# The shell-level wrappers in shared-gh-wrappers.sh (gh_issue_comment,
# gh_create_issue, gh_create_pr, gh_pr_comment) auto-inject the signature —
# but ONLY when callers invoke them by name. Raw `gh issue comment …` from
# an opencode Bash tool call, a shell script, or an interactive terminal
# bypasses them entirely, because the shell wrapper is a function name, not
# a binary on PATH. This shim closes that gap: it sits on PATH before the
# real `gh` binary, so every gh invocation — no matter the caller — goes
# through sig enforcement.
#
# Design principles
# -----------------
#   1. Fast path: non-write subcommands pay a single case-match then exec.
#   2. Fail-open infrastructure: missing helpers and report-quality findings
#      preserve native `gh` behaviour. Deterministic security/write-policy
#      violations still fail closed; conflicting metadata is normalized.
#   3. Recursion guard: if the shim somehow re-enters (subprocess inherits
#      PATH), the second invocation short-circuits to the real gh.
#   4. Single source of truth: uses the same marker (`<!-- aidevops:sig -->`)
#      as _gh_wrapper_auto_sig in shared-gh-wrappers.sh for idempotent dedup.
#
# Bypass
# ------
#   AIDEVOPS_GH_SHIM_DISABLE=1 gh …          # skip entire shim
#   AIDEVOPS_GH_SHIM_NO_REST_REWRITE=1 gh …  # skip read-rewrite only (t3037)
#   AIDEVOPS_GH_EXPLICIT_PAGINATION_DISABLE=1 # retain native opaque pagination
#   AIDEVOPS_GH_EXACT_QUOTA_CAPTURE=1         # private response-framed quota evidence
#
# Ops/audit comments
# ------------------
# Deterministic ops comments such as the dispatch audit trail are posted by
# shell automation, not by the worker LLM session. They must still be signed for
# provenance, but the footer must not inherit stale OpenCode session duration or
# token metrics from a long-lived Linux runner DB.
#
# Runtime-vs-pre-execution split (t2893)
# --------------------------------------
# Two enforcement layers cooperate, separated by WHEN they run:
#   - JS plugin hook (quality-hooks-signature.mjs):
#       Runs PRE-bash-execution. Blocks gh writes missing the signature,
#       repairs `--body` and pre-existing `--body-file` in place. CANNOT
#       see files that bash will create later in the same call — the
#       readFileSync sees ENOENT and now reports FAIL_REASON.FILE_NOT_FOUND
#       with same-bash-call mentorship instead of a generic guess.
#   - This PATH shim:
#       Runs at EXEC-TIME (after bash finishes building --body-file). The
#       file exists at the moment gh is invoked, so this layer is the
#       canonical enforcement point for the same-bash-call shape. The JS
#       hook is the canonical enforcement point for `--body "literal"`
#       writes (where exec-time has no opportunity to mutate the body).
# When the JS hook blocks a `--body-file` call whose file is created
# earlier in the same bash command, the worker should split into two
# bash calls or source `shared-gh-wrappers.sh` and call gh_issue_comment
# (etc.) by name. The wrapper-sourcing path runs in the worker's shell
# AFTER the file-creation steps complete, then this shim takes over for
# the actual gh exec. AIDEVOPS_GH_SHIM_DISABLE=1 only defeats this shim
# — the JS hook still blocks unsigned writes.
#
# Related
# -------
#   .agents/plugins/opencode-aidevops/quality-hooks.mjs — JS-side enforcement
#   .agents/plugins/opencode-aidevops/quality-hooks-signature.mjs — t2893 structured failures
#   .agents/AGENTS.md "Signature footer hallucination (t2685)" — prompt-level rule
#   .agents/scripts/shared-gh-wrappers.sh _gh_wrapper_auto_sig — reference impl
# =============================================================================

_SHIM_SOURCE="${BASH_SOURCE[0]:-$0}"
while [[ -L "$_SHIM_SOURCE" ]]; do
	_SHIM_LINK_DIR="$(cd "$(dirname "$_SHIM_SOURCE")" 2>/dev/null && pwd)" || exit 127
	_SHIM_SOURCE=$(readlink "$_SHIM_SOURCE") || exit 127
	[[ "$_SHIM_SOURCE" == /* ]] || _SHIM_SOURCE="${_SHIM_LINK_DIR}/${_SHIM_SOURCE}"
done
_SHIM_DIR="$(cd "$(dirname "$_SHIM_SOURCE")" 2>/dev/null && pwd)" || _SHIM_DIR=""
_SHIM_SOURCE="${_SHIM_DIR}/$(basename "$_SHIM_SOURCE")"

# shellcheck source=./gh-native-transport-lib.sh
# shellcheck disable=SC1091  # sibling library resolved at runtime via $_SHIM_DIR
source "${_SHIM_DIR}/gh-native-transport-lib.sh"
# shellcheck source=./gh-api-guards-lib.sh
# shellcheck disable=SC1091  # sibling library resolved at runtime via $_SHIM_DIR
source "${_SHIM_DIR}/gh-api-guards-lib.sh"
# shellcheck source=./managed-label-provisioning-lib.sh
# shellcheck disable=SC1091  # sibling library resolved at runtime via $_SHIM_DIR
source "${_SHIM_DIR}/managed-label-provisioning-lib.sh"
# shellcheck source=./gh-write-policy-lib.sh
# shellcheck disable=SC1091  # sibling library resolved at runtime via $_SHIM_DIR
source "${_SHIM_DIR}/gh-write-policy-lib.sh"

if [[ -f "${_SHIM_DIR}/gh-transport-controls.sh" ]]; then
	# shellcheck source=./gh-transport-controls.sh
	source "${_SHIM_DIR}/gh-transport-controls.sh"
fi

# Reject recursive entry rather than forwarding to another shim generation.
# The resolver below prevents normal cross-generation selection; this sentinel
# bounds any unexpected re-entry caused by a child process or stale shim.
if [[ -n "${_AIDEVOPS_GH_SHIM_ACTIVE:-}" ]]; then
	printf '[aidevops] gh shim: recursive aidevops gh shim invocation blocked\n' >&2
	exit 126
fi

# Capture the opt-in body path in shim-local state, then remove it from the
# environment before helper probes or native gh subprocesses can inherit it.
_AIDEVOPS_GH_EPHEMERAL_BODY_FILE="${AIDEVOPS_GH_EPHEMERAL_BODY_FILE:-}"
unset AIDEVOPS_GH_EPHEMERAL_BODY_FILE


# The documented emergency bypass must avoid instrumentation and all normal
# shim policy while still rejecting every other aidevops-managed gh wrapper.
if [[ "${AIDEVOPS_GH_SHIM_DISABLE:-0}" == "1" ]]; then
	if [[ -n "$_AIDEVOPS_GH_EPHEMERAL_BODY_FILE" ]]; then
		printf '[aidevops][gh-ephemeral-body][BLOCK] Ephemeral transport cannot bypass the aidevops gh shim.\n' >&2
		exit 1
	fi
	_real_gh="$(_find_real_gh)" || {
		printf '[aidevops] gh shim: native gh binary not found on PATH\n' >&2
		exit 127
	}
	exec "$_real_gh" "$@"
fi

# -----------------------------------------------------------------------------
# Source instrumentation helper for gh_record_call (GH#21857 — full visibility).
# Load once here rather than lazily inside _shim_rest_rewrite_read so ALL gh
# calls — not just REST-fallback reads — are captured in the log.
# Fail-open: if the helper is unavailable, define a no-op stub so every
# gh_record_call site below is unconditionally safe.
# -----------------------------------------------------------------------------
_INST_LOADED=0
for _cand in \
	"$_SHIM_DIR/gh-api-instrument.sh" \
	"$HOME/.aidevops/agents/scripts/gh-api-instrument.sh"; do
	if [[ -f "$_cand" ]]; then
		# shellcheck source=/dev/null
		source "$_cand" 2>/dev/null && _INST_LOADED=1
		break
	fi
done
if [[ $_INST_LOADED -eq 0 ]]; then
	gh_record_call() { return 0; }
fi
if ! type gh_new_logical_id >/dev/null 2>&1; then
	gh_new_logical_id() {
		printf 'shim-%s-%s\n' "${BASHPID:-$$}" "${RANDOM:-0}"
		return 0
	}
fi
if ! type gh_attempt_count_for_logical >/dev/null 2>&1; then
	gh_attempt_count_for_logical() {
		local ignored_logical_id="$1"
		: "$ignored_logical_id"
		printf '0\n'
		return 0
	}
fi
if ! type gh_request_attempt_state_begin >/dev/null 2>&1; then
	gh_request_attempt_state_begin() {
		local ignored_logical_id="$1"
		: "$ignored_logical_id"
		return 1
	}
fi
if ! type gh_request_attempt_state_cleanup >/dev/null 2>&1; then
	gh_request_attempt_state_cleanup() { return 0; }
fi
if ! type gh_request_has_http_failure >/dev/null 2>&1; then
	gh_request_has_http_failure() {
		local ignored_logical_id="$1"
		: "$ignored_logical_id"
		return 1
	}
fi
if ! type gh_run_transport_attempt >/dev/null 2>&1; then
	gh_run_transport_attempt() {
		local ignored_path="$1"; shift
		local ignored_caller="$1"; shift
		local ignored_logical_id="$1"; shift
		local ignored_page="$1"; shift
		local ignored_retry="$1"; shift
		: "$ignored_path" "$ignored_caller" "$ignored_logical_id" "$ignored_page" "$ignored_retry"
		[[ "${1:-}" == "--" ]] && shift
		"$@"
		return $?
	}
fi
unset _cand _INST_LOADED

# Direct REST calls have documented fixed primary-rate costs, with a documented
# zero-cost exception for GET /rate_limit. Keep the parser separate from this
# already-large shim and fail closed when its provenance checks are unavailable.
_GHQA_LOADED=0
for _cand in \
	"$_SHIM_DIR/gh-quota-attribution-lib.sh" \
	"${HOME:+${HOME}/.aidevops/agents/scripts/gh-quota-attribution-lib.sh}"; do
	if [[ -n "$_cand" && -f "$_cand" ]]; then
		# shellcheck source=/dev/null
		source "$_cand" 2>/dev/null && _GHQA_LOADED=1
		break
	fi
done
if [[ $_GHQA_LOADED -eq 0 ]]; then
	_ghqa_exact_success_cost() { return 1; }
fi
unset _cand _GHQA_LOADED

# One ID follows the complete wrapper invocation, including REST translation,
# each explicit page, and any GraphQL fallback retry.
AIDEVOPS_GH_LOGICAL_ID="${AIDEVOPS_GH_LOGICAL_ID:-$(gh_new_logical_id)}"
export AIDEVOPS_GH_LOGICAL_ID
_GH_REQUEST_ATTEMPT_STATE_READY=0
if gh_request_attempt_state_begin "$AIDEVOPS_GH_LOGICAL_ID"; then
	_GH_REQUEST_ATTEMPT_STATE_READY=1
	trap 'gh_request_attempt_state_cleanup' EXIT
fi

# _shim_classify_endpoint <sub1> [<sub2>]
# Classify a gh invocation for instrumentation. Returns one of:
#   graphql | rest | search-graphql | other
# Follows the classification from GH#21857:
#   gh search *        → search-graphql
#   gh api graphql     → graphql
#   gh api <other>     → rest  (REST API endpoints)
#   gh pr|issue|…      → graphql (default for gh CLI commands)

_GHRP_LOADED=0
for _cand in \
	"$_SHIM_DIR/gh-rest-pagination-lib.sh" \
	"${HOME:+${HOME}/.aidevops/agents/scripts/gh-rest-pagination-lib.sh}"; do
	if [[ -n "$_cand" && -f "$_cand" ]]; then
		# shellcheck source=/dev/null
		source "$_cand" 2>/dev/null && _GHRP_LOADED=1
		break
	fi
done
unset _cand


# -----------------------------------------------------------------------------
# Headless external-repo write guard.
#
# Signature/footer and privacy layers protect content shape. Automated pulse and
# routine workers also need a trust-boundary guard: contributor/read-only repos
# are observe-only unless a human explicitly instigates the exact target. Keep
# this in the PATH shim so raw `gh ...` calls from LLM workers and scripts share
# one fail-closed gate.
# -----------------------------------------------------------------------------

# -----------------------------------------------------------------------------
# Fast pass-through for non-intercepted subcommands.
# Every gh invocation pays this case-match plus one exec.
# t2876: extended to include issue:edit and pr:edit so privacy-scan layer
# protects body/title edits and review bodies the same way create/comment are
# protected.
# t3037: read subcommands (pr:view, issue:view, pr:list, issue:list) are
# intercepted for GraphQL→REST budget-aware rewriting.
# -----------------------------------------------------------------------------
_SHIM_READ_REWRITE=""
case "${1:-}:${2:-}" in
	auth:*)
		# Authentication is a credential-control path, not application API traffic.
		# Login, logout, refresh, status, token, and git-credential may prompt or
		# access the OS keyring. Transport framing and telemetry can alter interactive
		# behaviour or retain sensitive diagnostics (GH#27777, GH#29153). Preserve
		# native stdin, stdout, stderr, and exit status byte-for-byte for every auth
		# subcommand.
		_real_gh="$(_find_real_gh)" || {
			printf '[aidevops] gh shim: real gh binary not found on PATH\n' >&2
			exit 127
		}
		exec "$_real_gh" "$@"
		;;
	issue:comment | issue:create | issue:edit | issue:close | issue:reopen | issue:lock | issue:pin | issue:delete | \
	pr:create | pr:comment | pr:edit | pr:review | pr:merge | pr:close | pr:reopen | pr:ready | pr:lock | \
	label:create | label:edit | label:delete | release:create | release:delete | release:upload) ;;
pr:view | issue:view | pr:list | issue:list)
	# t3037: read subcommands — may be rewritten to REST when GraphQL budget
	# is low. Falls through to the read-rewrite handler below.
	_SHIM_READ_REWRITE="${1}:${2}"
	;;
api:*)
	# `gh api` subcommand — needs further analysis for POST/PATCH write endpoints.
	# Falls through to the api-specific handling block below.
	;;
*)
	_real_gh="$(_find_real_gh)" || {
		printf '[aidevops] gh shim: real gh binary not found on PATH\n' >&2
		exit 127
	}
	# GH#21857/t3448: instrument every passthrough gh call (pr merge, pr checks,
	# run list, repo clone, etc.) that previously bypassed all recording.
	_transport_path=$(_shim_classify_endpoint "${1:-}" "${2:-}")
	_transport_caller=$(_shim_caller_label "${1:-}" "${2:-}")
	gh_record_call "$_transport_path" "$_transport_caller" 2>/dev/null || true
	_shim_run_transport "$_real_gh" "$_transport_path" "$_transport_caller" 0 "$@"
	exit $?
	;;
esac

export _AIDEVOPS_GH_SHIM_ACTIVE=1

REAL_GH="$(_find_real_gh)" || {
	printf '[aidevops] gh shim: real gh binary not found; aborting\n' >&2
	exit 127
}

# REST translators are sourced into this process and invoke `gh api` by name.
# Keep those calls at the same logical ID while bypassing PATH recursion and
# recording each native page/try at the actual execution boundary.
gh() {
	local path=""
	local caller=""
	path=$(_shim_classify_endpoint "${1:-}" "${2:-}")
	caller=$(_shim_transport_caller_label "${1:-}" "${2:-}")
	_shim_run_transport "$REAL_GH" "$path" "$caller" "${AIDEVOPS_GH_RETRY_INDEX:-0}" "$@"
	return $?
}

# -----------------------------------------------------------------------------
# GH#27002: reject oversized literal GraphQL connection pages locally.
#
# This intentionally inspects only query values supplied directly through gh's
# field flags. Queries supplied through --input, variables, files, or shell
# expressions are not evaluated or rewritten. Dynamic first/last values pass
# through with an auditable warning; framework-owned pagination should use the
# bounded helper named in the diagnostic.
# -----------------------------------------------------------------------------

if [[ "${1:-}:${2:-}" == "api:graphql" ]]; then
	_shim_graphql_guard_literal_fields "$@" || exit 1
fi

# -----------------------------------------------------------------------------
# t3037: GraphQL→REST budget-aware read rewriting.
#
# When GraphQL budget is below threshold, rewrite high-frequency read
# subcommands (pr view, issue view, pr list, issue list) to use REST API
# translators from shared-gh-wrappers-rest-fallback.sh. This captures
# every worker `gh pr view` / `gh issue view` call without requiring
# workers to source shared-gh-wrappers.sh.
#
# Bypass: AIDEVOPS_GH_SHIM_NO_REST_REWRITE=1
# Fail-open before REST dispatch and after local projection failures. An HTTP
# failure from an attempted REST transport remains authoritative so the shim
# does not duplicate it through GraphQL.
# -----------------------------------------------------------------------------
if [[ -n "$_SHIM_READ_REWRITE" ]]; then
	# Bypass — explicit override.
	if [[ "${AIDEVOPS_GH_SHIM_NO_REST_REWRITE:-0}" == "1" ]]; then
		_transport_path=$(_shim_classify_endpoint "${1:-}" "${2:-}")
		_transport_caller=$(_shim_transport_caller_label "${1:-}" "${2:-}")
		_transport_shape=$(_shim_read_shape_digest "$@" 2>/dev/null || printf 'shape-unknown')
		_transport_decision="graphql-bypass:${_transport_shape}"
		gh_record_call "$_transport_path" "$_transport_caller" "" "" "$_transport_decision" 2>/dev/null || true
		AIDEVOPS_GH_ROUTE_DECISION="$_transport_decision" \
			_shim_run_transport "$REAL_GH" "$_transport_path" "$_transport_caller" 0 "$@"
		exit $?
	fi

	# _shim_extract_repo_and_args: Parse --repo/-R from args and build the
	# stripped arg array for REST translator delegation. Sets _SHIM_REPO
	# and _SHIM_REST_ARGS (global-ish, consumed by the caller).
	# Returns 1 if no repo can be determined.
	_shim_extract_repo_and_args() {
		_SHIM_REPO=""
		_SHIM_REST_ARGS=()
		local _i=0
		local _args=("$@")
		# Extract --repo/-R from args.
		while [[ $_i -lt ${#_args[@]} ]]; do
			case "${_args[$_i]}" in
			--repo)   _SHIM_REPO="${_args[_i + 1]:-}"; _i=$((_i + 2)); continue ;;
			--repo=*) _SHIM_REPO="${_args[$_i]#--repo=}" ;;
			-R)       _SHIM_REPO="${_args[_i + 1]:-}"; _i=$((_i + 2)); continue ;;
			-R*)      _SHIM_REPO="${_args[$_i]#-R}" ;;
			esac
			_i=$((_i + 1))
		done
		if [[ -z "$_SHIM_REPO" ]]; then
			_SHIM_REPO=$(gh api repos/:owner/:repo --jq '.full_name' 2>/dev/null) || true
			[[ -z "$_SHIM_REPO" ]] && return 1
		fi
		# Build stripped args: skip first two positional args and --repo/-R.
		local _positional_count=0 _skip_next=0
		for (( _i=0; _i < ${#_args[@]}; _i++ )); do
			if [[ $_skip_next -eq 1 ]]; then _skip_next=0; continue; fi
			local _a="${_args[$_i]}"
			if [[ "$_a" != -* && $_positional_count -lt 2 ]]; then
				_positional_count=$((_positional_count + 1))
				continue
			fi
			case "$_a" in
			--repo) _skip_next=1; continue ;; --repo=*) continue ;;
			-R)     _skip_next=1; continue ;; -R*)      continue ;;
			esac
			_SHIM_REST_ARGS+=("$_a")
		done
		return 0
	}

	_shim_read_has_json_flag() {
		local _arg=""
		for _arg in "$@"; do
			case "$_arg" in
			--json | --json=*) return 0 ;;
			esac
		done
		return 1
	}

	_SHIM_REST_REWRITE_ATTEMPTED=0
	_shim_rest_rewrite_has_http_failure() {
		gh_request_has_http_failure "$AIDEVOPS_GH_LOGICAL_ID"
		return $?
	}

	_shim_rest_rewrite_read() {
		_SHIM_REST_REWRITE_ATTEMPTED=0
		# Source the REST fallback helper (contains _rest_should_fallback
		# and the _rest_* translator functions).
		local _rest_helper=""
		local _cand
		for _cand in \
			"$_SHIM_DIR/shared-gh-wrappers-rest-fallback.sh" \
			"$HOME/.aidevops/agents/scripts/shared-gh-wrappers-rest-fallback.sh"; do
			[[ -f "$_cand" ]] && { _rest_helper="$_cand"; break; }
		done
		[[ -z "$_rest_helper" ]] && return 1

		# Source the instrumentation helper for gh_record_call. Fail-open.
		local _inst_helper=""
		for _cand in \
			"$_SHIM_DIR/gh-api-instrument.sh" \
			"$HOME/.aidevops/agents/scripts/gh-api-instrument.sh"; do
			[[ -f "$_cand" ]] && { _inst_helper="$_cand"; break; }
		done
		# shellcheck source=/dev/null
		[[ -n "$_inst_helper" ]] && source "$_inst_helper" 2>/dev/null || true
		# shellcheck source=/dev/null
		source "$_rest_helper" 2>/dev/null || return 1

		# `gh * view/list --json ...` can include GraphQL-only safety-gate fields
		# (reviews, statusCheckRollup, reviewDecision). Preserve gate safety by
		# leaving non-equivalent reads on GraphQL. REST translators map common
		# issue/pr list/view fields onto gh-shaped output, and REST-first mode may
		# use them while GraphQL is healthy when the args are semantically safe.
		case "$_SHIM_READ_REWRITE" in
		pr:list) _rest_pr_list_can_preserve_args "${@:3}" || return 1 ;;
		pr:view) _rest_pr_view_can_preserve_args "${@:3}" || return 1 ;;
		issue:list) _rest_issue_list_can_preserve_args "${@:3}" || return 1 ;;
		issue:view) _rest_issue_view_can_preserve_args "${@:3}" || return 1 ;;
		*)
			_shim_read_has_json_flag "$@" && return 1
			;;
		esac

		# Prefer REST in pulse/workflow REST-first mode; otherwise use the legacy
		# low-GraphQL-budget fallback. This shares native GitHub quota pools instead
		# of imposing a synthetic lower GraphQL budget.
		if [[ "$_SHIM_READ_REWRITE" == pr:list ]] && _rest_pr_list_prefers_native_filter "${@:3}"; then
			_rest_should_fallback || return 1
		else
			_rest_read_first_enabled || _rest_should_fallback || return 1
		fi
		[[ "$_GH_REQUEST_ATTEMPT_STATE_READY" -eq 1 ]] || return 1

		# Extract repo and build stripped args.
		_shim_extract_repo_and_args "$@" || return 1

		# Record the call as REST under the original gh operation. The REST
		# translator records its own _rest_* label too; this shadow record keeps
		# before/after ratios visible for the original high-frequency command.
		local _route_path="rest"
		if { [[ "$_SHIM_READ_REWRITE" == "pr:list" ]] && _rest_args_have_author "${_SHIM_REST_ARGS[@]}"; } ||
			{ [[ "$_SHIM_READ_REWRITE" == "issue:list" ]] && _rest_args_have_search "${_SHIM_REST_ARGS[@]}"; }; then
			_route_path="search-rest"
		fi
		gh_record_call "$_route_path" "$(_shim_caller_label "${1:-}" "${2:-}")" 2>/dev/null || true

		# Dispatch to the appropriate REST translator.
		_SHIM_REST_REWRITE_ATTEMPTED=1
		case "$_SHIM_READ_REWRITE" in
		pr:view)    _rest_pr_view "${_SHIM_REST_ARGS[@]}" --repo "$_SHIM_REPO" ;;
		issue:view) _rest_issue_view "${_SHIM_REST_ARGS[@]}" --repo "$_SHIM_REPO" ;;
		pr:list)    _rest_pr_list_dispatch "${_SHIM_REST_ARGS[@]}" --repo "$_SHIM_REPO" ;;
		issue:list) _rest_issue_list_dispatch "${_SHIM_REST_ARGS[@]}" --repo "$_SHIM_REPO" ;;
		*) return 1 ;;
		esac
		return $?
	}

	# Attempt REST rewrite. A response-backed HTTP failure is authoritative:
	# retrying through GraphQL duplicates traffic and can turn a clear REST error
	# into an ambiguous second failure. Local projection failures retain native
	# fallback because the REST response may not reproduce the requested output.
	if _shim_rest_rewrite_read "$@"; then
		exit 0
	else
		_rest_rc=$?
		# A local admission/cooldown stop is not a failed JSON projection and
		# must not be retried against a different API resource.
		[[ "$_rest_rc" -ne 75 ]] || exit 75
		[[ "$_rest_rc" -ne 125 ]] || exit 125
		if [[ "$_SHIM_REST_REWRITE_ATTEMPTED" -eq 1 ]] && _shim_rest_rewrite_has_http_failure; then
			exit "$_rest_rc"
		fi
		# Preserve native fallback for projection/local failures because REST may
		# have returned data that cannot safely reproduce gh output. Routes that
		# never attempted REST are plain GraphQL selection.
		_transport_caller=$(_shim_transport_caller_label "${1:-}" "${2:-}")
		_transport_retry=$(gh_attempt_count_for_logical "$AIDEVOPS_GH_LOGICAL_ID")
		[[ "$_transport_retry" =~ ^[0-9]+$ ]] || _transport_retry=0
		_transport_decision=graphql-selected
		[[ "$_SHIM_REST_REWRITE_ATTEMPTED" -eq 0 ]] || _transport_decision=rest-fallback-graphql
		_transport_shape=$(_shim_read_shape_digest "$@" 2>/dev/null || printf 'shape-unknown')
		_transport_decision="${_transport_decision}:${_transport_shape}"
		gh_record_call graphql "$_transport_caller" "" "" "$_transport_decision" 2>/dev/null || true
		AIDEVOPS_GH_ROUTE_DECISION="$_transport_decision" \
			_shim_run_transport "$REAL_GH" graphql "$_transport_caller" "$_transport_retry" "$@"
		exit $?
	fi
fi

# -----------------------------------------------------------------------------
# Locate signature helper.
# -----------------------------------------------------------------------------
SIG_HELPER=""
for _cand in \
	"$_SHIM_DIR/gh-signature-helper.sh" \
	"$HOME/.aidevops/agents/scripts/gh-signature-helper.sh"; do
	if [[ -x "$_cand" ]]; then
		SIG_HELPER="$_cand"
		break
	fi
done

if [[ -z "$SIG_HELPER" ]]; then
	# No helper available — fail-open: exec real gh unchanged.
	_transport_path=$(_shim_classify_endpoint "${1:-}" "${2:-}")
	_transport_caller=$(_shim_caller_label "${1:-}" "${2:-}")
	gh_record_call "$_transport_path" "$_transport_caller" 2>/dev/null || true
	_shim_run_transport "$REAL_GH" "$_transport_path" "$_transport_caller" 0 "$@"
	exit $?
fi

# -----------------------------------------------------------------------------
# _shim_api_is_write_endpoint
# Returns 0 (true) when the current _modified_args array represents a
# POST or PATCH to an issue, issue-comment, PR, PR-review, or PR-review-comment
# REST endpoint.
# Recognises: /repos/*/issues, /repos/*/issues/N/comments, /repos/*/pulls,
# /repos/*/pulls/N/reviews, and PR review-comment endpoints.
# Leaves GET, DELETE, and non-content endpoints (e.g. /labels PATCH) untouched.
# -----------------------------------------------------------------------------

# -----------------------------------------------------------------------------
# t2876: Privacy-scan layer
# After signature-footer injection, before exec'ing real gh, scan write
# content for private-repo references when the target is a public repo.
# Fail-closed on hit; fail-open on missing helper / unauthenticated gh /
# unparseable args. Bypass via AIDEVOPS_GH_PRIVACY_BYPASS=1.
# -----------------------------------------------------------------------------

# Locate privacy-guard-helper.sh (same lookup pattern as SIG_HELPER above).
_PRIVACY_HELPER=""
for _cand in \
	"$_SHIM_DIR/privacy-guard-helper.sh" \
	"$HOME/.aidevops/agents/scripts/privacy-guard-helper.sh"; do
	if [[ -f "$_cand" ]]; then
		_PRIVACY_HELPER="$_cand"
		break
	fi
done

# _shim_privacy_scan
# Returns 0 to allow exec, 1 to block (caller should print no extra
# message — this function emits its own mentoring error to stderr).
# Fail-open on every error path.
_shim_privacy_scan() {
	# Bypass — explicit override with audit log.
	if [[ "${AIDEVOPS_GH_PRIVACY_BYPASS:-0}" == "1" ]]; then
		printf '[aidevops][privacy-scan] BYPASSED via AIDEVOPS_GH_PRIVACY_BYPASS=1\n' >&2
		return 0
	fi

	# Helper available?
	if [[ -z "$_PRIVACY_HELPER" || ! -f "$_PRIVACY_HELPER" ]]; then
		return 0 # fail-open
	fi
	# shellcheck source=/dev/null
	source "$_PRIVACY_HELPER" 2>/dev/null || return 0

	# Determine target repo from --repo arg, gh api /repos/owner/repo path, or
	# current git remote.
	local target_url="" _idx=0
	while [[ $_idx -lt ${#_modified_args[@]} ]]; do
		case "${_modified_args[$_idx]}" in
		--repo)
			target_url="${_modified_args[_idx + 1]:-}"
			_idx=$((_idx + 2))
			continue
			;;
		--repo=*) target_url="${_modified_args[$_idx]#--repo=}" ;;
		-R)
			target_url="${_modified_args[_idx + 1]:-}"
			_idx=$((_idx + 2))
			continue
			;;
		-R*) target_url="${_modified_args[$_idx]#-R}" ;;
		esac
		_idx=$((_idx + 1))
	done
	if [[ -z "$target_url" && "${_modified_args[0]:-}" == "api" ]]; then
		target_url="$(_shim_api_target_from_path 2>/dev/null || true)"
	fi
	if [[ -z "$target_url" ]]; then
		target_url=$(git remote get-url origin 2>/dev/null) || return 0
		[[ -z "$target_url" ]] && return 0
	fi
	# Normalize bare owner/repo to a URL form privacy_is_target_public accepts.
	if [[ "$target_url" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then
		target_url="https://github.com/${target_url}"
	fi

	# Public-target check — bind the already-resolved native binary so helper
	# probes cannot re-enter this PATH shim while its recursion guard is active.
	# This covers both the auth status probe and the subsequent repo API query.
	PRIVACY_GH_BIN="$REAL_GH" privacy_is_target_public "$target_url"
	local _pub_rc=$?
	if [[ $_pub_rc -ne 0 ]]; then
		return 0
	fi

	# Build content blob from --body / --body-file / --title args plus
	# `-f body=...` / `-F body=@...` forms used by gh api. Secret-material
	# egress scan runs before private-slug enumeration so it remains active even
	# when the user has no private repos configured.
	local _blob="" _bf _kv
	_idx=0
	while [[ $_idx -lt ${#_modified_args[@]} ]]; do
		case "${_modified_args[$_idx]}" in
		--body)
			_blob+="${_modified_args[_idx + 1]:-}"$'\n'
			_idx=$((_idx + 2))
			continue
			;;
		--body=*) _blob+="${_modified_args[$_idx]#--body=}"$'\n' ;;
		--body-file)
			_bf="${_modified_args[_idx + 1]:-}"
			[[ -n "$_bf" && -f "$_bf" ]] && _blob+="$(<"$_bf")"$'\n'
			_idx=$((_idx + 2))
			continue
			;;
		--body-file=*)
			_bf="${_modified_args[$_idx]#--body-file=}"
			[[ -n "$_bf" && -f "$_bf" ]] && _blob+="$(<"$_bf")"$'\n'
			;;
		--title)
			_blob+="${_modified_args[_idx + 1]:-}"$'\n'
			_idx=$((_idx + 2))
			continue
			;;
		--title=*) _blob+="${_modified_args[$_idx]#--title=}"$'\n' ;;
		-f | --field | -F | --raw-field)
			_kv="${_modified_args[_idx + 1]:-}"
			case "$_kv" in
			body=@*)
				_bf="${_kv#body=@}"
				[[ -f "$_bf" ]] && _blob+="$(<"$_bf")"$'\n'
				;;
			body=*) _blob+="${_kv#body=}"$'\n' ;;
			title=*) _blob+="${_kv#title=}"$'\n' ;;
			esac
			_idx=$((_idx + 2))
			continue
			;;
		-f* | -F* | --field=* | --raw-field=*)
			case "${_modified_args[$_idx]}" in
			-f*) _kv="${_modified_args[$_idx]#-f}" ;;
			-F*) _kv="${_modified_args[$_idx]#-F}" ;;
			--field=*) _kv="${_modified_args[$_idx]#--field=}" ;;
			--raw-field=*) _kv="${_modified_args[$_idx]#--raw-field=}" ;;
			esac
			case "$_kv" in
			body=@*)
				_bf="${_kv#body=@}"
				[[ -f "$_bf" ]] && _blob+="$(<"$_bf")"$'\n'
				;;
			body=*) _blob+="${_kv#body=}"$'\n' ;;
			title=*) _blob+="${_kv#title=}"$'\n' ;;
			esac
			;;
		esac
		_idx=$((_idx + 1))
	done

	local _secret_hits
	_secret_hits=$(privacy_scan_secret_material_text "$_blob")
	local _secret_scan_rc=$?
	if [[ $_secret_scan_rc -eq 1 ]]; then
		printf '\n[aidevops][privacy-scan][BLOCK] Write to public %s contains secret/private-key material:\n\n' "$target_url" >&2
		printf '%s\n' "$_secret_hits" | sed 's/^/  /' >&2
		printf '\n  Remove secret material before posting. Use synthetic fixtures only; never paste private keys or credential values.\n' >&2
		printf '  Bypass (audit-logged): AIDEVOPS_GH_PRIVACY_BYPASS=1 gh ...\n\n' >&2
		return 1
	fi

	# Enumerate private slugs.
	local _slugs_file
	_slugs_file=$(mktemp 2>/dev/null) || return 0
	if ! privacy_enumerate_private_slugs "$_slugs_file" 2>/dev/null; then
		rm -f "$_slugs_file"
		return 0
	fi
	# Build content blob from --body / --body-file / --title args plus
	# `-f body=...` / `-F body=@...` forms used by gh api.
	_blob=""
	_idx=0
	while [[ $_idx -lt ${#_modified_args[@]} ]]; do
		case "${_modified_args[$_idx]}" in
		--body)
			_blob+="${_modified_args[_idx + 1]:-}"$'\n'
			_idx=$((_idx + 2))
			continue
			;;
		--body=*) _blob+="${_modified_args[$_idx]#--body=}"$'\n' ;;
		--body-file)
			_bf="${_modified_args[_idx + 1]:-}"
			[[ -n "$_bf" && -f "$_bf" ]] && _blob+="$(<"$_bf")"$'\n'
			_idx=$((_idx + 2))
			continue
			;;
		--body-file=*)
			_bf="${_modified_args[$_idx]#--body-file=}"
			[[ -n "$_bf" && -f "$_bf" ]] && _blob+="$(<"$_bf")"$'\n'
			;;
		--title)
			_blob+="${_modified_args[_idx + 1]:-}"$'\n'
			_idx=$((_idx + 2))
			continue
			;;
		--title=*) _blob+="${_modified_args[$_idx]#--title=}"$'\n' ;;
		-f | --field | -F | --raw-field)
			_kv="${_modified_args[_idx + 1]:-}"
			case "$_kv" in
			body=@*)
				_bf="${_kv#body=@}"
				[[ -f "$_bf" ]] && _blob+="$(<"$_bf")"$'\n'
				;;
			body=*) _blob+="${_kv#body=}"$'\n' ;;
			title=*) _blob+="${_kv#title=}"$'\n' ;;
			esac
			_idx=$((_idx + 2))
			continue
			;;
		-f* | -F* | --field=* | --raw-field=*)
			case "${_modified_args[$_idx]}" in
			-f*) _kv="${_modified_args[$_idx]#-f}" ;;
			-F*) _kv="${_modified_args[$_idx]#-F}" ;;
			--field=*) _kv="${_modified_args[$_idx]#--field=}" ;;
			--raw-field=*) _kv="${_modified_args[$_idx]#--raw-field=}" ;;
			esac
			case "$_kv" in
			body=@*)
				_bf="${_kv#body=@}"
				[[ -f "$_bf" ]] && _blob+="$(<"$_bf")"$'\n'
				;;
			body=*) _blob+="${_kv#body=}"$'\n' ;;
			title=*) _blob+="${_kv#title=}"$'\n' ;;
			esac
			;;
		esac
		_idx=$((_idx + 1))
	done

	if [[ -z "$_blob" ]]; then
		rm -f "$_slugs_file"
		return 0
	fi

	local _hits
	_hits=$(privacy_scan_text "$_blob" "$_slugs_file")
	local _scan_rc=$?
	rm -f "$_slugs_file"

	if [[ $_scan_rc -eq 1 ]]; then
		printf '\n[aidevops][privacy-scan][BLOCK] Write to public %s contains private-repo references:\n\n' "$target_url" >&2
		printf '%s\n' "$_hits" | sed 's/^/  /' >&2
		printf '\n  Use generic placeholders (e.g. <webapp>) for private repo names before posting to public repos.\n' >&2
		printf '  Private slugs source: privacy_enumerate_private_slugs (mirror_upstream/local_only in repos.json + ~/.aidevops/configs/privacy-guard-extra-slugs.txt).\n' >&2
		printf '  Bypass (audit-logged): AIDEVOPS_GH_PRIVACY_BYPASS=1 gh ...\n\n' >&2
		return 1
	fi
	return 0
}

# -----------------------------------------------------------------------------
# Initialise the mutable args array used by all injection code below.
# -----------------------------------------------------------------------------
_modified_args=("$@")

# -----------------------------------------------------------------------------
# t3565: Raw interactive tracking-issue label normalization.
#
# The prompt rule says to use gh_create_issue/claim-task-id wrappers, but raw
# `gh issue create` still reaches this PATH shim from ad-hoc Bash calls. When an
# interactive session creates an aidevops-shaped tracking issue, inject the live
# ownership labels that the wrappers would have applied so the issue is never
# born invisible to review/dispatch guards.
# -----------------------------------------------------------------------------

_shim_normalize_interactive_tracking_issue_create
_shim_normalize_dispatch_labels


_shim_normalize_pr_create_origin

if ! _shim_block_pr_create_without_linked_issue_if_needed; then
	exit 1
fi

_shim_advise_framework_bug_issue_create_if_needed

if _shim_is_content_write_command "${_modified_args[@]}"; then
	if ! _shim_block_headless_external_write_if_needed "${_modified_args[@]}"; then
		exit 1
	fi
fi

# -----------------------------------------------------------------------------
# Handle `gh api` subcommand: intercept write calls to content endpoints.
# Non-targeted api calls pass straight through after this block.
# t2876: write endpoints also run through the privacy-scan layer.
# -----------------------------------------------------------------------------
if [[ "${1:-}" == "api" ]]; then
	if _shim_api_is_write_endpoint; then
		if ! _shim_block_headless_external_write_if_needed "${_modified_args[@]}"; then
			exit 1
		fi
		_shim_api_inject_body_sig
		if ! _shim_privacy_scan; then
			exit 1
		fi
	fi
	# GH#21857: instrument gh api calls — rest or graphql depending on path.
	_transport_path=$(_shim_classify_endpoint "${1:-}" "${2:-}")
	_transport_caller=$(_shim_transport_caller_label "${1:-}" "${2:-}")
	gh_record_call "$_transport_path" "$_transport_caller" 2>/dev/null || true
	_shim_run_transport "$REAL_GH" "$_transport_path" "$_transport_caller" 0 "${_modified_args[@]}"
	exit $?
fi

# -----------------------------------------------------------------------------
# Scan args for --body / --body-file and inject signature if missing.
# Mirrors _gh_wrapper_auto_sig in shared-gh-wrappers.sh. Marker-based dedup
# means idempotent: running the shim twice on the same args is a no-op. A PR
# merge body becomes a Git commit message, so it must remain byte-for-byte
# caller supplied to preserve terminal Git trailers (GH#29724).
# -----------------------------------------------------------------------------
_i=0
_body_idx=-1
_body_val=""
_body_eq=0
_body_file_idx=-1
_body_file_val=""
_body_file_eq=0
_comment_idx=-1
_comment_val=""
_comment_eq=0

while [[ $_i -lt ${#_modified_args[@]} ]]; do
	case "${_modified_args[_i]}" in
	--body)
		_body_idx=$_i
		_body_val="${_modified_args[_i + 1]:-}"
		_body_eq=0
		;;
	--body=*)
		_body_idx=$_i
		_body_val="${_modified_args[_i]#--body=}"
		_body_eq=1
		;;
	--body-file)
		_body_file_idx=$_i
		_body_file_val="${_modified_args[_i + 1]:-}"
		_body_file_eq=0
		;;
	--body-file=*)
		_body_file_idx=$_i
		_body_file_val="${_modified_args[_i]#--body-file=}"
		_body_file_eq=1
		;;
	--comment)
		if [[ "${_modified_args[0]:-}:${_modified_args[1]:-}" == "issue:close" ||
			"${_modified_args[0]:-}:${_modified_args[1]:-}" == "pr:close" ]]; then
			_comment_idx=$_i
			_comment_val="${_modified_args[_i + 1]:-}"
			_comment_eq=0
		fi
		;;
	--comment=*)
		if [[ "${_modified_args[0]:-}:${_modified_args[1]:-}" == "issue:close" ||
			"${_modified_args[0]:-}:${_modified_args[1]:-}" == "pr:close" ]]; then
			_comment_idx=$_i
			_comment_val="${_modified_args[_i]#--comment=}"
			_comment_eq=1
		fi
		;;
	-c)
		if [[ "${_modified_args[0]:-}:${_modified_args[1]:-}" == "issue:close" ||
			"${_modified_args[0]:-}:${_modified_args[1]:-}" == "pr:close" ]]; then
			_comment_idx=$_i
			_comment_val="${_modified_args[_i + 1]:-}"
			_comment_eq=0
		fi
		;;
	-c=*)
		if [[ "${_modified_args[0]:-}:${_modified_args[1]:-}" == "issue:close" ||
			"${_modified_args[0]:-}:${_modified_args[1]:-}" == "pr:close" ]]; then
			_comment_idx=$_i
			_comment_val="${_modified_args[_i]#-c=}"
			_comment_eq=1
		fi
		;;
	esac
	_i=$((_i + 1))
done

# Open a pre-signed managed body on an inherited descriptor, then remove and
# verify its only pathname before native gh starts the external write. This is
# an explicit one-shot mode for public triage comments: callers must use the
# exact aidevops shim and must not retry the consumed descriptor through REST.

# --- --body case -------------------------------------------------------------
if [[ "${_modified_args[0]:-}:${_modified_args[1]:-}" != "pr:merge" &&
	$_body_idx -ge 0 && -n "$_body_val" ]]; then
	if ! grep -Fqx '<!-- aidevops:sig -->' <<<"$_body_val"; then
		_sig_footer=$("$SIG_HELPER" footer --body "$_body_val" 2>/dev/null || echo "")
		if [[ -n "$_sig_footer" ]]; then
			_new_body="${_body_val}${_sig_footer}"
			if [[ $_body_eq -eq 1 ]]; then
				_modified_args[_body_idx]="--body=${_new_body}"
			else
				_modified_args[_body_idx + 1]="$_new_body"
			fi
		fi
	fi
fi

# --- issue close --comment/-c case ------------------------------------------
# Keep the close comment on the native close invocation so GitHub performs one
# comment-and-close mutation. Splitting it into `issue comment && issue close`
# would make retry recovery ambiguous if the first write succeeded and closure
# failed. This mirrors --body signing while preserving every close-only flag.
if [[ $_comment_idx -ge 0 && -n "$_comment_val" ]]; then
	if ! grep -Fqx '<!-- aidevops:sig -->' <<<"$_comment_val"; then
		_sig_footer=$("$SIG_HELPER" footer --body "$_comment_val" 2>/dev/null || echo "")
		if [[ -n "$_sig_footer" ]]; then
			_new_comment="${_comment_val}${_sig_footer}"
			if [[ $_comment_eq -eq 1 ]]; then
				_modified_args[_comment_idx]="--comment=${_new_comment}"
			else
				_modified_args[_comment_idx + 1]="$_new_comment"
			fi
		fi
	fi
fi

# --- --body-file case --------------------------------------------------------
# t2861: write the augmented content to a fresh temp file rather than appending
# to the user's source. The user's brief on disk stays byte-identical after the
# gh call. The temp file is cleaned up by a background reaper because the native
# transport may outlive setup-time shell traps.
if [[ "${_modified_args[0]:-}:${_modified_args[1]:-}" != "pr:merge" &&
	$_body_file_idx -ge 0 && -n "$_body_file_val" && -f "$_body_file_val" ]]; then
	if ! grep -Fqx '<!-- aidevops:sig -->' "$_body_file_val" 2>/dev/null; then
		_file_content=$(<"$_body_file_val") || _file_content=""
		_sig_footer=$("$SIG_HELPER" footer --body "$_file_content" 2>/dev/null || echo "")
		if [[ -n "$_sig_footer" ]]; then
			# Build augmented body in a temp file we own — never mutate the source.
			_tmp_body_file=$(mktemp -t aidevops-gh-shim-body.XXXXXX 2>/dev/null) || _tmp_body_file=""
			if [[ -n "$_tmp_body_file" ]]; then
				if printf '%s%s' "$_file_content" "$_sig_footer" >"$_tmp_body_file" 2>/dev/null; then
					# Substitute the arg pointing at user's file with our temp file.
					if [[ $_body_file_eq -eq 1 ]]; then
						_modified_args[_body_file_idx]="--body-file=${_tmp_body_file}"
					else
						_modified_args[_body_file_idx + 1]="$_tmp_body_file"
					fi
					# Fork a sleep-then-rm reaper so the temp survives long enough
					# for native gh to read it (30s >> typical gh runtime).
					( sleep 30 && rm -f "$_tmp_body_file" ) &
					disown
				else
					rm -f "$_tmp_body_file"
					# Fall through: gh receives original file, footer is omitted.
					# Better than corrupting the user's source on a write failure.
				fi
			fi
			# If mktemp failed, fall through silently — same safe degradation.
		fi
	fi
fi

# t2876: privacy scan runs after sig footer injection — fail-closed on hit.
if ! _shim_privacy_scan; then
	exit 1
fi

if ! _shim_prepare_ephemeral_body_file; then
	exit 1
fi

# t2861: test-mode hook — short-circuits exec so regression tests can inspect
# the resolved --body-file path without actually calling the real gh binary.
# Usage: SHIM_TEST_MODE=1 ./gh issue create --body-file <file> ...
if [[ "${SHIM_TEST_MODE:-}" == "1" ]]; then
	_shim_t=0
	while [[ $_shim_t -lt ${#_modified_args[@]} ]]; do
		case "${_modified_args[$_shim_t]}" in
		--body-file)
			printf 'resolved_body_file=%s\n' "${_modified_args[_shim_t + 1]}"
			;;
		--body-file=*)
			printf 'resolved_body_file=%s\n' "${_modified_args[$_shim_t]#--body-file=}"
			;;
		esac
		_shim_t=$((_shim_t + 1))
	done
	exit 0
fi

# Managed-label provisioning is a write and must happen only after all normal
# authorization, privacy, and local preparation gates accept the native create.
_shim_ensure_requested_managed_labels_for_create || true

# GH#21857: instrument write commands (issue comment/create/edit, pr create/
# comment/edit) — these all use the GraphQL endpoint.
_transport_caller=$(_shim_caller_label "${1:-}" "${2:-}")
gh_record_call graphql "$_transport_caller" 2>/dev/null || true
if [[ "${_modified_args[0]:-}:${_modified_args[1]:-}" == "pr:create" ]]; then
	_shim_run_pr_create_transport "$REAL_GH" graphql "$_transport_caller" 0 "${_modified_args[@]}"
	exit $?
fi
_shim_run_transport "$REAL_GH" graphql "$_transport_caller" 0 "${_modified_args[@]}"
exit $?
