#!/usr/bin/env bash
# Möbius pre-push gate.
#
# Catches the classes of breakage that have actually reached `main` and
# hurt — merge-conflict markers (which once crash-looped prod), syntax
# errors, and frontend-unit regressions — BEFORE they're pushed. Fast by
# design: conflict + syntax checks are instant; frontend-unit (~6s) runs
# when frontend changed (if node_modules is present); the backend suite
# runs on backend changes via the shared venv (~2m; --no-verify to skip).
# CI is the backstop for anything skipped (e.g. from a deps-less worktree).
#
# Install: scripts/install-hooks.sh (copies this into the shared hooks
# dir, so every worktree is covered). Bypass once: `git push --no-verify`.
#
# Design note: this is a GATE, not a full CI replacement. It mirrors the
# fast CI jobs (frontend-unit, syntax) so a red push is caught in seconds
# instead of minutes — but e2e + the full backend suite still run in CI.

set -uo pipefail

# Resolve the repo root, self-healing the core.bare=true corruption that
# app-git test escapes leak into the shared .git/config (.pm/096): it makes
# --show-toplevel fail with "must be run in a work tree" and silently breaks
# every worktree (they share .git/config). Detect, repair to false, retry.
# Plain echo here, not warn() — the helpers aren't defined until below.
ROOT="$(git rev-parse --show-toplevel 2>/dev/null)"
if [ -z "$ROOT" ]; then
  if git config --local core.bare 2>/dev/null | grep -q '^true$'; then
    echo "[pre-push] core.bare=true in shared config — repairing to false" >&2
    git config --local core.bare false
    ROOT="$(git rev-parse --show-toplevel)" || exit 1
  else
    exit 1
  fi
fi
cd "$ROOT" || exit 1

# Main checkout (parent of the shared .git dir). Worktrees don't get their
# own node_modules / backend venv, so the dep-bearing checks below resolve
# their tools from here — the venv runs the WORKTREE's code (its cwd is on
# sys.path), it just supplies the deps. Equals $ROOT in the main checkout.
MAIN="$(cd "$(dirname "$(git rev-parse --git-common-dir)")" && pwd 2>/dev/null)" || MAIN="$ROOT"

# Per-invocation temp dir for check output (mode 0700, unguessable name),
# removed on exit. Avoids the symlink-race / clobber vector of fixed
# /tmp paths on a shared host.
PP_TMP="$(mktemp -d "${TMPDIR:-/tmp}/mobius-pp.XXXXXX")" || exit 1
trap 'rm -rf "$PP_TMP"' EXIT

FAIL=0
c_ok=$'\033[1;32m'; c_warn=$'\033[1;33m'; c_err=$'\033[1;31m'; c_off=$'\033[0m'
ok()   { printf '%s[pre-push]%s %s\n' "$c_ok"  "$c_off" "$*"; }
warn() { printf '%s[pre-push]%s %s\n' "$c_warn" "$c_off" "$*"; }
err()  { printf '%s[pre-push]%s %s\n' "$c_err" "$c_off" "$*" >&2; }

# Commits being pushed = everything on HEAD not yet on origin/main.
BASE="$(git merge-base HEAD origin/main 2>/dev/null || echo HEAD~1)"
CHANGED="$(git diff --name-only --diff-filter=d "$BASE"..HEAD 2>/dev/null || true)"
changed_match() { echo "$CHANGED" | grep -qE "$1"; }
changed_list()  { echo "$CHANGED" | grep -E "$1" || true; }

# 1. Conflict markers — the prod-outage preventer. Matches the real git
#    marker shape (`<<<<<<< ` / `>>>>>>> `), not bare `=======` which is a
#    common doc separator. Scans the tip being pushed.
if git grep -nIE '^(<<<<<<<|>>>>>>>) ' HEAD -- \
     'backend' 'frontend/src' 'skill' 'scripts' >"$PP_TMP/confl" 2>/dev/null; then
  err "merge-conflict markers in the tree you're pushing:"
  sed 's/^/    /' "$PP_TMP/confl" >&2
  FAIL=1
else
  ok "no conflict markers"
fi

# 1b. Test-pollution detector. The app-git feature's `ensure_repo` writes
#     "Initialize app repo" commits authored `Mobius <mobius@localhost>`.
#     Those belong only in per-app tmp repos; if they're among the commits
#     being pushed, an app-git test escaped its tmpdir and polluted HEAD
#     (.pm/096 — it pushed 18 such commits + a clobbered .gitignore to main
#     once already). Block them before they reach origin/main.
POLLUTION="$(git log --format='%an <%ae>' "$BASE"..HEAD 2>/dev/null \
  | grep -c 'Mobius <mobius@localhost>' || true)"
if [ "${POLLUTION:-0}" -gt 0 ]; then
  err "detected $POLLUTION Mobius-authored commit(s) on this branch — likely app-git test pollution:"
  git log --format='%h %an %s' "$BASE"..HEAD | grep 'Mobius' | sed 's/^/    /' >&2
  err "recover: git reset --hard origin/main  (drops the pollution); see scripts/git-doctor.sh"
  FAIL=1
fi

# 2. Python syntax on changed .py files (instant; no deps beyond python3).
PY="$(changed_list '\.py$')"
if [ -n "$PY" ]; then
  # Read into an array so a path with spaces survives — unquoted $PY would
  # word-split it, and a quoted "$PY" would pass the whole list as one arg.
  mapfile -t _py < <(printf '%s\n' "$PY")
  if python3 -m py_compile "${_py[@]}" 2>"$PP_TMP/py"; then
    ok "python syntax ok (${#_py[@]} file(s))"
  else
    err "python syntax errors:"; sed 's/^/    /' "$PP_TMP/py" >&2; FAIL=1
  fi
fi

# 3. JSX/JS parse on changed frontend files via esbuild (no import
#    resolution — pure syntax). Skipped if esbuild isn't installed.
ESB="$MAIN/frontend/node_modules/.bin/esbuild"
JSX="$(changed_list '^frontend/src/.*\.(jsx|js)$')"
if [ -n "$JSX" ] && [ -x "$ESB" ]; then
  jsx_fail=0
  while IFS= read -r f; do
    [ -z "$f" ] && continue
    # `--loader=jsx` (no extension) is stdin-only; for a file argument it
    # errors "loader without extension only applies when reading from stdin"
    # and false-fails EVERY frontend file. `--loader:.js=jsx` applies the jsx
    # loader to .js (and .jsx is inferred from its extension) for file args.
    "$ESB" "$f" --loader:.js=jsx --outfile=/dev/null >"$PP_TMP/esb" 2>&1 || {
      err "jsx parse error in $f:"; sed 's/^/    /' "$PP_TMP/esb" >&2; jsx_fail=1; }
  done <<< "$JSX"
  [ "$jsx_fail" -eq 0 ] && ok "jsx parse ok" || FAIL=1
fi

# 4. Frontend-unit — only when frontend changed (~6s). Mirrors the CI
#    frontend-unit job, so a logic regression is caught here in seconds.
if changed_match '^frontend/'; then
  if [ -d "$ROOT/frontend/node_modules" ]; then
    warn "running frontend-unit…"
    if (cd frontend && npm test >"$PP_TMP/fe" 2>&1); then
      ok "frontend-unit passed"
    else
      err "frontend-unit FAILED:"; tail -n 30 "$PP_TMP/fe" | sed 's/^/    /' >&2; FAIL=1
    fi
  else
    warn "frontend changed, no frontend/node_modules here (worktree?) — skipping frontend-unit (CI runs it)."
  fi
fi

# 4b. Static-app packager — this is a repo-root Node tool used by agents to
#     turn built third-party GitHub apps into Mobius `static_assets` packages.
#     It has no npm deps, so run it whenever its implementation/test or root
#     package script changes.
if changed_match '^(backend/scripts/package-static-app\.mjs|scripts/package-static-app\.test\.mjs|package\.json)$'; then
  if command -v node >/dev/null 2>&1; then
    warn "running static-app packager unit…"
    if npm run test:packager >"$PP_TMP/packager" 2>&1; then
      ok "static-app packager unit passed"
    else
      err "static-app packager unit FAILED:"
      tail -n 30 "$PP_TMP/packager" | sed 's/^/    /' >&2
      FAIL=1
    fi
  else
    warn "node not found — skipping static-app packager unit (CI runs it)."
  fi
fi

# 5. Backend pytest — only if a local venv exists AND backend changed.
#    No venv → skip with a one-time setup hint (CI still runs the suite).
if changed_match '^backend/'; then
  VENV="$MAIN/backend/.venv/bin/python"
  if [ -x "$VENV" ]; then
    warn "running backend pytest (full suite ~2m; bypass with --no-verify)…"
    # esbuild on PATH (compile tests shell out to it) + a throwaway
    # SECRET_KEY so Settings validates. The shared venv supplies deps; cwd
    # ($ROOT/backend) supplies the code under test.
    # GIT_CEILING_DIRECTORIES="$ROOT" stops git's upward repo discovery at the
    # repo root, so an app-git test (.pm/096) can't walk out of its tmpdir and
    # mutate THIS repo's .git (flip core.bare / append "Initialize app repo"
    # commits). Verified: it blocks enclosing-repo discovery from backend/ and
    # no backend test relies on implicit discovery of the enclosing repo.
    if (cd backend \
        && GIT_CEILING_DIRECTORIES="$ROOT" \
           PATH="$MAIN/frontend/node_modules/.bin:$PATH" \
           SECRET_KEY="${SECRET_KEY:-$(python3 -c 'import secrets;print(secrets.token_hex(32))')}" \
           "$VENV" -m pytest -q -p no:cacheprovider >"$PP_TMP/be" 2>&1); then
      ok "backend pytest passed"
    else
      err "backend pytest FAILED:"; tail -n 30 "$PP_TMP/be" | sed 's/^/    /' >&2; FAIL=1
    fi
  else
    warn "backend changed, no shared venv ($MAIN/backend/.venv) — skipping pytest (CI runs it)."
    warn "  enable: python3 -m venv \"$MAIN/backend/.venv\" \\"
    warn "    && \"$MAIN/backend/.venv/bin/pip\" install -r \"$MAIN/backend/requirements.txt\""
  fi
fi

if [ "$FAIL" -ne 0 ]; then
  err "push blocked. Fix the above, or bypass with:  git push --no-verify"
  exit 1
fi
ok "all gate checks passed — pushing"
exit 0
