#!/bin/bash
set -euo pipefail

# jq is required — skip silently if not installed (fresh macOS)
command -v jq >/dev/null 2>&1 || exit 0

HOOK_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
if [ -f "$HOOK_DIR/lib/inbox-guard.sh" ]; then
  # shellcheck source=lib/inbox-guard.sh
  . "$HOOK_DIR/lib/inbox-guard.sh"
else
  # Degrade to the pre-guard behaviour rather than dying: a partial install
  # must still nudge, just without the identity hint or the loop breaker.
  pc_inbox_reason() { printf '%s patchcord message(s) waiting — call inbox() and reply.' "$1"; }
  pc_streak_ok() { return 0; }
  pc_state_path() { printf ''; }
  pc_streak_n() { printf '1'; }
fi

INPUT=$(cat)

# Validate input is parseable JSON before proceeding — Claude Code may include
# non-standard numeric values (NaN, Infinity) in edge cases that trip jq.
if ! printf '%s' "$INPUT" | jq -e '.' >/dev/null 2>&1; then
  INPUT='{}'
fi

# Get project cwd from Claude Code's input JSON, fall back to $PWD
PROJECT_CWD=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null || true)
[ -z "$PROJECT_CWD" ] || [ "$PROJECT_CWD" = "null" ] && PROJECT_CWD="$PWD"

# Guard against infinite loops: stop_hook_active is true when Claude
# is already continuing because a previous Stop hook told it to.
STOP_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false' 2>/dev/null || echo "false")
if [ "$STOP_ACTIVE" = "true" ]; then
  exit 0
fi

# ── Update check (once per session, first run only) ───────────
UPDATE_RUNTIME_DIR=$(pc_runtime_dir 2>/dev/null || true)
[ -n "$UPDATE_RUNTIME_DIR" ] || exit 0
UPDATE_FLAG="$UPDATE_RUNTIME_DIR/patchcord_update_checked_$$"
if [ ! -f "$UPDATE_FLAG" ]; then
  touch "$UPDATE_FLAG"
  plugin_json="${CLAUDE_PLUGIN_ROOT:-.}/.claude-plugin/plugin.json"
  if [ -f "$plugin_json" ]; then
    installed_ver=$(jq -r '.version // ""' "$plugin_json" 2>/dev/null || true)
    if [ -n "$installed_ver" ]; then
      latest=$(npm view patchcord version --json 2>/dev/null | tr -d '"' || true)
      if [ -n "$latest" ] && [ "$latest" != "$installed_ver" ]; then
        echo "⬆ Patchcord plugin update: v${installed_ver} → v${latest}. Run: npx patchcord@latest install" >&2
      fi
    fi
  fi
fi

# Resolve config from project-scoped .mcp.json only.
TOKEN=""
URL=""
MCP_JSON=""
[ -f "$PROJECT_CWD/.mcp.json" ] && MCP_JSON="$PROJECT_CWD/.mcp.json"

if [ -n "$MCP_JSON" ]; then
  MCP_URL=$(jq -r '.mcpServers.patchcord.url // empty' "$MCP_JSON" 2>/dev/null || true)
  MCP_AUTH=$(jq -r '.mcpServers.patchcord.headers.Authorization // empty' "$MCP_JSON" 2>/dev/null || true)
  if [ -n "$MCP_URL" ] && [ -n "$MCP_AUTH" ]; then
    URL="${MCP_URL%/mcp}"
    URL="${URL%/mcp/bearer}"
    TOKEN="${MCP_AUTH#Bearer }"
  fi
fi

if [ -z "$URL" ] || [ -z "$TOKEN" ]; then
  exit 0  # Not configured, skip silently
fi

# Check inbox — one lightweight HTTP call
MACHINE_NAME=$(hostname -s 2>/dev/null || echo "unknown")
INSTALL_PATH=$(dirname "$MCP_JSON")

# Capture body and status in one pipe. The old response path let concurrent
# stop hooks truncate one another's body; not creating a response file removes
# that collision entirely.
RESPONSE=$(curl -s -w $'\n%{http_code}' --max-time 5 \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "x-patchcord-machine: ${MACHINE_NAME}" \
  -H "x-patchcord-install-path: ${INSTALL_PATH}" \
  "${URL}/api/inbox?status=pending&limit=5&count_only=1" 2>/dev/null || printf '\n000')
HTTP_CODE=${RESPONSE##*$'\n'}
RESPONSE=${RESPONSE%$'\n'*}

if [ "$HTTP_CODE" = "401" ] || [ "$HTTP_CODE" = "403" ]; then
  jq -n '{
    "decision": "block",
    "reason": "PATCHCORD AUTH FAILED: token rejected by server (HTTP '"$HTTP_CODE"'). Check your token in .mcp.json — it may be wrong, expired, or not yet registered on the server."
  }'
  exit 0
fi

if [ "$HTTP_CODE" = "000" ]; then
  # Server unreachable — skip silently
  exit 0
fi

# "COULD NOT TELL" IS NOT "NOTHING PENDING". Below, an unparseable body used to
# fall through jq's `// 0` default to COUNT=0, which takes the same exit path as
# a genuinely empty inbox AND clears the streak state. So a corrupted response
# told the agent it had no messages, silently, with no way to tell the two apart
# — the same absence-reads-as-a-value shape that cost this project a night.
# Refuse to answer instead: leave the streak state alone and say nothing.
if ! printf '%s' "$RESPONSE" | jq -e 'type == "object"' >/dev/null 2>&1; then
  exit 0
fi

# ── Auto-apply custom skill from web console ──────────────────
# Writes to .claude/skills/patchcord-custom/SKILL.md — Claude Code
# native project-level skill directory. Auto-discovered by Claude.
NAMESPACE=$(echo "$RESPONSE" | jq -r '.namespace_id // empty' 2>/dev/null || true)
AGENT_ID=$(echo "$RESPONSE" | jq -r '.agent_id // empty' 2>/dev/null || true)

if [ -n "$NAMESPACE" ] && [ -n "$AGENT_ID" ]; then
  SKILL_RESP=$(curl -s --max-time 3 \
    -H "Authorization: Bearer ${TOKEN}" \
    "${URL}/api/skills/${NAMESPACE}/${AGENT_ID}" 2>/dev/null || true)

  if [ -n "$SKILL_RESP" ]; then
    SKILL_TEXT=$(echo "$SKILL_RESP" | jq -r '.skill_text // empty' 2>/dev/null || true)
    SKILL_HASH=$(printf '%s' "$SKILL_TEXT" | (md5sum 2>/dev/null || md5 2>/dev/null) | cut -d' ' -f1 || echo "nohash")
    RUNTIME_DIR=$(pc_runtime_dir 2>/dev/null || true)
    [ -n "$RUNTIME_DIR" ] || exit 0
    CACHE_FILE="$RUNTIME_DIR/patchcord_skill_hash_${NAMESPACE}_${AGENT_ID}"
    OLD_HASH=$(cat "$CACHE_FILE" 2>/dev/null || echo "")

    if [ -n "$SKILL_TEXT" ] && [ "$SKILL_HASH" != "$OLD_HASH" ]; then
      PROJECT_ROOT=$(dirname "$MCP_JSON")
      SKILL_DIR="${PROJECT_ROOT}/.claude/skills/patchcord-custom"
      SKILL_FILE="${SKILL_DIR}/SKILL.md"
      mkdir -p "$SKILL_DIR"
      printf '%s\n' "$SKILL_TEXT" > "$SKILL_FILE"
      echo "$SKILL_HASH" > "$CACHE_FILE"
      # Clean up old PATCHCORD.md if it exists
      rm -f "${PROJECT_ROOT}/PATCHCORD.md"
    fi
  fi
fi

# ── Inbox notification (deduplicated across Stop + Notification hooks) ──
COUNT=$(echo "$RESPONSE" | jq -r '.count // .pending_count // 0' 2>/dev/null || echo "0")
case "$COUNT" in
  ''|*[!0-9]*) COUNT=0 ;;
esac

STREAK_STATE=$(pc_state_path "patchcord_inbox_streak" "$NAMESPACE" "$AGENT_ID" 2>/dev/null || echo "")

if [ "$COUNT" -eq 0 ]; then
  [ -n "$STREAK_STATE" ] && rm -f "$STREAK_STATE"
  exit 0
fi

# PER-IDENTITY lock. This was a single global path, so on a host running several
# seats one agent's notification suppressed EVERY other agent's for 5s — an
# agent with real pending messages could be silenced by an unrelated seat that
# happened to stop first. The dedupe is meant to stop ONE identity being told
# twice by the Stop and Notification hooks, never to stop a second identity
# being told at all.
NOTIFY_LOCK=$(pc_state_path "patchcord_notify_lock" "$NAMESPACE" "$AGENT_ID" 2>/dev/null || echo "")
[ -n "$NOTIFY_LOCK" ] || exit 0
LOCK_AGE=5
if [ -f "$NOTIFY_LOCK" ]; then
  LOCK_MTIME=$(stat -c %Y "$NOTIFY_LOCK" 2>/dev/null || stat -f %m "$NOTIFY_LOCK" 2>/dev/null || echo "0")
  NOW=$(date +%s)
  if [ $(( NOW - LOCK_MTIME )) -lt $LOCK_AGE ]; then
    exit 0  # Already notified within 5s
  fi
fi

# Give up after MAX identical blocks. Keyed on identity+count, so a genuinely
# new message resets it and always gets through — but a hook that keeps
# counting messages the session cannot see (stale on-disk token, see
# lib/inbox-guard.sh) stops burning turns instead of looping forever.
if [ -n "$STREAK_STATE" ] \
   && ! pc_streak_ok "$STREAK_STATE" "${NAMESPACE}/${AGENT_ID}/${COUNT}" 3; then
  exit 0
fi

touch "$NOTIFY_LOCK"
# Read the streak AFTER pc_streak_ok has incremented it, so the text can
# escalate on evidence: quiet on the first nudge, and only explaining the
# stale-token condition once a repeat proves inbox() did not clear it.
NUDGE_STREAK=$(pc_streak_n "$STREAK_STATE" 2>/dev/null || echo 1)
REASON=$(pc_inbox_reason "$COUNT" "$NAMESPACE" "$AGENT_ID" "inbox()" "restart this Claude Code session" "$NUDGE_STREAK")
jq -n --arg reason "$REASON" '{"decision": "block", "reason": $reason}'
