#!/usr/bin/env bash
# status-snapshot.sh — OpenClaws-Mini live status snapshot.
#
# Runs every 30 min via ai.openclaw.status-snapshot LaunchAgent.
# Collects 10 read-only signals and PATCHes the code block on
# the "Live Status Heartbeat" Notion page. Zero LLM wrapping:
# direct shell -> curl -> Notion API. This is the explicit
# anti-pattern for THR-47 (LLM-cron hallucination); do not add
# agent invocations or reasoning steps here.
#
# Secrets: NOTION_API_KEY is sourced from ~/.openclaw/.env (the
# single source of truth for openclaw secrets). The plist no
# longer carries the literal; rotation is a one-file edit of
# .env and takes effect on the next tick.
#
# Exit codes:
#   0  tick succeeded (PATCH HTTP 200)
#   1  no code block found on the page, or PATCH returned non-200
#   2  required env var or env file missing
#   3  GET to fetch children failed
# Per-signal failures are fail-open: the body still ships with
# "unavailable" for that section; overall tick still reports 0.

set -u -o pipefail

MODE="${1:-run}"   # run | --compose-only | --dry-run-patch

# --- Source .env for secrets (single source of truth; avoids
#     embedding NOTION_API_KEY in the plist) -----------------------
ENV_FILE="/Users/openclaw/.openclaw/.env"
if [ -r "$ENV_FILE" ]; then
  # shellcheck disable=SC1090
  set -a; . "$ENV_FILE"; set +a
else
  echo "FATAL env file unreadable: $ENV_FILE"; exit 2
fi

# --- Required env (defensive checks after sourcing .env) ---------
: "${NOTION_API_KEY:?NOTION_API_KEY not set after sourcing .env}" 2>/dev/null || {
  echo "FATAL NOTION_API_KEY not set after sourcing .env"; exit 2; }
: "${NOTION_STATUS_PAGE_ID:?NOTION_STATUS_PAGE_ID not set}" 2>/dev/null || {
  echo "FATAL NOTION_STATUS_PAGE_ID not set"; exit 2; }
NOTION_VERSION="${NOTION_VERSION:-2022-06-28}"

# --- Paths & constants -------------------------------------------
APPROVALS_JSON="/Users/openclaw/.openclaw/exec-approvals.json"
SCRAPER_LOG="/tmp/openclaw/notion-write-scraper.log"
WRITES_JSONL="/tmp/openclaw/notion-writes.jsonl"
BREAKER_LOG="/tmp/openclaw/circuit-breaker-watchdog.log"
PAUSE_FLAG="/Users/openclaw/.openclaw/workspace/state/breaker-paused.flag"
CRON_RUNS_DIR="/Users/openclaw/.openclaw/cron/runs"
TMP_DIR="/tmp/openclaw"

# --- Helpers -----------------------------------------------------
safe() {
  # Run a command; on nonzero or empty output, echo the fallback.
  local out
  out="$(eval "$1" 2>/dev/null)" || out=""
  if [ -z "$out" ]; then printf '%s\n' "${2:-unavailable}"; else printf '%s\n' "$out"; fi
}

# --- Signal collection ------------------------------------------
ts_line="$(/bin/date '+%Y-%m-%d %H:%M:%S %Z (%s)')"
ts_utc="$(/bin/date -u '+%Y-%m-%dT%H:%M:%SZ')"

s_scraper_launchctl="$(safe "/bin/launchctl list | /usr/bin/grep ai.openclaw.notion-write-scraper" "NOT LOADED")"

s_scraper_log="$(safe "/usr/bin/tail -3 $SCRAPER_LOG" "no log")"

s_writes_count="$(safe "/usr/bin/wc -l < $WRITES_JSONL | /usr/bin/awk '{print \$1}'" "0")"

s_breaker_log="$(safe "/usr/bin/tail -5 $BREAKER_LOG" "no log")"

if [ -f "$PAUSE_FLAG" ]; then
  s_pause_flag="$(/bin/cat "$PAUSE_FLAG" 2>/dev/null)"
  [ -z "$s_pause_flag" ] && s_pause_flag="(empty flag file present)"
else
  s_pause_flag="absent"
fi

s_cron_runs="$(safe "/bin/ls -lt $CRON_RUNS_DIR 2>/dev/null | /usr/bin/head -10" "no runs dir")"

s_disk="$(safe "/usr/bin/du -sh $TMP_DIR | /usr/bin/awk '{print \$1}'" "unknown")"

# Signal 9: exec-approvals integrity (hash + count + per-agent).
# Fail-open: if jq fails or file missing, note it but keep going.
if [ -r "$APPROVALS_JSON" ]; then
  s_approvals_sha="$(/usr/bin/shasum -a 256 "$APPROVALS_JSON" | /usr/bin/awk '{print $1}')"
  # Total + per-agent pattern counts. Count entries under
  # .agents.<name>.allowlist[] for each agent.
  s_approvals_counts="$(/opt/homebrew/bin/jq -r '
    (.agents // {}) as $a
    | [$a | to_entries[] | {agent:.key, n:(.value.allowlist // [] | length)}] as $per
    | ([$per[].n] | add // 0) as $total
    | "total: \($total)\nper-agent: \(
        [$per[] | "\(.agent)=\(.n)"] | join(", ")
      )"
  ' "$APPROVALS_JSON" 2>/dev/null)"
  [ -z "$s_approvals_counts" ] && s_approvals_counts="total: ?
per-agent: (jq parse failed)"
else
  s_approvals_sha="unavailable"
  s_approvals_counts="total: ?
per-agent: (file unreadable)"
fi

# Signal 10: gateway running? Spec pattern from §2.10.
s_gateway_raw="$(/bin/launchctl list 2>/dev/null | /usr/bin/grep -E '^[^-].*ai\.openclaw\.gateway\b' || true)"
if [ -n "$s_gateway_raw" ]; then
  gw_pid="$(printf '%s' "$s_gateway_raw" | /usr/bin/awk '{print $1}')"
  s_gateway="yes (PID $gw_pid)"
else
  s_gateway="NOT RUNNING"
fi

# --- Compose plaintext body -------------------------------------
read -r -d '' BODY <<EOF || true
=== OpenClaw Live Status Heartbeat ===
Last updated: $ts_line
UTC:          $ts_utc

--- Scraper ---
launchctl: $s_scraper_launchctl
writes.jsonl lines: $s_writes_count
recent log:
$s_scraper_log

--- Circuit breaker ---
recent log:
$s_breaker_log
pause-flag: $s_pause_flag

--- Cron runs (last 10) ---
$s_cron_runs

--- System ---
/tmp/openclaw size: $s_disk

--- exec-approvals integrity ---
sha256: $s_approvals_sha
$s_approvals_counts

--- Gateway ---
running: $s_gateway
EOF

if [ "$MODE" = "--compose-only" ]; then
  printf '%s\n' "$BODY"
  exit 0
fi

# --- Build rich_text chunk array (<=2000 chars per object) ------
# jq pipeline:
#  - split body on newlines
#  - greedy-pack lines under 2000 chars (preserve newlines)
#  - hard-split any chunk still >2000 (single line > 2000 case)
#  - shape as Notion rich_text array
RICH_TEXT_JSON="$(printf '%s' "$BODY" | /opt/homebrew/bin/jq -Rs '
  def hard_chunk:
    . as $s
    | if ($s | length) <= 2000 then [$s]
      else [$s[0:2000]] + ($s[2000:] | hard_chunk) end;
  split("\n")
  | reduce .[] as $line (
      {chunks: [], current: ""};
      if .current == "" then .current = $line
      elif ((.current | length) + ($line | length) + 1) > 2000
        then .chunks += [.current] | .current = $line
      else .current = (.current + "\n" + $line)
      end
    )
  | (.chunks + [.current])
  | map(select(length > 0))
  | map(hard_chunk) | flatten
  | map({type:"text", text:{content: .}})
')"

# --- Fetch children, find first code block ----------------------
CHILDREN_URL="https://api.notion.com/v1/blocks/${NOTION_STATUS_PAGE_ID}/children"
CHILDREN_RESP="$(/usr/bin/curl -sS \
  -H "Authorization: Bearer ${NOTION_API_KEY}" \
  -H "Notion-Version: ${NOTION_VERSION}" \
  -w '\n__HTTP__%{http_code}' \
  "$CHILDREN_URL")" || {
    echo "FATAL children GET failed (curl exit)"; exit 3; }

HTTP_CODE="$(printf '%s' "$CHILDREN_RESP" | /usr/bin/tail -1 | /usr/bin/sed 's/^__HTTP__//')"
CHILDREN_BODY="$(printf '%s' "$CHILDREN_RESP" | /usr/bin/sed '$d')"
if [ "$HTTP_CODE" != "200" ]; then
  echo "FATAL children GET status=$HTTP_CODE body=$(printf '%s' "$CHILDREN_BODY" | /usr/bin/head -c 300)"
  exit 3
fi

CODE_BLOCK_ID="$(printf '%s' "$CHILDREN_BODY" | /opt/homebrew/bin/jq -r '
  [.results[] | select(.type=="code")][0].id // empty
')"
if [ -z "$CODE_BLOCK_ID" ]; then
  echo "FATAL no code block found on page ${NOTION_STATUS_PAGE_ID}"
  exit 1
fi

# --- Build PATCH body -------------------------------------------
PATCH_BODY="$(/opt/homebrew/bin/jq -n --argjson rt "$RICH_TEXT_JSON" '
  {code: {rich_text: $rt, language: "plain text"}}
')"

if [ "$MODE" = "--dry-run-patch" ]; then
  echo "--- DRY RUN ---"
  echo "code_block_id: $CODE_BLOCK_ID"
  echo "patch_body_preview:"
  printf '%s\n' "$PATCH_BODY" | /opt/homebrew/bin/jq '.'
  echo "(no PATCH sent)"
  exit 0
fi

# --- Send PATCH --------------------------------------------------
PATCH_URL="https://api.notion.com/v1/blocks/${CODE_BLOCK_ID}"
PATCH_RESP="$(/usr/bin/curl -sS \
  -X PATCH \
  -H "Authorization: Bearer ${NOTION_API_KEY}" \
  -H "Notion-Version: ${NOTION_VERSION}" \
  -H "Content-Type: application/json" \
  -w '\n__HTTP__%{http_code}' \
  -d "$PATCH_BODY" \
  "$PATCH_URL")" || {
    echo "FATAL patch curl exited nonzero"; exit 1; }

PATCH_CODE="$(printf '%s' "$PATCH_RESP" | /usr/bin/tail -1 | /usr/bin/sed 's/^__HTTP__//')"
PATCH_RBODY="$(printf '%s' "$PATCH_RESP" | /usr/bin/sed '$d')"
if [ "$PATCH_CODE" = "200" ]; then
  echo "OK ts=$ts_utc block=$CODE_BLOCK_ID body_len=${#BODY} chunks=$(printf '%s' "$RICH_TEXT_JSON" | /opt/homebrew/bin/jq 'length')"
  exit 0
else
  echo "FATAL patch status=$PATCH_CODE body=$(printf '%s' "$PATCH_RBODY" | /usr/bin/head -c 500)"
  exit 1
fi
