#!/usr/bin/env bash
# Task 560 — emit subagent hook decisions to a sidecar buffer that the
# parent-side post-tool-use-agent.sh propagator reads on its next fire.
#
# This library is fail-open: every error path is logged to server.log
# under [hook-propagate-error] and returns success so the hook's primary
# allow/block contract is never coupled to propagation succeeding.

: "${MAXY_HOOK_BUFFER_DIR:=$HOME/.maxy-code/logs/hook-decisions}"
: "${MAXY_HOOK_SERVER_LOG:=$HOME/.maxy-code/logs/server.log}"
: "${MAXY_HOOK_STDERR_CAP:=4096}"
# Task 563 — mkdir-based mutex serialises emitter append vs drainer rename.
# 100 attempts × 10ms = ~1s ceiling. Portable across Linux (Pi) and macOS
# (tests) without flock(1).
: "${MAXY_HOOK_LOCK_ATTEMPTS:=100}"
: "${MAXY_HOOK_LOCK_SLEEP:=0.01}"
# Task 569 — TTL for orphaned lockdirs left behind when a holder dies
# between `mkdir lock` and `rmdir lock` (SIGKILL, OOM, systemd restart).
# Default 5s is far longer than the legitimate hold (microseconds for a
# rename or append) and shorter than operator-noticeable propagation lag.
: "${MAXY_HOOK_LOCK_TTL_SECONDS:=5}"

# _hook_lockdir_age_seconds <lockdir> — prints integer age in seconds, or
# -1 if the lockdir cannot be stat'd. Portable across Linux (`stat -c`)
# and macOS (`stat -f`).
_hook_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))
}

# _hook_acquire_lock <lockdir> — returns 0 on success, 1 on timeout.
# On each mkdir failure, if the lockdir is older than
# MAXY_HOOK_LOCK_TTL_SECONDS, the orphaned holder is presumed dead: the
# lockdir is force-removed, a stale-lock-reclaimed observability line is
# emitted, and one mkdir retry is attempted before resuming the spin.
_hook_acquire_lock() {
  local lockdir="$1" i=0 age
  while ! mkdir "$lockdir" 2>/dev/null; do
    age=$(_hook_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
}

# _hook_release_lock <lockdir>
_hook_release_lock() {
  rmdir "$1" 2>/dev/null || true
}

# hook_emit_decision <agentId> <hookName> <toolName> <decision> <reason>
#                    <stderrPayload> <exitCode> <durationMs>
hook_emit_decision() {
  local agentId="$1" hookName="$2" toolName="$3" decision="$4" reason="$5"
  local stderrPayload="$6" exitCode="$7" durationMs="$8"

  local rec
  rec=$(MAXY_HOOK_STDERR_CAP="$MAXY_HOOK_STDERR_CAP" python3 - \
    "$agentId" "$hookName" "$toolName" "$decision" "$reason" \
    "$stderrPayload" "$exitCode" "$durationMs" <<'PY' 2>/dev/null
import json, os, sys, datetime
cap = int(os.environ.get("MAXY_HOOK_STDERR_CAP", "4096"))
(agentId, hookName, toolName, decision, reason,
 stderrPayload, exitCode, durationMs) = sys.argv[1:9]
truncated = False
if len(stderrPayload) > cap:
    dropped = len(stderrPayload) - cap
    stderrPayload = stderrPayload[:cap] + f"…[truncated {dropped} bytes]"
    truncated = True
print(json.dumps({
    "ts": datetime.datetime.utcnow().replace(microsecond=0).isoformat() + "Z",
    "agentId": agentId,
    "hookName": hookName,
    "toolName": toolName,
    "decision": decision,
    "reason": reason,
    "stderr": stderrPayload,
    "exitCode": int(exitCode) if exitCode.lstrip("-").isdigit() else 0,
    "durationMs": int(durationMs) if durationMs.lstrip("-").isdigit() else 0,
    "truncated": truncated,
}, separators=(",", ":")))
PY
  )
  if [[ -z "$rec" ]]; then
    mkdir -p "$(dirname "$MAXY_HOOK_SERVER_LOG")" 2>/dev/null
    printf '[hook-propagate-error] reason=record-build agentId=%s hook=%s\n' \
      "$agentId" "$hookName" >>"$MAXY_HOOK_SERVER_LOG" 2>/dev/null
    return 0
  fi

  mkdir -p "$MAXY_HOOK_BUFFER_DIR" 2>/dev/null
  local buf="$MAXY_HOOK_BUFFER_DIR/$agentId.jsonl"
  local lock="${buf}.lock"
  if ! _hook_acquire_lock "$lock"; then
    mkdir -p "$(dirname "$MAXY_HOOK_SERVER_LOG")" 2>/dev/null
    printf '[hook-propagate-error] reason=lock-timeout agentId=%s hook=%s\n' \
      "$agentId" "$hookName" >>"$MAXY_HOOK_SERVER_LOG" 2>/dev/null
    return 0
  fi
  if ! printf '%s\n' "$rec" >>"$buf" 2>/dev/null; then
    _hook_release_lock "$lock"
    mkdir -p "$(dirname "$MAXY_HOOK_SERVER_LOG")" 2>/dev/null
    printf '[hook-propagate-error] reason=buffer-write agentId=%s hook=%s\n' \
      "$agentId" "$hookName" >>"$MAXY_HOOK_SERVER_LOG" 2>/dev/null
    return 0
  fi
  _hook_release_lock "$lock"

  local bytesIn="${#stderrPayload}"
  local bytesOut="${#rec}"
  local trunc=false
  [[ "$rec" == *'"truncated":true'* ]] && trunc=true
  mkdir -p "$(dirname "$MAXY_HOOK_SERVER_LOG")" 2>/dev/null
  printf '[hook-propagate] agentId=%s hookName=%s tool=%s decision=%s bytesIn=%s bytesOut=%s truncated=%s\n' \
    "$agentId" "$hookName" "$toolName" "$decision" "$bytesIn" "$bytesOut" "$trunc" \
    >>"$MAXY_HOOK_SERVER_LOG" 2>/dev/null
  return 0
}

# hook_block_with_emit <agentId> <hookName> <toolName> <reason>
#                      <userMessage> [durationMs]
# Convenience wrapper for any PreToolUse hook that records a block decision:
# emits the propagation record, echoes the user-facing block message to stderr,
# and exits 2.
hook_block_with_emit() {
  local agentId="$1" hookName="$2" toolName="$3" reason="$4"
  local userMessage="$5" durationMs="${6:-0}"
  hook_emit_decision "$agentId" "$hookName" "$toolName" "block" "$reason" \
    "$userMessage" 2 "$durationMs"
  printf '%s\n' "$userMessage" >&2
  exit 2
}
