#!/bin/bash
# Shared stop-hook guards for the pending-message nudge.
#
# Sourced by check-inbox.sh (Claude Code), codex-stop-hook.sh and
# cursor-stop-hook.sh. Pure functions + a state file; no network, no jq
# dependency of its own.
#
# Why this exists — the stale-identity loop:
#   A stop hook reads the bearer token from the config file ON DISK. The agent's
#   MCP connection holds the token it was given when the session STARTED. Those
#   are the same token right up until something rewrites the config — a
#   re-provision, a team restore, a seat handover. From that moment the hook
#   counts messages for the NEW identity while inbox() still queries as the OLD
#   one, so the hook says "2 waiting" and inbox() truthfully answers 0. The
#   agent has no way to see the contradiction, so it calls inbox() again, gets 0
#   again, and burns turns until a human kills it.
#
#   Neither side is wrong and neither can detect it alone. So the hook names the
#   identity the count belongs to, which turns an invisible contradiction into
#   something the agent can read and act on, and gives up after a few identical
#   blocks instead of looping forever.

if [ -f "$(dirname "${BASH_SOURCE[0]}")/runtime-dir.sh" ]; then
  # shellcheck source=runtime-dir.sh
  . "$(dirname "${BASH_SOURCE[0]}")/runtime-dir.sh"
fi

# pc_inbox_reason <count> <namespace> <agent> <inbox_call> <restart_hint> [streak]
#
# The nudge text. Names the identity so a session authenticated as a different
# agent can recognise the mismatch.
#
# BOTH BRANCHES ARE READ BY A HUMAN. They are printed in the transcript, so
# every word is read by a person as well as by an agent: lead with the action,
# keep the diagnosis to one sentence, and never emit "message(s)" — singular and
# plural are already computed one line below.
#
# THE STALE-IDENTITY EXPLANATION IS ONLY EMITTED ONCE THE SYMPTOM HAS ACTUALLY
# OCCURRED. It used to be appended to EVERY nudge, fleet-wide, which was wrong
# for three reasons:
#
#   1. It is ~7 lines of failure analysis attached to "you have one message" —
#      the overwhelmingly common case, where nothing is wrong at all.
#   2. It made every agent read a paragraph about its own credential possibly
#      being dead, on every single nudge. That is not neutral: an agent told
#      often enough that its identity may be broken starts diagnosing that
#      instead of doing the work. One harness did exactly this — it hit a normal
#      empty inbox, concluded "the session token appears stale or replaced",
#      refused to call inbox() again for the rest of the session, and asked the
#      human to restart. Nothing was wrong with its token. The nudge taught it
#      that conclusion.
#   3. A warning that fires on every occasion carries no information on any
#      occasion. It gets skimmed, and then it is not there when it matters.
#
# `streak` is the consecutive-identical-block count from pc_streak_ok. A streak
# of 1 means "first time we are telling you" — short text, no theory. A streak of
# 2+ means the agent has already been nudged for this exact identity and count
# and it did NOT clear, which is the actual evidence of the stale-token
# condition. That is when the explanation is worth its length.
#
# Omitted/unknown streak is treated as the first block, i.e. short. Callers that
# do not pass it keep the quiet wording, which is the safe default.
pc_inbox_reason() {
  local count="$1" ns="$2" agent="$3" call="${4:-inbox()}" hint="${5:-restart this session}"
  local streak="${6:-1}"
  local noun="messages"
  [ "$count" = "1" ] && noun="message"
  case "$streak" in ''|*[!0-9]*) streak=1 ;; esac

  if [ -z "$ns" ] || [ -z "$agent" ]; then
    printf '%s patchcord %s waiting — call %s and reply.' \
      "$count" "$noun" "$call"
    return
  fi

  if [ "$streak" -lt 2 ]; then
    printf '%s patchcord %s for %s@%s — call %s and reply.' \
      "$count" "$noun" "$agent" "$ns" "$call"
    return
  fi

  # Second consecutive identical block: the nudge did NOT clear, which is the
  # actual evidence of the stale-token condition. Even here the action comes
  # first and the diagnosis is one sentence on its own line — a repeat is a
  # worse moment to make someone parse a paragraph, not a better one.
  printf 'Still %s patchcord %s for %s@%s — %s did not clear it. Call %s once more.
If it shows a different agent or 0 pending: the token on disk changed after this session started, so this session is signed in as someone else. Do not retry — tell the user and ask them to %s.' \
    "$count" "$noun" "$agent" "$ns" \
    "$call" "$call" \
    "$hint"
}

# pc_streak_n <state_file>
#
# The current consecutive-identical-block count, or 0. Read AFTER pc_streak_ok
# has run, so it reflects this block. Exists so the nudge text can escalate on
# evidence instead of warning unconditionally.
pc_streak_n() {
  local prev n
  prev=$(cat "$1" 2>/dev/null || true)
  [ -n "$prev" ] || { printf '0'; return; }
  n="${prev##*|}"
  case "$n" in ''|*[!0-9]*) n=0 ;; esac
  printf '%s' "$n"
}

# pc_streak_ok <state_file> <key> <max>
#
# Consecutive-identical-block breaker. `key` should encode identity + count, so
# the streak resets the moment either changes and a genuinely new message always
# gets through. Returns 0 (block) while the streak is under `max`, 1 (stay
# silent) once it is reached. Writes state best-effort; an unwritable runtime
# degrades to "always block", never to a hard failure.
pc_streak_ok() {
  local state="$1" key="$2" max="${3:-3}"
  local prev="" prev_key="" prev_n=0

  prev=$(cat "$state" 2>/dev/null || true)
  if [ -n "$prev" ]; then
    prev_key="${prev%|*}"
    prev_n="${prev##*|}"
    case "$prev_n" in ''|*[!0-9]*) prev_n=0 ;; esac
  fi

  if [ "$prev_key" = "$key" ]; then
    if [ "$prev_n" -ge "$max" ]; then
      return 1
    fi
    prev_n=$(( prev_n + 1 ))
  else
    prev_n=1
  fi

  # Braces so the REDIRECT's own failure is captured too — `printf > bad 2>/dev/null`
  # still prints the redirect error, and kimi's hook treats stderr as the
  # notification channel.
  { printf '%s|%s' "$key" "$prev_n" > "$state"; } 2>/dev/null || true
  return 0
}

# pc_state_path <prefix> <namespace> <agent>
# A filesystem-safe state path. Identity is part of the name so two agents in
# one project never share a streak counter.
pc_state_path() {
  local prefix="$1" ns="${2:-nons}" agent="${3:-noagent}"
  local slug
  slug=$(printf '%s_%s' "$ns" "$agent" | tr -c 'A-Za-z0-9_.-' '_')
  local root
  root=$(pc_runtime_dir 2>/dev/null || true)
  [ -n "$root" ] || return 1
  printf '%s/%s_%s' "$root" "$prefix" "$slug"
}
