#!/usr/bin/env bash
#
# Combine multiple per-model AI code reviews into a single sticky PR comment.
#
# This is the pure-assembly half of the AI Code Reviewer workflow
# (.github/workflows/ai-code-reviewer.yml), split out so the non-trivial parts —
# header/footer stripping, "Reviewer N" section assembly, label union, and the
# pass/fail aggregation — can be unit-tested without any network or GitHub API
# access. The workflow runs the models (network) and writes each reviewer's raw
# stdout to resp_<i>.json and its exit code to code_<i>; this script turns those
# into the comment body, label set, decision, and success count.
#
# Usage:
#   combine-ai-reviews.sh <work_dir> <model_1> [<model_2> ...]
#
#   Reads, for i in 0..N-1 (N = number of model args):
#     <work_dir>/resp_<i>.json  - raw stdout from ai-reviewer.sh for reviewer i
#     <work_dir>/code_<i>        - that reviewer's integer exit code
#     <work_dir>/err_<i>.log     - that reviewer's stderr (only shown in DEBUG)
#   The model names are used only for the per-reviewer log-group labels; the
#   posted comment stays anonymized ("Reviewer 1", "Reviewer 2", ...).
#
#   Environment:
#     HEAD_SHA    - head commit sha for the "Last reviewed at commit" line (opt)
#     DEBUG_MODE  - "true" to echo raw responses + stderr to this script's stderr
#
#   Writes (into <work_dir>):
#     comment_body.md    - the combined sticky-comment body
#     labels.txt         - deduped labels, one per line (may be empty)
#     decision.txt       - "pass" or "fail"
#     success_count.txt  - number of reviewers that produced a usable review
#
#   Logs ::group:: sections and debug output to stderr (kept off stdout so the
#   output files are the only contract). Exit status is 0 even when every
#   reviewer failed — the caller inspects success_count.txt and decides whether
#   to fail the workflow. A non-zero exit means a usage error only.

set -euo pipefail

WORK_DIR="${1:-}"
if [ -z "$WORK_DIR" ] || [ ! -d "$WORK_DIR" ]; then
  echo "usage: $0 <work_dir> <model> [<model> ...]" >&2
  exit 2
fi
shift
MODELS=("$@")
if [ "${#MODELS[@]}" -eq 0 ]; then
  echo "error: at least one model name is required" >&2
  exit 2
fi

DEBUG_MODE="${DEBUG_MODE:-false}"
HEAD_SHA="${HEAD_SHA:-}"

COMBINED_REVIEW=""
ALL_LABELS=""
ANY_FAIL="false"
SUCCESS_COUNT=0

# Assemble the combined comment in model order. Reviewers are anonymized as
# "Reviewer N" — the model -> number mapping appears only in the (debug) logs
# below, so readers judge the feedback, not the model.
for i in "${!MODELS[@]}"; do
  REVIEWER_NUM=$((i + 1))
  EXIT_CODE="$(cat "$WORK_DIR/code_$i" 2>/dev/null || echo 1)"
  # A missing/empty/garbage exit-code file counts as a failed reviewer rather
  # than tripping the numeric comparison below (which would print "integer
  # expression expected" and then fall through to wrongly treating the reviewer
  # as a success).
  [[ "$EXIT_CODE" =~ ^[0-9]+$ ]] || EXIT_CODE=1
  AI_RESPONSE="$(cat "$WORK_DIR/resp_$i.json" 2>/dev/null || echo "")"
  echo "::group::Reviewer #$REVIEWER_NUM (${MODELS[i]})" >&2

  if [ "$DEBUG_MODE" = "true" ]; then
    {
      echo "=== RAW AI RESPONSE (reviewer #$REVIEWER_NUM, exit $EXIT_CODE) ==="
      echo "$AI_RESPONSE"
      echo "--- stderr ---"
      cat "$WORK_DIR/err_$i.log" 2>/dev/null || true
      echo "=== END RAW AI RESPONSE ==="
    } >&2
  fi

  # One model failing (hard error, empty, or non-JSON) must not sink the others:
  # record a short failure note and keep going. The caller fails the workflow
  # only if *every* reviewer failed (success_count == 0). A reviewer that errors
  # out never sets a verdict, so it can never flip the aggregate decision to
  # "fail" — only a model that returns a valid "fail" verdict does. Sensitive raw
  # output is never written to the comment.
  # Require a JSON *object*: a bare `jq .` accepts any valid JSON, including a
  # top-level string/array/number that a refusing or misbehaving model might
  # emit ("I refuse to review this"). That would pass the gate and then crash
  # the `.review` access below under `set -e`, sinking *every* reviewer and
  # writing no output at all. `type == "object"` rejects non-objects so they
  # degrade to a per-reviewer failure note, preserving the others.
  if [ "$EXIT_CODE" -ne 0 ] || [ -z "$AI_RESPONSE" ] || ! echo "$AI_RESPONSE" | jq -e 'type == "object"' >/dev/null 2>&1; then
    echo "⚠️ Reviewer #$REVIEWER_NUM did not return a usable review (exit $EXIT_CODE)" >&2
    REVIEW_BODY="_This reviewer could not complete its review (see workflow logs for details)._"
    LABELS=""
  else
    REVIEW_RAW=$(echo "$AI_RESPONSE" | jq -r '.review // "No review provided"')
    # Trim whitespace with tr (the verdict is a single token). The previous
    # `| xargs` aborted the entire script on an unbalanced quote in the value,
    # which—inside this per-reviewer loop—would sink every reviewer.
    DECISION=$(echo "$AI_RESPONSE" | jq -r '.fail_pass_workflow // "uncertain"' | tr -d '[:space:]')
    LABELS=$(echo "$AI_RESPONSE" | jq -r '.labels_added[]? // empty')
    SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
    [ "$DECISION" = "fail" ] && ANY_FAIL="true"

    # Each review opens with a "## AI Code Review" H2 and closes with the
    # singular Friendly AI Reviewer footer. Under a "### 👤 Reviewer N"
    # subheading the H2 is redundant and the footer would repeat once per model,
    # so strip both here; a single (plural) footer is added to the combined
    # comment below. The footer match keys on the stable "Review by [Friendly AI
    # Reviewer]" prefix and the \s*\z tail absorbs trailing whitespace; if the
    # upstream markers ever change and aren't found, the text is left intact
    # (nothing is dropped).
    REVIEW_BODY=$(printf '%s' "$REVIEW_RAW" \
      | perl -0pe 's/\A\s*##\s*AI Code Review\s*\n+//' \
      | perl -0pe 's/\n*---\s*\n\*Review by \[Friendly AI Reviewer\][^\n]*\n?\s*\z//s')
  fi

  # Lead each section with a blank line so the "### Reviewer N" heading always
  # renders (command substitution strips trailing newlines, so the separator
  # must precede the heading, not follow the previous body).
  COMBINED_REVIEW=$(printf '%s\n\n### 👤 Reviewer %s\n\n%s' "$COMBINED_REVIEW" "$REVIEWER_NUM" "$REVIEW_BODY")
  [ -n "$LABELS" ] && ALL_LABELS=$(printf '%s\n%s' "$ALL_LABELS" "$LABELS")
  echo "::endgroup::" >&2
done

# Aggregate labels (union across reviewers) and the pass/fail decision (request
# changes if ANY reviewer did).
LABELS=$(printf '%s\n' "$ALL_LABELS" | sed '/^[[:space:]]*$/d' | sort -u)
if [ "$ANY_FAIL" = "true" ]; then
  DECISION="fail"
else
  DECISION="pass"
fi

REVIEWER_WORD="reviewers"
[ "${#MODELS[@]}" -eq 1 ] && REVIEWER_WORD="reviewer"
STICKY_MARKER="<!-- ai-code-review:sticky -->"
FOOTER="*Reviews by [Friendly AI Reviewer](https://github.com/LearningCircuit/Friendly-AI-Reviewer) - made with ❤️*"
# COMBINED_REVIEW already starts with "\n\n", so it follows the title directly
# (no extra newline) to yield exactly one blank line before "### Reviewer 1".
# The backticks in `%s` are intentional literal Markdown (inline-code the SHA),
# not a command substitution — single quotes keep them literal on purpose.
# shellcheck disable=SC2016
COMMENT_BODY=$(printf '%s\n\n## 🤖 AI Code Review (%s %s)%s\n\n---\n%s\n\n_Last reviewed at commit `%s`_' \
  "$STICKY_MARKER" "${#MODELS[@]}" "$REVIEWER_WORD" "$COMBINED_REVIEW" "$FOOTER" "${HEAD_SHA:0:7}")

printf '%s' "$COMMENT_BODY" > "$WORK_DIR/comment_body.md"
printf '%s' "$LABELS" > "$WORK_DIR/labels.txt"
printf '%s' "$DECISION" > "$WORK_DIR/decision.txt"
printf '%s' "$SUCCESS_COUNT" > "$WORK_DIR/success_count.txt"
