#!/usr/bin/env bash
# H13 (ersatztv#541) — session-end: leave the SHARED checkout at /Users/timothy/ersatztv fresh, so
# the next session's kickoff paste is current.
#
# Why this is a script and not a rule. The standing lore already says the shared tree is not a `main`
# mirror and its HEAD must never be read as truth — but on 2026-07-21 the trap arrived through a
# route that rule doesn't cover: a session pasted `docs/handoffs/chicorytv-issue-queue.md` FROM that
# tree, which was 81 commits behind, and the file still described a queue protocol retired the day
# before (read tracker #237 — since closed, and now explicitly "do not read for queue state"). No
# `git log` was consulted, so no discipline check would have fired. A stale checkout silently
# serves stale FILES, and the only durable fix is to stop it being stale.
#
# What it does: fast-forwards the shared tree to origin/main, and reinstalls web/node_modules when
# the lockfile moved (worktrees copy that directory; a stale copy has broken typecheck before).
#
# What it will NOT do — it is deliberately timid, because the tree is shared and another session may
# be mid-flight in it. It refuses (loudly, exit 0) and leaves the WORKING TREE untouched when it is
# not on `main`, is dirty, has local commits, or is mid-rebase/merge. It never switches branches,
# never stashes, never discards. (A refusal reached after the fetch will have advanced
# refs/remotes/origin/main — remote-tracking refs only, never your files.) A refusal is a normal
# outcome, not a failure.
#
# Run at session end (kickoff session-end protocol). Safe to run at session start too — it is
# idempotent. Fail-OPEN: a missing/unreachable tree prints a notice and exits 0.
#
# ETV_SHARED_CHECKOUT overrides the target directory.
set -uo pipefail

shared="${ETV_SHARED_CHECKOUT:-/Users/timothy/ersatztv}"

skip() {
  echo "refresh-shared-checkout: NO-OP — $1"
  echo "  (nothing was changed)"
  exit 0
}

[ -d "$shared/.git" ] || skip "no git checkout at $shared"

cd "$shared" || skip "cannot cd to $shared"

# $shared/.git existing is NOT proof $shared is the repo root: if it is an invalid/emptied .git dir
# and some ancestor IS a repo, git walks UP and every command below would silently operate on that
# ancestor instead — the worst possible outcome for a script that fast-forwards and reinstalls.
toplevel="$(git rev-parse --show-toplevel 2>/dev/null)" || skip "not a git repo: $shared"
[ "$toplevel" = "$(pwd -P)" ] || skip "$shared is not a repo root (git resolves it to $toplevel)"

# mid-operation? never interfere
git_dir="$(git rev-parse --git-dir 2>/dev/null)" || skip "not a git repo: $shared"
# NOTE: keep the `|| true` — without it this loop's `&&` tail would abort the script if anyone
# ever adds `set -e` (a non-match returns 1 as the body's last status).
for state in rebase-merge rebase-apply MERGE_HEAD CHERRY_PICK_HEAD BISECT_LOG REVERT_HEAD; do
  { [ -e "$git_dir/$state" ] && skip "a $state is in progress"; } || true
done

branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null)"
[ "$branch" = "main" ] || skip "on branch '$branch', not main — that is another session's work"

if [ -n "$(git status --porcelain 2>/dev/null)" ]; then
  skip "working tree is dirty (uncommitted changes)"
fi

git fetch origin main --quiet || skip "could not fetch origin/main"

ahead="$(git rev-list --count origin/main..HEAD 2>/dev/null)" || skip "cannot compare against origin/main"
behind="$(git rev-list --count HEAD..origin/main 2>/dev/null)" || skip "cannot compare against origin/main"

if [ "$ahead" != "0" ]; then
  skip "$ahead local commit(s) not on origin/main — someone committed here; resolve by hand"
fi

if [ "$behind" = "0" ]; then
  echo "refresh-shared-checkout: already current with origin/main ($(git rev-parse --short HEAD))."
  exit 0
fi

lock_before=""
[ -f web/package-lock.json ] && lock_before="$(git hash-object web/package-lock.json 2>/dev/null)"

if ! git merge --ff-only origin/main --quiet; then
  skip "fast-forward to origin/main failed"
fi

echo "refresh-shared-checkout: fast-forwarded $behind commit(s) to $(git rev-parse --short HEAD)."

lock_after=""
[ -f web/package-lock.json ] && lock_after="$(git hash-object web/package-lock.json 2>/dev/null)"

if [ -n "$lock_after" ] && [ "$lock_before" != "$lock_after" ]; then
  echo "refresh-shared-checkout: web/package-lock.json changed — refreshing node_modules..."
  # `npm ci`, not `npm install`: install can REWRITE the lockfile, which would leave this shared
  # tree dirty — and a dirty tree is exactly what the next run (and every worktree-hygiene check)
  # refuses on. ci installs strictly from the lockfile and never writes it.
  # Accepted trade-off: `ci` DELETES node_modules first, so a session copying it at that instant gets
  # a partial copy. Bounded and rare — everything removed is regenerable from the lockfile, no human
  # work can be lost, and this only runs on a merge that actually moved the lockfile.
  if (cd web && npm ci --no-audit --no-fund >/dev/null 2>&1); then
    echo "refresh-shared-checkout: node_modules refreshed."
  else
    echo "refresh-shared-checkout: WARNING — npm ci failed; worktrees copying web/node_modules"
    echo "  from here may fail typecheck. Run 'npm ci' in $shared/web by hand."
  fi
fi

# belt and braces: leaving the shared tree dirty is the one outcome that would make this script a
# net negative, so assert it and say so loudly rather than exiting quietly on a bad state
if [ -n "$(git status --porcelain 2>/dev/null)" ]; then
  echo "refresh-shared-checkout: WARNING — the shared tree is DIRTY after refresh:"
  git status --short | sed 's/^/    /'
  echo "  Nothing was discarded. Clean it by hand before the next session pastes files from here."
fi
