#!/usr/bin/env bash
# buildbox-parity.sh — record the fleet's read-only ~/.claude runtime verdict for the deck.
#
# Run by systemd-user timer. Runtime apply is owned by the release reconciler; this probe
# never bootstraps, repairs, relinks, or otherwise mutates a buildbox runtime.
#
# Anti-spam: one message per crossing into drift. A host that has already been announced
# never announces again until it comes back converged. `unreachable` carries no
# information about config and never changes what has been announced.
set -uo pipefail

SELF="$(readlink -f "${BASH_SOURCE[0]}")"
REPO="${SELF%/modules/workstation/claude/bin/*}"
BUILDBOX="${BUILDBOX_BIN:-$REPO/modules/buildbox/bin/buildbox}"
STATE_DIR="${PARITY_STATE_DIR:-$HOME/.claude/run/buildbox-parity}"
STATE_FILE="$STATE_DIR/state.json"
LOG_FILE="${PARITY_LOG_FILE:-$HOME/.claude/buildbox-parity.log}"
NOTIFY_CMD="${PARITY_NOTIFY_CMD:-notify-send}"

mkdir -p "$STATE_DIR"

# Serialize state writes when a manual probe overlaps the timer.
exec 9>"$STATE_DIR/converge.lock"
flock -n 9 || exit 0

log() { printf '%s %s\n' "$(date -Is)" "$*" >>"$LOG_FILE"; }

declare -A VERDICT DETAIL
SEEN=0

# `buildbox claude-parity` prints one line per host, host first. Its exit code cannot tell
# drifted from unreachable — both are 1 — so the verdict comes from the line.
read_verdicts() {
  local host rest
  while read -r host rest; do
    [ -n "$host" ] || continue
    SEEN=$((SEEN + 1))
    case "$rest" in
      converged*)   VERDICT["$host"]=converged ;;
      drifted*)     VERDICT["$host"]=drifted ;;
      unreachable*) VERDICT["$host"]=unreachable ;;
      *)            VERDICT["$host"]=error ;;
    esac
    DETAIL["$host"]="$rest"
  done
}

RUNTIME_DESCRIPTOR="${PARITY_RUNTIME_DESCRIPTOR:-}"
RUNTIME_RELEASE_ROOT="${PARITY_RUNTIME_RELEASE_ROOT:-}"
if [ -z "$RUNTIME_DESCRIPTOR" ] || [ -z "$RUNTIME_RELEASE_ROOT" ]; then
  log "runtime descriptor selection not configured — leaving the recorded state untouched"
  exit 3
fi

read_verdicts < <("$BUILDBOX" claude-parity \
  --runtime-descriptor "$RUNTIME_DESCRIPTOR" \
  --runtime-release-root "$RUNTIME_RELEASE_ROOT" \
  2>>"$LOG_FILE")

if [ "$SEEN" -eq 0 ]; then
  log "probe produced no host lines — leaving the recorded state untouched"
  exit 3
fi

readarray -t HOSTS < <(printf '%s\n' "${!VERDICT[@]}" | LC_ALL=C sort)

converge_at=""

probed_at="$(date -Is)"
announce=()
notify=()
for h in "${HOSTS[@]}"; do
  prev="$(jq -r --arg h "$h" '.announced[$h] // ""' "$STATE_FILE" 2>/dev/null)"
  case "${VERDICT[$h]}" in
    converged)   tier=ok ;;
    unreachable) tier="$prev" ;;
    *)           tier=bad ;;
  esac
  if [ "$tier" = bad ] && [ "$prev" != bad ]; then notify+=("$h"); fi
  if [ "$tier" != "$prev" ]; then log "$h ${prev:-unknown} -> ${tier:-unknown} (${DETAIL[$h]})"; fi
  announce+=("$h	$tier")
done

{
  for h in "${HOSTS[@]}"; do
    printf 'host\t%s\t%s\t%s\n' "$h" "${VERDICT[$h]}" "${DETAIL[$h]}"
  done
  printf 'announced\t%s\n' "${announce[@]}"
} | jq -Rn --arg probedAt "$probed_at" --arg convergedAt "$converge_at" '
  [inputs | split("\t")] as $rows
  | {
      schemaVersion: 1,
      probedAt: $probedAt,
      lastConvergeAt: (if $convergedAt == "" then null else $convergedAt end),
      hosts: ($rows | map(select(.[0] == "host"))
              | map({ key: .[1], value: { verdict: .[2], detail: .[3], probedAt: $probedAt } })
              | from_entries),
      announced: ($rows | map(select(.[0] == "announced") | select(.[2] != ""))
              | map({ key: .[1], value: .[2] }) | from_entries),
    }' >"$STATE_FILE.tmp" && mv "$STATE_FILE.tmp" "$STATE_FILE"

if [ "${#notify[@]}" -gt 0 ]; then
  "$NOTIFY_CMD" -u normal -i network-error "Buildbox config drift" \
    "${notify[*]} runtime drifted — apply a validated runtime release; see $LOG_FILE" 2>/dev/null || true
fi
