#!/bin/bash
# Git Pre-push Hook for OrchestKit Plugin
# Mirrors CI version-check.yml - fail fast locally before pushing
# Version: 1.0.0

set -uo pipefail

PROJECT_ROOT="$(git rev-parse --show-toplevel)"
cd "$PROJECT_ROOT"

# Resolve the branch actually being PUSHED, not the one checked out (#3290).
#
# git feeds pre-push one line per ref on stdin:
#   <local ref> <local sha> <remote ref> <remote sha>
#
# `git rev-parse --abbrev-ref HEAD` returns the literal string "HEAD" in a
# DETACHED worktree, so the conventional-commit prefix exemptions below never
# matched and the version gate fired on branches that are explicitly exempt.
# That is not an edge case: git refuses to check out one branch in two
# worktrees, so pushing a branch another worktree holds REQUIRES detached HEAD
# plus `git push origin HEAD:<branch>`. The hook penalised the only safe way to
# do it, and left --no-verify looking like the way out — which would relocate
# the security suite this hook also runs, not skip it.
#
# Falls back to the old behaviour when stdin is a tty (hook run by hand) or
# carries no branch ref, so nothing changes for a normal push.
resolve_push_branch() {
  local lref lsha rref rsha branch=""
  if [ ! -t 0 ]; then
    while read -r lref lsha rref rsha; do
      [ -n "${rref:-}" ] || continue
      case "$rref" in
        refs/heads/*)
          branch="${rref#refs/heads/}"
          break
          ;;
      esac
    done
  fi
  [ -n "$branch" ] || branch=$(git rev-parse --abbrev-ref HEAD)
  printf '%s\n' "$branch"
}

BRANCH=$(resolve_push_branch)
REMOTE="$1"
REMOTE_URL="$2"

echo "Running pre-push validations for branch: $BRANCH"

# ===== Skip for branches release-please or Dependabot own =====
# The skip pattern lives in ONE file shared with version-check.yml (#1460,
# #1457); edit it there, never inline here. An unreadable or empty pattern is
# a failure, not a skip and not an enforce: the gate refuses to decide.
# shellcheck source=../../scripts/ci/version-skip-pattern.sh
if ! . "$PROJECT_ROOT/scripts/ci/version-skip-pattern.sh"; then
  echo "FAILED: cannot source scripts/ci/version-skip-pattern.sh"
  exit 1
fi
if [[ -z "${VERSION_SKIP_PATTERN:-}" ]]; then
  echo "FAILED: VERSION_SKIP_PATTERN is empty; scripts/ci/version-skip-pattern.sh must define it"
  exit 1
fi
if [[ "$BRANCH" =~ $VERSION_SKIP_PATTERN ]]; then
  echo "  Skipping version check for $BRANCH (release-please or Dependabot owns the version)"
  exit 0
fi

# ===== Check if only non-code files changed =====
CHANGED_FILES=$(git diff --name-only origin/main...HEAD 2>/dev/null || git diff --name-only HEAD~1...HEAD)
CODE_CHANGES=$(echo "$CHANGED_FILES" | grep -vE '\.(md|txt)$|^docs/' || true)

if [[ -z "$CODE_CHANGES" ]]; then
  echo "  ✓ Skipping version check (only docs/config changes)"
  exit 0
fi

# ===== Version Check =====
echo -n "  Checking version bump... "

# Get versions (plugin.json moved to plugins/ork/ in marketplace restructure)
PLUGIN_JSON="plugins/ork/.claude-plugin/plugin.json"
PR_VERSION=$(jq -r '.version' "$PROJECT_ROOT/$PLUGIN_JSON" 2>/dev/null || echo "")
MAIN_VERSION=$(git show "origin/main:$PLUGIN_JSON" 2>/dev/null | jq -r '.version' 2>/dev/null || echo "")

if [[ -z "$PR_VERSION" ]]; then
  echo "FAILED (cannot read version)"
  exit 1
fi

if [[ -z "$MAIN_VERSION" ]]; then
  echo "OK (new repo or no main branch)"
else
  if [[ "$PR_VERSION" == "$MAIN_VERSION" ]]; then
    echo "FAILED"
    echo ""
    echo "╔════════════════════════════════════════════════════════════╗"
    echo "║  Version not bumped! Current: $MAIN_VERSION"
    echo "╠════════════════════════════════════════════════════════════╣"
    echo "║  Run: ./bin/bump-version.sh patch"
    echo "╚════════════════════════════════════════════════════════════╝"
    exit 1
  fi

  # Verify branch version is GREATER than main (not just different).
  #
  # This used `sort -V`, which is not semver: it ranks a prerelease ABOVE the
  # release it precedes. During the v10 alpha train that reads 10.0.0-alpha.1 as
  # newer than 10.0.0, so a push moving the version backwards would pass.
  # shellcheck source=../../scripts/lib/semver.sh
  . "$(git rev-parse --show-toplevel)/scripts/lib/semver.sh"
  HIGHER=$(semver_max "$MAIN_VERSION" "$PR_VERSION")
  if [[ "$HIGHER" != "$PR_VERSION" ]]; then
    echo "FAILED"
    echo ""
    echo "╔════════════════════════════════════════════════════════════╗"
    echo "║  Version $PR_VERSION is not greater than main ($MAIN_VERSION)!"
    echo "╠════════════════════════════════════════════════════════════╣"
    echo "║  Did you branch before a release merged to main?"
    echo "║  Run: git fetch origin main && ./bin/bump-version.sh patch"
    echo "╚════════════════════════════════════════════════════════════╝"
    exit 1
  fi

  echo "OK ($MAIN_VERSION → $PR_VERSION)"
fi

# ===== Changelog Check =====
echo -n "  Checking CHANGELOG.md... "

# Note: git diff exits 1 when differences exist; pipefail would break the pipe.
# Capture output first to avoid pipefail interaction.
# Note: pipefail + "echo $big_var | grep -q" causes SIGPIPE when grep exits early.
# Use bash pattern matching instead.
_CL_FILES=$(git diff origin/main --name-only 2>/dev/null </dev/null || true)
if [[ "$_CL_FILES" != *"CHANGELOG.md"* ]]; then
  echo "FAILED"
  echo ""
  echo "╔════════════════════════════════════════════════════════════╗"
  echo "║  CHANGELOG.md not updated!"
  echo "╠════════════════════════════════════════════════════════════╣"
  echo "║  Run: ./bin/bump-version.sh patch"
  echo "║  Then edit CHANGELOG.md with your changes"
  echo "╚════════════════════════════════════════════════════════════╝"
  exit 1
fi
echo "OK"

# ===== Changelog Entry Check =====
echo -n "  Checking CHANGELOG entry for v$PR_VERSION... "

if ! grep -q "^\## \[$PR_VERSION\]" CHANGELOG.md; then
  echo "FAILED"
  echo ""
  echo "╔════════════════════════════════════════════════════════════╗"
  echo "║  CHANGELOG.md missing entry for version $PR_VERSION"
  echo "╠════════════════════════════════════════════════════════════╣"
  echo "║  Add: ## [$PR_VERSION] - $(date +%Y-%m-%d)"
  echo "╚════════════════════════════════════════════════════════════╝"
  exit 1
fi
echo "OK"

# ===== Version Sync Check =====
echo -n "  Checking version sync... "

PLUGIN_V=$(jq -r '.version' plugins/ork/.claude-plugin/plugin.json 2>/dev/null)
MARKET_V=$(jq -r '.version' .claude-plugin/marketplace.json 2>/dev/null)
PYPROJ_V=$(grep -E '^version\s*=' pyproject.toml 2>/dev/null | sed -E 's/.*"([^"]*)".*/\1/')

MISMATCH=0
[[ "$PLUGIN_V" != "$MARKET_V" ]] && MISMATCH=1
[[ "$PLUGIN_V" != "$PYPROJ_V" ]] && MISMATCH=1

if [[ $MISMATCH -eq 1 ]]; then
  echo "FAILED"
  echo "  plugin.json: $PLUGIN_V"
  echo "  marketplace.json: $MARKET_V"
  echo "  pyproject.toml: $PYPROJ_V"
  echo ""
  echo "  Run: ./bin/bump-version.sh patch"
  exit 1
fi
echo "OK (all at v$PLUGIN_V)"

# Cross-platform timeout function (defined early for use in all checks)
run_with_timeout() {
  local timeout_secs=$1
  shift
  if command -v timeout &>/dev/null; then
    timeout "$timeout_secs" "$@"
  elif command -v gtimeout &>/dev/null; then
    gtimeout "$timeout_secs" "$@"
  else
    # No timeout command available (macOS without coreutils).
    # Both perl alarm and background-process watchdog produce unreliable exit
    # codes with npm on macOS. Run directly — timeout protection is only
    # critical on CI where Linux `timeout` is available.
    "$@"
  fi
}

# ===== TypeScript Type Check =====
echo -n "  Running TypeScript type check... "

if [[ -f "src/hooks/tsconfig.json" ]]; then
  if ! run_with_timeout 120 npx tsc --noEmit -p src/hooks/tsconfig.json >/dev/null 2>&1; then
    echo "FAILED"
    echo ""
    echo "╔════════════════════════════════════════════════════════════╗"
    echo "║  TypeScript validation failed!"
    echo "╠════════════════════════════════════════════════════════════╣"
    echo "║  Run: cd src/hooks && npx tsc --noEmit"
    echo "╚════════════════════════════════════════════════════════════╝"
    exit 1
  fi
  echo "OK"
else
  echo "SKIP (no tsconfig.json)"
fi

# ===== Build Verification =====
echo -n "  Verifying build... "

if ! run_with_timeout 300 npm run build >/dev/null 2>&1; then
  echo "FAILED"
  echo ""
  echo "╔════════════════════════════════════════════════════════════╗"
  echo "║  Build failed!"
  echo "╠════════════════════════════════════════════════════════════╣"
  echo "║  Run: npm run build"
  echo "╚════════════════════════════════════════════════════════════╝"
  exit 1
fi
echo "OK"

# ===== Import Verification =====
echo -n "  Verifying imports... "

if [[ -f "scripts/verify-imports.js" ]]; then
  if ! run_with_timeout 60 node scripts/verify-imports.js >/dev/null 2>&1; then
    echo "FAILED"
    echo ""
    echo "╔════════════════════════════════════════════════════════════╗"
    echo "║  Import verification failed!"
    echo "╠════════════════════════════════════════════════════════════╣"
    echo "║  Run: node scripts/verify-imports.js"
    echo "╚════════════════════════════════════════════════════════════╝"
    exit 1
  fi
  echo "OK"
else
  echo "SKIP (verify-imports.js not found)"
fi

# ===== Unit Tests (mirrors CI exactly) =====
echo -n "  Running unit tests... "

# Discover unit tests exactly the way CI does.
#
# This used to be a hardcoded 38-entry array under a comment claiming it "must
# match .github/workflows/ci.yml exactly". It did not, and nothing checked.
# CI globs (scripts/ci/run-tests.sh:57 — find -name "test-*.sh" -o -name
# "test-*.mjs"), so every file added since the array was last hand-edited ran in
# CI and never locally. Measured 2026-08-05: 38 listed, 61 on disk, 26 skipped,
# with zero comments justifying any exclusion. A developer whose pre-push passed
# could still be red in CI on tests they never saw execute.
#
# Sharing CI's discovery rule is what makes the comment true. The runner below
# is already parallel (MAX_JOBS), so the extra files are not a linear cost.
#
# If a test must be excluded, add it to SKIP_UNIT_TESTS with a REASON. An
# explicit, justified skip stays visible; a stale array does not.
SKIP_UNIT_TESTS=()

# NOT `mapfile` — it is a bash 4+ builtin and this script's shebang is
# `#!/bin/bash`, which on macOS is the bundled bash 3.2.57. There, `mapfile`
# is "command not found", CI_UNIT_TESTS stays empty, and the guard below then
# fires with "no unit tests discovered" on a repo that plainly has 61 of them.
# `scripts/ci/run-tests.sh:61` already reads its glob with this while-read
# loop for exactly that reason; sharing CI's discovery rule means sharing the
# way CI reads it.
CI_UNIT_TESTS=()
while IFS= read -r file; do
  [[ -n "$file" ]] || continue
  CI_UNIT_TESTS+=("$file")
done < <(
  find tests/unit \( -name "test-*.sh" -o -name "test-*.mjs" \) -type f \
    | sort \
    | { if ((${#SKIP_UNIT_TESTS[@]})); then grep -vFx -f <(printf '%s\n' "${SKIP_UNIT_TESTS[@]}"); else cat; fi; }
)

if ((${#CI_UNIT_TESTS[@]} == 0)); then
  echo "FAIL (no unit tests discovered under tests/unit — check the glob)"
  exit 1
fi


# Run tests in parallel (~160s sequential → ~75s parallel)
MAX_JOBS=8
if command -v nproc &>/dev/null; then
  CORES=$(nproc)
elif command -v sysctl &>/dev/null; then
  CORES=$(sysctl -n hw.ncpu 2>/dev/null || echo 8)
else
  CORES=8
fi
[[ $CORES -lt $MAX_JOBS ]] && MAX_JOBS=$CORES

export _PREPUSH_FAIL_DIR=$(mktemp -d "${TMPDIR:-/tmp}/ork.XXXXXX")
trap 'rm -rf "$_PREPUSH_FAIL_DIR"' EXIT

_run_test() {
  local test_file="$1"
  if [[ -f "$test_file" ]]; then
    if ! bash "$test_file" >/dev/null 2>&1; then
      touch "$_PREPUSH_FAIL_DIR/$(basename "$test_file")"
    fi
  else
    # A missing file used to be skipped in silence, so the summary counted it
    # as run. The old hardcoded list named 4 files that had been deleted, and
    # pre-push reported "38 tests" while executing 34. Discovery is a glob now,
    # so this should be unreachable; if it fires, something raced or the tree
    # changed mid-run, and that is worth failing rather than hiding.
    echo "MISSING: $test_file" >&2
    touch "$_PREPUSH_FAIL_DIR/MISSING-$(basename "$test_file")"
  fi
}
export -f _run_test

printf '%s\n' "${CI_UNIT_TESTS[@]}" | xargs -P "$MAX_JOBS" -I {} bash -c '_run_test "$1"' _ {}

FAILURES=()
for f in "$_PREPUSH_FAIL_DIR"/*; do
  [[ -e "$f" ]] && FAILURES+=("$(basename "$f")")
done

if [[ ${#FAILURES[@]} -gt 0 ]]; then
  echo "FAILED"
  for f in "${FAILURES[@]}"; do
    echo "    FAIL: $f"
  done
  echo ""
  echo "╔════════════════════════════════════════════════════════════╗"
  echo "║  Unit tests failed! (${#FAILURES[@]} test file(s) failed)"
  echo "╠════════════════════════════════════════════════════════════╣"
  echo "║  Run failing tests to see details"
  echo "╚════════════════════════════════════════════════════════════╝"
  exit 1
fi
echo "OK (${#CI_UNIT_TESTS[@]} tests, ${MAX_JOBS} parallel)"

# ===== Security Tests =====
echo -n "  Running security tests... "

if [[ -x "tests/security/run-security-tests.sh" ]]; then
  if ! ./tests/security/run-security-tests.sh >/dev/null 2>&1; then
    echo "FAILED"
    echo ""
    echo "╔════════════════════════════════════════════════════════════╗"
    echo "║  Security tests failed!"
    echo "╠════════════════════════════════════════════════════════════╣"
    echo "║  Run: ./tests/security/run-security-tests.sh"
    echo "╚════════════════════════════════════════════════════════════╝"
    exit 1
  fi
  echo "OK"
else
  echo "SKIP (not found)"
fi

# ===== MDX Compile Guard (#1401 — catches nested-fence + other mdx bugs) =====
echo -n "  Running mdx-compile guard... "

if [[ -x "tests/unit/test-mdx-compile.sh" ]]; then
  if ! run_with_timeout 120 ./tests/unit/test-mdx-compile.sh >/dev/null 2>&1; then
    echo "FAILED"
    echo ""
    echo "╔════════════════════════════════════════════════════════════╗"
    echo "║  MDX compile guard failed!"
    echo "╠════════════════════════════════════════════════════════════╣"
    echo "║  Run: npm run test:mdx"
    echo "║  Fix likely needs a 4-backtick outer fence in a SKILL.md"
    echo "╚════════════════════════════════════════════════════════════╝"
    exit 1
  fi
  echo "OK"
else
  echo "SKIP (not found)"
fi

# ===== Agent frontmatter validation =====
echo -n "  Validating agent frontmatter... "

if ! run_with_timeout 60 bash tests/agents/test-agent-frontmatter.sh >/dev/null 2>&1; then
  echo "FAILED"
  echo ""
  echo "╔════════════════════════════════════════════════════════════╗"
  echo "║  Agent frontmatter validation failed!"
  echo "╠════════════════════════════════════════════════════════════╣"
  echo "║  Run: npm run test:agents"
  echo "╚════════════════════════════════════════════════════════════╝"
  exit 1
fi
echo "OK"

# ===== Skill structure validation =====
echo -n "  Validating skill structure... "

if ! run_with_timeout 180 bash tests/skills/structure/test-skill-md.sh >/dev/null 2>&1; then
  echo "FAILED"
  echo ""
  echo "╔════════════════════════════════════════════════════════════╗"
  echo "║  Skill structure validation failed!"
  echo "╠════════════════════════════════════════════════════════════╣"
  echo "║  Run: npm run test:skills"
  echo "╚════════════════════════════════════════════════════════════╝"
  exit 1
fi
echo "OK"

# ===== Manifest consistency =====
echo -n "  Checking manifests... "

MANIFEST_FAILED=0
for check in tests/manifests/test-skill-uniqueness.sh \
             tests/manifests/test-manifest-dependencies.sh \
             tests/manifests/test-plugin-orphan-skills.sh; do
  if [[ -f "$check" ]]; then
    if ! run_with_timeout 30 bash "$check" >/dev/null 2>&1; then
      MANIFEST_FAILED=1
      break
    fi
  fi
done

if [[ $MANIFEST_FAILED -eq 1 ]]; then
  echo "FAILED"
  echo ""
  echo "╔════════════════════════════════════════════════════════════╗"
  echo "║  Manifest consistency failed!"
  echo "╠════════════════════════════════════════════════════════════╣"
  echo "║  Run: npm run test:manifests"
  echo "╚════════════════════════════════════════════════════════════╝"
  exit 1
fi
echo "OK"

# ===== Token overhead budget (blocking + warn tier) =====
echo -n "  Checking token overhead budget... "

TOKEN_LOG=$(mktemp "${TMPDIR:-/tmp}/ork.XXXXXX")
trap 'rm -f "$TOKEN_LOG"' EXIT

if ! run_with_timeout 30 bash tests/performance/test-token-overhead.sh >"$TOKEN_LOG" 2>&1; then
  echo "FAILED"
  echo ""
  echo "╔════════════════════════════════════════════════════════════╗"
  echo "║  Token overhead exceeds budget!"
  echo "╠════════════════════════════════════════════════════════════╣"
  tail -6 "$TOKEN_LOG" | sed 's/^/║  /'
  echo "╚════════════════════════════════════════════════════════════╝"
  exit 1
fi

# Surface any 90%+ warnings even when the hard check passed
WARN_LINES=$(grep -E '⚠|WARN:' "$TOKEN_LOG" || true)
if [[ -n "$WARN_LINES" ]]; then
  echo "OK (with warnings)"
  echo "$WARN_LINES" | sed 's/^/    /'
else
  echo "OK"
fi


# ===== Hooks vitest suite (catches Bundle-C-style fix-up cycles) =====
# Project lesson from M126 #1543: changes to src/hooks/ trigger a 30+ test
# surface in CI (cross-reference, split-bundles, webhook-forwarder-coverage,
# hooks-json-wiring, dispatcher-registry-wiring). Running the hook unit
# suite locally catches these BEFORE push instead of after a failed CI run.
HOOK_CHANGES=$(echo "$CHANGED_FILES" | grep -E '^src/hooks/' || true)
if [[ -n "$HOOK_CHANGES" ]]; then
  echo -n "  Running hook vitest suite... "
  HOOK_LOG="$(mktemp "${TMPDIR:-/tmp}/ork.XXXXXX")"
  trap 'rm -f "$HOOK_LOG"' EXIT
  if ! (cd src/hooks && npx vitest run --reporter=dot >"$HOOK_LOG" 2>&1); then
    echo "FAILED"
    echo ""
    echo "╔════════════════════════════════════════════════════════════╗"
    echo "║  Hooks vitest suite failed locally."
    echo "║  CI runs the same suite — fix before push to avoid round-trips."
    echo "╠════════════════════════════════════════════════════════════╣"
    tail -20 "$HOOK_LOG" | sed 's/^/║  /'
    echo "╚════════════════════════════════════════════════════════════╝"
    exit 1
  fi
  echo "OK"
fi

echo ""
echo "Pre-push validation passed"
exit 0
