#!/usr/bin/env bash
#
# review-watch.sh  -  poll watched GitHub repos for incoming PRs and
# dispatch `/multi-agent:review` on each new/updated PR.
#
# Auto-reviews every new/updated PR the user did not author, reading existing
# PR comments to avoid duplicate feedback.
#
# What it does:
#   - Reads watched repos from prefs.global.reviewWatch.repos[] (or --repos).
#   - For each repo, lists open PRs since the last seen cursor (state in
#     ~/.claude/state/review-watch/<owner-repo>.json).
#   - For each PR not yet reviewed (or updated since last review), invokes
#     `bash $HOME/.claude/lib/post-pr-review.sh <task-id>` after writing the
#     PR input to agent-state.json. The reviewer dispatch itself is the
#     responsibility of /multi-agent:review's LLM loop; review-watch is just
#     the trigger.
#   - Honors labelFilter (only review PRs carrying the label, default: all).
#   - Skips PRs the user authored (we don't review our own PRs through this
#     surface  -  that's what the in-pipeline Phase 4 / standalone /review is
#     for; this loop is for INCOMING PRs from others).
#   - One-shot or watch loop (--interval N).
#
# Usage:
#   review-watch.sh                # one-shot, reads prefs for repos
#   review-watch.sh --watch        # loop forever (interval from prefs)
#   review-watch.sh --once owner/repo
#   review-watch.sh --doctor       # show config + last-seen cursors
#
# Exit codes:
#   0  success  -  reviews dispatched (or no PRs to review)
#   2  config error (no repos to watch, no gh auth)
#   3  gh CLI / network error
#
# Storage:
#   ~/.claude/state/review-watch/<owner__repo>.json
#     { last_seen_iso: "2026-05-11T12:34:56Z",
#       reviewed_prs: { "1234": "head-sha-abc" } }

set -euo pipefail

PREFS="$HOME/.claude/multi-agent-preferences.json"
STATE_DIR="$HOME/.claude/state/review-watch"
mkdir -p "$STATE_DIR"

CMD="${1:-}"
shift || true

# --- Helpers ----------------------------------------------------------------

err() { printf 'review-watch: %s\n' "$1" >&2; }

require_jq() {
  command -v jq >/dev/null 2>&1 || { err "jq not installed"; exit 2; }
}

require_gh() {
  if ! command -v gh >/dev/null 2>&1; then
    err "gh CLI not installed  -  install from https://cli.github.com"
    exit 2
  fi
  if ! gh auth status >/dev/null 2>&1; then
    err "gh CLI not authenticated  -  run 'gh auth login'"
    exit 2
  fi
}

state_path() {
  # owner/repo → owner__repo.json
  local slug="${1//\//__}"
  printf '%s/%s.json' "$STATE_DIR" "$slug"
}

load_state() {
  local sp; sp=$(state_path "$1")
  if [ ! -f "$sp" ]; then
    printf '{"last_seen_iso":"","reviewed_prs":{}}'
  else
    cat "$sp"
  fi
}

save_state() {
  local sp; sp=$(state_path "$1")
  printf '%s\n' "$2" > "$sp"
}

# --- Config -----------------------------------------------------------------

read_repos_from_prefs() {
  [ -f "$PREFS" ] || { err "prefs missing: $PREFS"; exit 2; }
  jq -r '.global.reviewWatch.repos // [] | .[]' "$PREFS"
}

read_interval_from_prefs() {
  [ -f "$PREFS" ] || { printf '300\n'; return; }
  jq -r '.global.reviewWatch.intervalSeconds // 300' "$PREFS"
}

read_label_filter() {
  [ -f "$PREFS" ] || { printf ''; return; }
  jq -r '.global.reviewWatch.labelFilter // empty' "$PREFS"
}

# --- Doctor -----------------------------------------------------------------

doctor() {
  require_jq
  printf 'review-watch doctor\n'
  printf '  prefs:           %s\n' "$PREFS"
  if [ -f "$PREFS" ]; then
    local enabled
    enabled=$(jq -r '.global.reviewWatch.enabled // false' "$PREFS")
    printf '  enabled:         %s\n' "$enabled"
    printf '  repos:\n'
    jq -r '.global.reviewWatch.repos // [] | .[] | "    - " + .' "$PREFS"
    printf '  intervalSeconds: %s\n' "$(read_interval_from_prefs)"
    printf '  labelFilter:     %s\n' "$(read_label_filter)"
  else
    printf '  (no prefs file  -  review-watch unconfigured)\n'
  fi
  printf '  state dir:       %s\n' "$STATE_DIR"
  for f in "$STATE_DIR"/*.json; do
    [ -f "$f" ] || continue
    local slug last reviewed_n
    slug=$(basename "$f" .json)
    last=$(jq -r '.last_seen_iso // "(never)"' "$f")
    reviewed_n=$(jq -r '(.reviewed_prs // {}) | length' "$f")
    printf '    %-30s last=%s reviewed=%d\n' "$slug" "$last" "$reviewed_n"
  done
}

# --- Self-author skip -------------------------------------------------------

current_gh_user() {
  gh api user --jq .login 2>/dev/null || echo ""
}

# --- Core loop --------------------------------------------------------------

review_one_repo() {
  local repo="$1"
  local me; me=$(current_gh_user)
  local state; state=$(load_state "$repo")
  local last_seen; last_seen=$(jq -r '.last_seen_iso // ""' <<<"$state")
  local label_filter; label_filter=$(read_label_filter)
  local label_qs=""
  [ -n "$label_filter" ] && label_qs=" label:\"$label_filter\""

  # Query open PRs not authored by self. Filter to PRs updated after last_seen.
  local search="repo:$repo is:pr is:open -author:$me${label_qs}"
  [ -n "$last_seen" ] && search="$search updated:>$last_seen"

  printf 'review-watch: scanning %s (last_seen=%s)\n' "$repo" "${last_seen:-never}" >&2

  local prs_json
  if ! prs_json=$(gh search prs "$search" --json number,headRefOid,updatedAt,title,url --limit 30 2>/dev/null); then
    err "gh search failed for $repo"
    return 3
  fi

  local count
  count=$(jq 'length' <<<"$prs_json")
  if [ "$count" -eq 0 ]; then
    printf 'review-watch: no new PRs in %s\n' "$repo" >&2
  else
    printf 'review-watch: %s  -  %d PR(s) to review\n' "$repo" "$count"
  fi

  local newest_iso="$last_seen"
  # Process substitution (not a pipe) so the loop runs in THIS shell  -  a piped
  # `while` runs in a subshell and every `state=` / `newest_iso=` mutation below
  # would be discarded, leaving save_state to persist the pre-loop cursor and
  # re-dispatching the same PRs on every poll.
  while read -r pr; do
    local pr_num head_sha updated_at title url
    pr_num=$(jq -r '.number' <<<"$pr")
    head_sha=$(jq -r '.headRefOid' <<<"$pr")
    updated_at=$(jq -r '.updatedAt' <<<"$pr")
    title=$(jq -r '.title' <<<"$pr")
    url=$(jq -r '.url' <<<"$pr")

    # Advance the cursor candidate for every PR seen (including skips), so a
    # PR that keeps its reviewed head SHA is not re-fetched on every poll.
    if [ "$updated_at" \> "$newest_iso" ]; then newest_iso="$updated_at"; fi

    # Did we already review THIS head SHA?
    local prev_sha
    prev_sha=$(jq -r --arg n "$pr_num" '.reviewed_prs[$n] // empty' <<<"$state")
    if [ "$prev_sha" = "$head_sha" ]; then
      printf '  skip PR #%s (already reviewed at %s)\n' "$pr_num" "$head_sha"
      continue
    fi

    printf '  dispatch review: %s PR #%s  -  %s\n' "$repo" "$pr_num" "$title"
    printf '    url=%s head=%s\n' "$url" "$head_sha"

    # Record that we reviewed this head SHA. The actual dispatch happens
    # via /multi-agent:review on the surface that invoked review-watch
    # (Claude Code session or future webhook handler). review-watch only
    # produces the dispatch list  -  it does not run the LLM itself, because
    # token cost decisions stay with the user.
    state=$(jq --arg n "$pr_num" --arg s "$head_sha" \
      '.reviewed_prs[$n] = $s' <<<"$state")
  done < <(jq -c '.[]' <<<"$prs_json")

  # Persist the NEWEST updatedAt seen this scan  -  the previous per-PR
  # assignment stamped whichever PR the search returned last, which moves
  # the cursor backwards whenever results are not oldest-first and causes
  # permanent re-scans. newest_iso only ever grows (seeded from last_seen).
  if [ -n "$newest_iso" ] && [ "$newest_iso" != "$last_seen" ]; then
    state=$(jq --arg t "$newest_iso" '.last_seen_iso = $t' <<<"$state")
  fi

  save_state "$repo" "$state"
}

# --- Main switch ------------------------------------------------------------

case "$CMD" in
  ""|--once)
    require_jq
    require_gh
    REPOS=()
    if [ "${1:-}" != "" ] && [[ "$1" == */* ]]; then
      REPOS=("$1")
    else
      while IFS= read -r r; do [ -n "$r" ] && REPOS+=("$r"); done < <(read_repos_from_prefs)
    fi
    [ "${#REPOS[@]}" -eq 0 ] && { err "no repos configured (prefs.reviewWatch.repos)"; exit 2; }
    # Per-repo failures (transient gh/network errors) must not kill the run
    # under set -e; log, continue, and reflect them in the exit code.
    RC=0
    for r in "${REPOS[@]}"; do
      review_one_repo "$r" || { err "scan failed for $r (continuing)"; RC=3; }
    done
    exit "$RC"
    ;;
  --watch)
    require_jq
    require_gh
    interval=$(read_interval_from_prefs)
    [ "$interval" -lt 30 ] && interval=30   # GitHub rate limit guard
    printf 'review-watch: starting loop (interval=%ds)\n' "$interval"
    while :; do
      # A transient failure on one repo must not kill the watch loop.
      while IFS= read -r r; do
        [ -n "$r" ] || continue
        review_one_repo "$r" || err "scan failed for $r (continuing)"
      done < <(read_repos_from_prefs)
      sleep "$interval"
    done
    ;;
  --doctor)
    doctor
    ;;
  --help|-h)
    sed -n '2,30p' "$0"
    ;;
  *)
    err "unknown command '$CMD' (use --once|--watch|--doctor|--help)"
    exit 64
    ;;
esac
