#!/usr/bin/env bash
# Task 560 — PostToolUse hook scoped to tool_name=Agent in the admin role.
# Drains every subagent hook-decision buffer in the dir, prints each record
# as a [hook-propagate] line to stdout (Claude Code attaches the stdout to
# the parent JSONL as a hook_success attachment), and emits one
# [hook-propagate-census] line. N != M in the census is the propagation
# regression signal.
#
# Task 563 — concurrency hardening:
#   - Atomic claim before read. Each buffer is renamed under a mkdir mutex
#     to a per-fire `claimed/<basename>-<epoch>-<pid>.jsonl` path. The lock
#     is held only for the rename syscall; reading happens against the
#     renamed inode, which no emitter can address. The race window between
#     the drainer's read and the buffer rotation is closed.
#   - No cursor. The cursor file was load-bearing only because consumed
#     buffers could be re-seen; with atomic claim the file's presence in
#     the directory is the unambiguous signal. Dropping the cursor closes
#     two further races (concurrent-drainer cursor write, shared
#     `.unknown.cursor` across broken-stdin fires).
#   - Unknown-parent observability. When stdin's `session_id` is missing
#     or unparseable, an explicit `[hook-propagate] parentSession=unknown
#     reason=session-id-missing` line is emitted to both stdout and
#     server.log so the broken-stdin path is no longer silent.

set -uo pipefail

: "${MAXY_HOOK_BUFFER_DIR:=$HOME/.maxy-code/logs/hook-decisions}"
: "${MAXY_HOOK_SERVER_LOG:=$HOME/.maxy-code/logs/server.log}"
: "${MAXY_HOOK_LOCK_ATTEMPTS:=100}"
: "${MAXY_HOOK_LOCK_SLEEP:=0.01}"
# Task 569 — TTL for orphaned lockdirs (see hook-emit.sh for rationale).
: "${MAXY_HOOK_LOCK_TTL_SECONDS:=5}"

if [ -t 0 ]; then
  # No stdin — no correlation possible. Fail-open.
  exit 0
fi
INPUT=$(cat)
PARENT_SESSION=$(printf '%s' "$INPUT" | python3 -c '
import sys, json
try:
    sid = json.load(sys.stdin).get("session_id") or ""
    print(sid if sid else "unknown")
except Exception:
    print("unknown")
' 2>/dev/null || echo "unknown")

if [[ "$PARENT_SESSION" == "unknown" ]]; then
  # Conservative play (Task 563 brief option C): emit observability and
  # exit without draining. A broken-stdin fire has no business processing
  # buffers — the next valid parent fire will pick them up.
  mkdir -p "$(dirname "$MAXY_HOOK_SERVER_LOG")" 2>/dev/null
  printf '[hook-propagate] parentSession=unknown reason=session-id-missing\n'
  printf '[hook-propagate] parentSession=unknown reason=session-id-missing\n' \
    >>"$MAXY_HOOK_SERVER_LOG" 2>/dev/null || true
  exit 0
fi

mkdir -p "$MAXY_HOOK_BUFFER_DIR/claimed" "$MAXY_HOOK_BUFFER_DIR/consumed" 2>/dev/null

# Sweep orphaned `.<session>.cursor` files left behind by an older
# version of this script. The cursor mechanism is gone; without this
# they would persist forever as dir noise.
rm -f "$MAXY_HOOK_BUFFER_DIR"/.*.cursor 2>/dev/null || true

NOW_EPOCH=$(date +%s)
N_OBSERVED=0
N_EMITTED=0

# Task 569 — same stale-lockdir TTL reclamation as hook-emit.sh.
_drain_lockdir_age_seconds() {
  local lockdir="$1" mtime now
  mtime=$(stat -c %Y "$lockdir" 2>/dev/null || stat -f %m "$lockdir" 2>/dev/null)
  [[ -z "$mtime" ]] && { echo -1; return; }
  now=$(date +%s)
  echo $((now - mtime))
}

_drain_acquire_lock() {
  local lockdir="$1" i=0 age
  while ! mkdir "$lockdir" 2>/dev/null; do
    age=$(_drain_lockdir_age_seconds "$lockdir")
    if [[ "$age" -ge "$MAXY_HOOK_LOCK_TTL_SECONDS" ]]; then
      rmdir "$lockdir" 2>/dev/null || rm -rf "$lockdir" 2>/dev/null
      mkdir -p "$(dirname "$MAXY_HOOK_SERVER_LOG")" 2>/dev/null
      printf '[hook-propagate-error] reason=stale-lock-reclaimed lockdir=%s ageSeconds=%s\n' \
        "$lockdir" "$age" >>"$MAXY_HOOK_SERVER_LOG" 2>/dev/null || true
      mkdir "$lockdir" 2>/dev/null && return 0
    fi
    i=$((i+1))
    [[ "$i" -ge "$MAXY_HOOK_LOCK_ATTEMPTS" ]] && return 1
    sleep "$MAXY_HOOK_LOCK_SLEEP" 2>/dev/null || true
  done
  return 0
}

shopt -s nullglob
for buf in "$MAXY_HOOK_BUFFER_DIR"/*.jsonl; do
  [[ -f "$buf" ]] || continue
  case "$(basename "$buf")" in
    .*) continue;;
  esac
  lock="${buf}.lock"
  if ! _drain_acquire_lock "$lock"; then
    mkdir -p "$(dirname "$MAXY_HOOK_SERVER_LOG")" 2>/dev/null
    printf '[hook-propagate-error] reason=lock-timeout parentSession=%s buffer=%s\n' \
      "$PARENT_SESSION" "$(basename "$buf")" >>"$MAXY_HOOK_SERVER_LOG" 2>/dev/null || true
    continue
  fi
  # Re-check existence under lock — a peer drainer may have claimed it
  # between our `for` iteration and our lock acquisition.
  if [[ ! -f "$buf" ]]; then
    rmdir "$lock" 2>/dev/null || true
    continue
  fi
  CLAIMED="$MAXY_HOOK_BUFFER_DIR/claimed/$(basename "$buf" .jsonl)-${NOW_EPOCH}-$$.jsonl"
  if ! mv "$buf" "$CLAIMED" 2>/dev/null; then
    rmdir "$lock" 2>/dev/null || true
    mkdir -p "$(dirname "$MAXY_HOOK_SERVER_LOG")" 2>/dev/null
    printf '[hook-propagate-error] reason=claim-mv parentSession=%s buffer=%s\n' \
      "$PARENT_SESSION" "$(basename "$buf")" >>"$MAXY_HOOK_SERVER_LOG" 2>/dev/null || true
    continue
  fi
  rmdir "$lock" 2>/dev/null || true

  # Ground-truth observed count: non-empty lines in the claimed file. The
  # printf-success N_EMITTED is incremented independently below, so a
  # printf failure (SIGPIPE on a closed downstream pipe, EIO on stdout)
  # produces N_OBSERVED > N_EMITTED — the propagation regression signal
  # the census promises. The two counters must not be bumped together.
  BUF_LINES=$(grep -c . "$CLAIMED" 2>/dev/null || echo 0)
  N_OBSERVED=$((N_OBSERVED + BUF_LINES))
  while IFS= read -r line || [[ -n "$line" ]]; do
    [[ -z "$line" ]] && continue
    if printf '[hook-propagate] %s\n' "$line" 2>/dev/null; then
      N_EMITTED=$((N_EMITTED+1))
    fi
  done <"$CLAIMED"
  TARGET="$MAXY_HOOK_BUFFER_DIR/consumed/$(basename "$CLAIMED")"
  if ! mv "$CLAIMED" "$TARGET" 2>/dev/null; then
    mkdir -p "$(dirname "$MAXY_HOOK_SERVER_LOG")" 2>/dev/null
    printf '[hook-propagate-error] reason=consumed-mv parentSession=%s buffer=%s\n' \
      "$PARENT_SESSION" "$(basename "$CLAIMED")" >>"$MAXY_HOOK_SERVER_LOG" 2>/dev/null || true
  fi
done
shopt -u nullglob

printf '[hook-propagate-census] parentSession=%s subagentHooksObserved=%d attachmentsEmitted=%d\n' \
  "$PARENT_SESSION" "$N_OBSERVED" "$N_EMITTED"

mkdir -p "$(dirname "$MAXY_HOOK_SERVER_LOG")" 2>/dev/null
printf '[hook-propagate-census] parentSession=%s subagentHooksObserved=%d attachmentsEmitted=%d\n' \
  "$PARENT_SESSION" "$N_OBSERVED" "$N_EMITTED" >>"$MAXY_HOOK_SERVER_LOG" 2>/dev/null || true

exit 0
