#!/usr/bin/env bash
set -euo pipefail

# Generated by fallow setup-hooks.
# Installer version: @@FALLOW_INSTALLER_VERSION@@
# Requires bash and jq. On Windows run via git-bash or WSL.
# Blocks Claude Code git commit and git push when fallow audit returns verdict fail.
# Runtime errors fail open with a single stderr notice so skips stay visible.
# Set FALLOW_GATE_DEBUG=1 to also log when a command is skipped because it is
# not classified as a git commit or push.
#
# Version floor (FALLOW_GATE_MIN_VERSION, default 2.85.0). The gate passes
# --gate-marker agent (added in v2.85.0) so Impact can record containment;
# older binaries reject the flag entirely, which would make every audit fail
# open. The floor also covers the older uncommitted-changes inclusion fix
# (aabb8e1b, v2.46.0). Set the env var to the empty string to disable.
# Floor comparison uses `sort -V`; GNU and BSD agree on plain semver but
# diverge on prereleases (BSD sorts `2.48.0-alpha.1` ABOVE `2.48.0`, GNU below).
# If you set a prerelease floor explicitly, verify the behavior on the target OS.

if ! command -v jq >/dev/null 2>&1; then
  echo "fallow-gate: jq not on PATH, skipping audit." >&2
  exit 0
fi

INPUT="$(cat)"
CMD="$(jq -r '.tool_input.command // empty' <<<"$INPUT")"

# Tokenize instead of matching one regex so git-level options between `git`
# and the subcommand (git -c k=v commit, git -C dir push, git --no-pager
# commit, git --git-dir=/x push) still route into the audit, while subcommand
# lookalikes in arguments (git log commit-message.txt) do not. See issue #2106.
is_git_write_command() {
  local cmd="$1" segment
  # Control operators separate simple commands; each becomes its own line.
  while IFS= read -r segment; do
    # Intentional word splitting; globbing is disabled below.
    # shellcheck disable=SC2086
    set -- $segment
    while [ "$#" -gt 0 ]; do
      if [ "$1" != "git" ]; then
        shift
        continue
      fi
      shift
      while [ "$#" -gt 0 ]; do
        case "$1" in
          commit | push)
            return 0
            ;;
          -c | -C | --git-dir | --work-tree | --namespace | --config-env | --super-prefix | --exec-path | --list-cmds | --attr-source)
            # Global option whose value arrives as the next word.
            shift
            [ "$#" -gt 0 ] && shift
            ;;
          -*)
            # Value-less global option (--no-pager) or inline-value form
            # (--git-dir=/x, -cuser.name=x).
            shift
            ;;
          *)
            # A different subcommand; resume scanning for a later `git` word.
            break
            ;;
        esac
      done
    done
  done < <(printf '%s\n' "$cmd" | tr ';|&()' '\n\n\n\n\n')
  return 1
}

set -f
if is_git_write_command "$CMD"; then
  GIT_WRITE=1
else
  GIT_WRITE=0
fi
set +f
if [ "$GIT_WRITE" -eq 0 ]; then
  if [ -n "${FALLOW_GATE_DEBUG:-}" ]; then
    echo "fallow-gate: not a git commit/push, skipping audit." >&2
  fi
  exit 0
fi

if command -v fallow >/dev/null 2>&1; then
  RUNNER=(fallow)
  BIN_DESC="$(command -v fallow)"
elif command -v npx >/dev/null 2>&1 && VER_PROBE="$(npx --no-install fallow --version 2>/dev/null || true)" && [[ "$VER_PROBE" == fallow* ]]; then
  RUNNER=(npx --no-install fallow)
  BIN_DESC="npx --no-install fallow"
else
  echo "fallow-gate: fallow binary not found (tried PATH and npx --no-install), skipping audit." >&2
  exit 0
fi

VERSION_RAW="$("${RUNNER[@]}" --version 2>/dev/null || true)"
VERSION="${VERSION_RAW#fallow }"
VERSION="${VERSION%% *}"

MIN_VERSION="${FALLOW_GATE_MIN_VERSION-2.85.0}"
if [ -n "$MIN_VERSION" ] && [ -n "$VERSION" ]; then
  LOWER="$(printf '%s\n%s\n' "$MIN_VERSION" "$VERSION" | sort -V | head -n1)"
  if [ "$LOWER" != "$MIN_VERSION" ]; then
    {
      echo "fallow-gate: blocked: $BIN_DESC is fallow $VERSION, below required $MIN_VERSION."
      echo "fallow-gate: older binaries reject the --gate-marker flag this gate passes"
      echo "fallow-gate: (added in fallow v2.85.0), so the audit cannot run."
      echo "fallow-gate: upgrade the fallow on PATH (e.g. npm install -g fallow@latest or"
      echo "fallow-gate: cargo install fallow-cli), or set FALLOW_GATE_MIN_VERSION= to disable."
    } >&2
    exit 2
  fi
fi

TMP_JSON="$(mktemp)"
TMP_ERR="$(mktemp)"
cleanup() {
  rm -f "$TMP_JSON" "$TMP_ERR"
}
trap cleanup EXIT

if "${RUNNER[@]}" audit --format json --quiet --explain --gate-marker agent >"$TMP_JSON" 2>"$TMP_ERR"; then
  STATUS=0
else
  STATUS=$?
fi

VERDICT="$(jq -r '.verdict // empty' <"$TMP_JSON" 2>/dev/null || true)"
IS_ERROR="$(jq -r '.error // false' <"$TMP_JSON" 2>/dev/null || echo false)"

if [ "$VERDICT" = "fail" ]; then
  echo "fallow-gate: blocked by fallow ${VERSION:-unknown} at $BIN_DESC" >&2
  cat "$TMP_JSON" >&2
  exit 2
fi

if [ "$STATUS" -eq 2 ] || [ "$IS_ERROR" = "true" ]; then
  MSG="$(jq -r '.message // empty' <"$TMP_JSON" 2>/dev/null || true)"
  if [ -n "$MSG" ]; then
    echo "fallow-gate: fallow audit runtime error ($MSG), skipping." >&2
  else
    echo "fallow-gate: fallow audit runtime error, skipping." >&2
  fi
  exit 0
fi

if [ "$STATUS" -ne 0 ]; then
  ERR_LINE="$(sed -n '1p' "$TMP_ERR" 2>/dev/null || true)"
  if [ -n "$ERR_LINE" ]; then
    echo "fallow-gate: fallow audit exited $STATUS ($ERR_LINE), skipping." >&2
  else
    echo "fallow-gate: fallow audit exited $STATUS, skipping." >&2
  fi
  exit 0
fi

exit 0
