#!/usr/bin/env bash
set -euo pipefail

# ---------------------------------------------------------------------------
# check-stale-docs.sh — Songwhip docs plugin, Claude Code Stop hook
#
# When enough code changed in the current repo, emits a
# {"decision":"block","reason":...} payload that asks Claude to flag any stale
# docs and OFFER to update them via /update-docs. It never edits docs itself.
#
# Noise gate: only fires when at least SONGWHIP_DOCS_MIN_CHANGED_FILES (default
# 3) non-markdown code files have changed, so small edits don't nag.
# ---------------------------------------------------------------------------

MIN_CHANGED_FILES="${SONGWHIP_DOCS_MIN_CHANGED_FILES:-3}"
# Sanitize: the env override is user-controlled and later used in a numeric
# comparison. A non-integer would make `[[ -lt ]]` error and, under `set -e`,
# abort the hook — so fall back to the default if it isn't a positive integer.
if ! [[ "$MIN_CHANGED_FILES" =~ ^[0-9]+$ ]]; then
  MIN_CHANGED_FILES=3
fi

# ---------------------------------------------------------------------------
# JSON helpers (prefer jq, fall back to python3)
# ---------------------------------------------------------------------------

json_get() {
  local json="$1" field="$2"
  if command -v jq > /dev/null 2>&1; then
    echo "$json" | jq -r ".$field // empty"
  elif command -v python3 > /dev/null 2>&1; then
    echo "$json" | python3 -c "import json,sys; d=json.load(sys.stdin); v=d.get('$field',''); print(str(v).lower() if isinstance(v,bool) else (v or ''))"
  else
    echo ""
  fi
}

json_output_block() {
  local reason="$1"
  if command -v jq > /dev/null 2>&1; then
    jq -n --arg reason "$reason" '{"decision": "block", "reason": $reason}'
  elif command -v python3 > /dev/null 2>&1; then
    echo -n "$reason" | python3 -c "import json,sys; print(json.dumps({'decision':'block','reason':sys.stdin.read()}))"
  fi
}

# This hook needs jq or python3 to parse its payload and emit a decision. Without
# either, fail open (loudly on stderr) rather than misbehaving: the
# stop_hook_active re-trigger guard below also relies on these tools, so emitting
# a block in this state could loop.
if ! command -v jq > /dev/null 2>&1 && ! command -v python3 > /dev/null 2>&1; then
  echo "songwhip-docs: check-stale-docs needs jq or python3 installed; skipping doc-staleness check." >&2
  exit 0
fi

# ---------------------------------------------------------------------------
# 1. Read the Stop-hook payload
# ---------------------------------------------------------------------------

INPUT="$(cat)"

CWD="$(json_get "$INPUT" "cwd")"
# Guard the cd: a missing/deleted cwd would make `cd` fail and, under `set -e`,
# abort the whole hook. Skip silently if it isn't a usable directory.
if [[ -n "$CWD" && -d "$CWD" ]]; then
  cd "$CWD" || exit 0
fi

# Avoid re-triggering on the response the block itself produces
STOP_HOOK_ACTIVE="$(json_get "$INPUT" "stop_hook_active")"
if [[ "$STOP_HOOK_ACTIVE" == "true" ]]; then
  exit 0
fi

# ---------------------------------------------------------------------------
# 2. Git change detection
# ---------------------------------------------------------------------------

if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then
  exit 0
fi

# Resolve the repo root up front so change paths and the doc-file walk below
# share one base. Running change detection from the repo root (git -C) keeps its
# output repo-root-relative even when the session started in a subdirectory —
# otherwise the "$REPO_ROOT/$code_file" join below would point at nonexistent
# paths and the area-doc walk would silently find nothing.
REPO_ROOT="$(git rev-parse --show-toplevel)"

CHANGED_FILES="$(
  {
    git -C "$REPO_ROOT" diff --no-renames --name-only 2>/dev/null || true
    git -C "$REPO_ROOT" diff --no-renames --cached --name-only 2>/dev/null || true
    git -C "$REPO_ROOT" ls-files --others --exclude-standard 2>/dev/null || true
  } | sort -u
)"

# Keep only code files (drop markdown, .env*/.envrc* secrets, and blanks). Env
# files aren't documented "code" — counting them would trip the noise gate on
# config-only changes and would surface secrets filenames in the prompt below.
CODE_FILES="$(echo "$CHANGED_FILES" | grep -v '\.md$' | grep -vE '(^|/)\.env(rc)?($|\.)' | grep -v '^$' || true)"

if [[ -z "$CODE_FILES" ]]; then
  exit 0
fi

CODE_FILE_COUNT="$(echo "$CODE_FILES" | grep -c . || true)"

# Noise gate: skip small changes
if [[ "$CODE_FILE_COUNT" -lt "$MIN_CHANGED_FILES" ]]; then
  exit 0
fi

# ---------------------------------------------------------------------------
# 3. Doc file collection
# ---------------------------------------------------------------------------

# All .claude/docs/*.md
DOCS_DIR_FILES="$(
  if [[ -d "$REPO_ROOT/.claude/docs" ]]; then
    ls "$REPO_ROOT/.claude/docs/"*.md 2>/dev/null || true
  fi
)"

# README.md / CLAUDE.md walking up from each changed file to the repo root
AREA_DOC_FILES=""
while IFS= read -r code_file; do
  [[ -z "$code_file" ]] && continue

  file_abs="$REPO_ROOT/$code_file"
  dir="$(dirname "$file_abs")"

  while true; do
    for doc_name in README.md CLAUDE.md; do
      candidate="$dir/$doc_name"
      if [[ -f "$candidate" ]]; then
        AREA_DOC_FILES="${AREA_DOC_FILES}"$'\n'"$candidate"
      fi
    done

    if [[ "$dir" == "$REPO_ROOT" ]]; then
      break
    fi

    parent="$(dirname "$dir")"
    if [[ "$parent" == "$dir" ]]; then
      break
    fi
    dir="$parent"
  done
done <<< "$CODE_FILES"

ALL_DOC_FILES="$(
  {
    echo "$DOCS_DIR_FILES"
    echo "$AREA_DOC_FILES"
  } | grep -v '^$' | sort -u
)"

if [[ -z "$ALL_DOC_FILES" ]]; then
  exit 0
fi

DOC_COUNT="$(echo "$ALL_DOC_FILES" | grep -c . || true)"

# ---------------------------------------------------------------------------
# 4. Build the offer prompt
# ---------------------------------------------------------------------------

CODE_FILES_LIST="$(echo "$CODE_FILES" | sed 's/^/  - /')"
DOC_FILES_LIST="$(echo "$ALL_DOC_FILES" | sed "s|^$REPO_ROOT/||" | sed 's/^/  - /')"

REASON="DOCS STALENESS CHECK — ${CODE_FILE_COUNT} code file(s) changed; ${DOC_COUNT} nearby doc(s).
Changed code:
${CODE_FILES_LIST}
Candidate docs:
${DOC_FILES_LIST}
If any of these docs are now stale because of the changes, name which and why (one line each), then OFFER to update them — tell the user to run /update-docs (or say 'update them'). Do NOT edit docs now. If none are stale, say 'Docs look fine.'"

json_output_block "$REASON"
