#!/usr/bin/env bash
#
# build-lock.sh  -  the build queue: one xcodebuild or Gradle build at a time
# across every worktree on this machine.
#
# DerivedData, simulators and the Gradle daemon contend across parallel
# worktrees, so every ios/android build and test call sits between
# `acquire` and `release`. The lock is a directory, because `mkdir` is atomic.
# `owner` inside it names the task holding it on its first line and, when
# MA_BUILD_LOCK_PID is set, the process that holds it (`pid=<n>`). The owner
# file is written to a temporary name and renamed into place, so a reader sees
# either no owner or a complete one.
#
# A waiter takes the lock over only when its holder is gone: the recorded
# owner process no longer exists, the owner file is older than the stale limit,
# or there is no owner file and the lock directory is older than the grace
# window (the holder crashed between `mkdir` and writing its owner).
#
# Two separate calls, so the build itself stays a command the caller runs (and
# the unattended guard judges) on its own:
#   bash build-lock.sh acquire <task-id>
#   xcodebuild ... 2>&1 | tail -5
#   bash build-lock.sh release <task-id>
# `release` removes the lock only when <task-id> matches its owner.
#
# Environment: MA_BUILD_LOCK (default /tmp/claude-xcodebuild.lock),
# MA_BUILD_LOCK_STALE_SEC (default 900), MA_BUILD_LOCK_POLL_SEC (default 5),
# MA_BUILD_LOCK_GRACE_SEC (default 5), MA_BUILD_LOCK_PID (owner process, optional).
#
# Exit: 0 acquired or released, 1 release refused (lock held by another task),
# 2 usage.
set -uo pipefail

BUILD_LOCK="${MA_BUILD_LOCK:-/tmp/claude-xcodebuild.lock}"
STALE_SEC="${MA_BUILD_LOCK_STALE_SEC:-900}"
POLL_SEC="${MA_BUILD_LOCK_POLL_SEC:-5}"
GRACE_SEC="${MA_BUILD_LOCK_GRACE_SEC:-5}"

# GNU first: `stat -f` is a valid GNU flag (file-system status) that succeeds
# with the wrong answer, so BSD-first would silently win on Linux.
mtime() {
  stat -c "%Y" "$1" 2>/dev/null || stat -f "%m" "$1" 2>/dev/null || echo 0
}

owner_task() {
  head -n 1 "$BUILD_LOCK/owner" 2>/dev/null
}

owner_pid() {
  sed -n 's/^pid=\([0-9][0-9]*\)$/\1/p' "$BUILD_LOCK/owner" 2>/dev/null | head -n 1
}

# Prints the reason the current lock may be taken over, or nothing.
takeover_reason() {
  local now pid age
  now=$(date +%s)
  if [ ! -f "$BUILD_LOCK/owner" ]; then
    age=$((now - $(mtime "$BUILD_LOCK")))
    [ -d "$BUILD_LOCK" ] && [ "$age" -gt "$GRACE_SEC" ] && echo "no owner after ${age}s"
    return 0
  fi
  pid=$(owner_pid)
  if [ -n "$pid" ] && ! kill -0 "$pid" 2>/dev/null; then
    echo "owner process $pid is gone"
    return 0
  fi
  age=$((now - $(mtime "$BUILD_LOCK/owner")))
  [ "$age" -gt "$STALE_SEC" ] && echo "stale (${age}s)"
  return 0
}

write_owner() {
  local tmp="$BUILD_LOCK/.owner.$$"
  {
    printf '%s\n' "$1"
    if [ -n "${MA_BUILD_LOCK_PID:-}" ]; then printf 'pid=%s\n' "$MA_BUILD_LOCK_PID"; fi
  } >"$tmp" && mv -f "$tmp" "$BUILD_LOCK/owner"
}

case "${1:-}" in
  acquire)
    task="${2:-unknown}"
    while ! mkdir "$BUILD_LOCK" 2>/dev/null; do
      judged=$(cat "$BUILD_LOCK/owner" 2>/dev/null || echo "")
      reason=$(takeover_reason)
      if [ -n "$reason" ]; then
        current=$(cat "$BUILD_LOCK/owner" 2>/dev/null || echo "")
        if [ "$current" = "$judged" ]; then
          echo "Taking over the build lock: $reason"
          trash="$BUILD_LOCK.stale.$$"
          mv "$BUILD_LOCK" "$trash" 2>/dev/null && rm -rf "$trash"
        fi
        continue
      fi
      owner=$(owner_task)
      age=$(($(date +%s) - $(mtime "$BUILD_LOCK/owner")))
      echo "Build queue: waiting (${owner:-unknown}, ${age}s)"
      sleep "$POLL_SEC"
    done
    write_owner "$task"
    ;;
  release)
    task="${2:-}"
    if [ -z "$task" ]; then
      echo "usage: build-lock.sh release <task-id>" >&2
      exit 2
    fi
    [ -d "$BUILD_LOCK" ] || exit 0
    owner=$(owner_task)
    if [ "$owner" != "$task" ]; then
      echo "build lock is held by ${owner:-an unknown owner}, not $task; left in place" >&2
      exit 1
    fi
    rm -rf "$BUILD_LOCK"
    ;;
  *)
    echo "usage: build-lock.sh acquire <task-id> | release <task-id>" >&2
    exit 2
    ;;
esac
