#!/usr/bin/env bash
# shim-drift-check — fail loudly when the PATH shims running on this machine are not
# the ones landed on origin/main.
#
# The guards are executed from ~/.claude/bin (a symlink into the deploy clone) while
# they are edited in a checkout, so "we fixed it" can be true of one copy while the
# machine still runs another. Canonical is landed main, never a working tree: an
# unlanded edit is drift by definition. Every checkout is checked too, against its own
# HEAD: an uncommitted shim edit is a copy no deploy will ever install.
#
# Usage: shim-drift-check
# Exit: 0 clean, 1 the LIVE shims drift, 3 only checkout copies carry uncommitted shim
# edits, 2 cannot determine.
# Env: OD_SHIM_LIVE_DIR (default ~/.claude/bin), OD_SHIM_REPO (default: the git repo
# holding this script), OD_SHIM_REF (default origin/main), OD_SHIM_REPOS_FILE (default
# ~/.local/state/overdeck/shim-repos, written by od-worktree).
set -uo pipefail

LIVE_DIR="${OD_SHIM_LIVE_DIR:-$HOME/.claude/bin}"
REF="${OD_SHIM_REF:-origin/main}"
SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO="${OD_SHIM_REPO:-$(git -C "$SELF_DIR" rev-parse --show-toplevel 2>/dev/null)}"
REPOS_FILE="${OD_SHIM_REPOS_FILE:-$HOME/.local/state/overdeck/shim-repos}"

GUARDED=(
  _git-guard-shim.sh
  _cpu-guard-shim.sh
  _tmpjail-shim.sh
  _agent-session-tmux
  _kill-guard-shim.sh
  install-git-guard-real
  shim-drift-check
  generate-tool-shims
  install-tool-shims-real
  ccr
  podman
  systemctl
)
LIB_GUARDED=(shim-guard.sh human-session.tmux.conf tool-shims.snapshot.json)

REL_BIN=modules/workstation/claude/bin
REL_LIB=modules/workstation/claude/lib

live_drift=0
copy_drift=0
BAND=live
report() {
  if [[ "$BAND" == live ]]; then live_drift=1; else copy_drift=1; fi
  echo "shim-drift($BAND): $1" >&2
}

# A linked worktree's .git is a file, not a directory, and the agent most likely to be
# holding an uncommitted shim edit is running inside one.
[[ -n "$REPO" && -e "$REPO/.git" ]] || {
  echo "shim-drift-check: no git repo for $SELF_DIR (set OD_SHIM_REPO)" >&2
  exit 2
}
git -C "$REPO" rev-parse --verify --quiet "$REF" >/dev/null || {
  echo "shim-drift-check: ref $REF not found in $REPO" >&2
  exit 2
}

compare() { # $1=repo-relative path $2=live path
  local rel="$1" live="$2" tmp
  if [[ ! -f "$live" ]]; then
    report "missing on this machine: $live"
    return
  fi
  tmp=$(mktemp) || {
    echo "shim-drift-check: mktemp failed" >&2
    exit 2
  }
  if ! git -C "$REPO" show "$REF:$rel" >"$tmp" 2>/dev/null; then
    report "$rel is not present in $REF (unlanded file running live at $live)"
    rm -f "$tmp"
    return
  fi
  cmp -s "$tmp" "$live" || report "$live differs from $REF:$rel"
  rm -f "$tmp"
}

for f in "${GUARDED[@]}"; do compare "$REL_BIN/$f" "$LIVE_DIR/$f"; done
for f in "${LIB_GUARDED[@]}"; do compare "$REL_LIB/$f" "$LIVE_DIR/../lib/$f"; done

# A checkout holding an uncommitted shim edit holds an executable copy that an agent
# working there can run and that no deploy will ever install, so every checkout is
# checked too. The comparison is against that checkout's own HEAD, not $REF: a clean
# checkout parked on an older commit necessarily holds the older file, and reporting
# that would make this band fire for every unrebased worktree forever.
BAND=copy

compare_checkout() { # $1=repo-relative path $2=worktree root
  local rel="$1" wt="$2" tmp path
  path="$wt/$rel"
  tmp=$(mktemp) || {
    echo "shim-drift-check: mktemp failed" >&2
    exit 2
  }
  if ! git -C "$wt" show "HEAD:$rel" >"$tmp" 2>/dev/null; then
    report "$path is not committed in its own checkout (unlanded shim runnable from $wt)"
    rm -f "$tmp"
    return
  fi
  cmp -s "$tmp" "$path" || report "$path carries an uncommitted edit its checkout's HEAD does not have"
  rm -f "$tmp"
}
scanned=()
scan_repo_worktrees() {
  local repo="$1" line wt seen key status rel
  if ! git -C "$repo" rev-parse --git-dir >/dev/null 2>&1; then
    report "registered checkout is not a git repo: $repo"
    return 0
  fi
  # Keyed on the shared git dir, not the path: a linked worktree and the checkout that
  # owns it are different paths onto the same set of worktrees.
  key=$(git -C "$repo" rev-parse --path-format=absolute --git-common-dir 2>/dev/null) || key="$repo"
  for seen in ${scanned[@]+"${scanned[@]}"}; do
    [[ "$seen" == "$key" ]] && return 0
  done
  scanned+=("$key")
  while IFS= read -r line; do
    [[ "$line" == worktree\ * ]] || continue
    wt="${line#worktree }"
    status=$(git -C "$wt" status --porcelain --untracked-files=all -- \
      "${GUARDED[@]/#/$REL_BIN/}" "${LIB_GUARDED[@]/#/$REL_LIB/}" 2>/dev/null) || {
      report "cannot inspect registered checkout: $wt"
      continue
    }
    while IFS= read -r rel; do
      [[ -n "$rel" ]] || continue
      case "$rel" in
        "$REL_BIN/"*|"$REL_LIB/"*) compare_checkout "$rel" "$wt" ;;
      esac
    done < <(printf '%s\n' "$status" | cut -c4-)
  done < <(git -C "$repo" worktree list --porcelain 2>/dev/null)
}

scan_repo_worktrees "$REPO"
# The live shims run from a deploy clone that has no link back to the dev checkouts, so
# od-worktree registers every checkout it acts on here.
if [[ -r "$REPOS_FILE" ]]; then
  while IFS= read -r other; do
    [[ -n "$other" ]] || continue
    scan_repo_worktrees "$other"
  done <"$REPOS_FILE"
else
  report "no checkout registry at $REPOS_FILE — checkout copies outside $REPO were not checked"
fi

if ((live_drift)); then
  echo "shim-drift-check: FAILED — the shims this machine executes are not $REF; land the change, then run packaging/deploy-local.sh" >&2
  exit 1
fi
if ((copy_drift)); then
  echo "shim-drift-check: HAZARD — the live shims match $REF but the checkout copies above carry uncommitted shim edits; an agent running one of those trees runs a shim no deploy will install" >&2
  exit 3
fi
echo "shim-drift-check: clean (live shims match $REF; no checkout carries an uncommitted shim edit)"
