#!/usr/bin/env bash
# Refuse a commit that touches a file you have not claimed on the board.
#
# The board cannot enforce a claim, because it never sees your editor. Git
# can, because every change passes through here. This is the difference
# between asking people to claim and requiring it.
#
# Bypassable with --no-verify, and hooks are not cloned with the repo, so
# treat this as a guard against accidents rather than a security boundary.
# The unbypassable layer is branch protection on the remote.
#
# Configure per clone:
#   git config switchboard.url   https://your-board.up.railway.app
#   git config switchboard.token YOUR_TOKEN
# Skip a path that nobody needs to claim:
#   git config --add switchboard.unclaimed 'docs/*'

URL=$(git config --get switchboard.url)
TOKEN=$(git config --get switchboard.token)
if [ -z "$URL" ] || [ -z "$TOKEN" ]; then
  echo "switchboard: not configured, skipping the claim check." >&2
  echo "  git config switchboard.url <board-url>" >&2
  echo "  git config switchboard.token <your-token>" >&2
  exit 0
fi

# D as well. Deleting a file somebody else holds a claim on is the most
# destructive change available and it was the one the hook waved through,
# because ACMR does not include a deletion.
staged=$(git diff --cached --name-only --diff-filter=ACMRD)
[ -n "$staged" ] || exit 0

me=$(curl -s -m 10 "$URL/api/whoami" -H "Authorization: Bearer $TOKEN" \
     | python3 -c 'import json,sys; print(json.load(sys.stdin).get("agent",""))' 2>/dev/null)
if [ -z "$me" ]; then
  # A board that cannot be reached must not silently stop enforcing. Refuse,
  # and make the operator decide, rather than letting an unclaimed commit
  # through because the network blinked.
  echo "switchboard: cannot reach the board to check claims. Commit refused." >&2
  echo "  Retry, or use --no-verify if you are certain and will say so on the board." >&2
  exit 1
fi

state=$(curl -s -m 10 "$URL/api/state" -H "Authorization: Bearer $TOKEN")
# Not mapfile: that is bash 4, and macOS ships bash 3.2, where it fails with
# "command not found" and leaves the array empty. The hook then kept running
# with no skip patterns at all, so switchboard.unclaimed silently did nothing.
skips=()
while IFS= read -r line; do
  [ -n "$line" ] && skips+=("$line")
done < <(git config --get-all switchboard.unclaimed)

unclaimed=$(python3 - "$me" "$state" "$staged" ${skips[@]+"${skips[@]}"} <<'PY'
import json, sys, fnmatch
me, raw, staged = sys.argv[1], sys.argv[2], sys.argv[3]
skips = sys.argv[4:]
try:
    claims = json.loads(raw).get("claims", [])
except Exception:
    print("PARSE_FAIL"); raise SystemExit
def norm(p):
    """The same spelling the board uses.

    Kept in step with switchboard_mcp.events.normalise_task and the _norm in
    hooks/pretooluse.py. This layer compared raw strings while the other two
    normalised, so three spellings the board accepted and the edit hook
    allowed were refused here: "./src/m.py", "SRC/m.py" and an absolute path.
    You claim it, your editor lets you edit it, and then the commit is
    refused. That reads as the tool being broken, and it is the last place
    anybody would look.
    """
    t = str(p).strip().replace("\\", "/")
    parts = []
    for seg in t.split("/"):
        if seg in ("", "."):
            continue
        if seg == ".." and parts and parts[-1] != "..":
            parts.pop()
            continue
        parts.append(seg)
    return "/".join(parts).lower()


def covered(task, path):
    """A claim covers a path if it names it or a directory above it.

    Compared segment by segment, so "leg1" does not swallow "leg10", which a
    startswith on the raw string did.
    """
    a, b = norm(task).split("/"), norm(path).split("/")
    n = min(len(a), len(b))
    if a[:n] == b[:n]:
        return True
    # One side absolute and the other repo-relative. Anchored on segments, so
    # "model.py" alone does not match "other/model.py".
    long_p, short_p = (a, b) if len(a) >= len(b) else (b, a)
    return len(short_p) > 1 and long_p[-len(short_p):] == short_p


mine = {c["task"] for c in claims if c.get("holder") == me and c.get("valid")}
bad = []
for path in staged.splitlines():
    if not path.strip():
        continue
    if any(fnmatch.fnmatch(path, s) for s in skips if s):
        continue
    if any(covered(c, path) for c in mine):
        continue
    bad.append(path)
print("\n".join(bad))
PY
)

if [ "$unclaimed" = "PARSE_FAIL" ]; then
  echo "switchboard: could not read the board state. Commit refused." >&2
  exit 1
fi

if [ -n "$unclaimed" ]; then
  echo "" >&2
  echo "switchboard: commit refused. You have not claimed these:" >&2
  echo "$unclaimed" | sed 's/^/  /' >&2
  echo "" >&2
  echo "Someone else may be editing them. Claim them first:" >&2
  echo "$unclaimed" | head -3 | sed 's|^|  /claim |' >&2
  echo "" >&2
  echo "Or, if nobody could possibly collide on it:" >&2
  echo "  git config --add switchboard.unclaimed '<pattern>'" >&2
  echo "" >&2
  exit 1
fi
exit 0
