#!/usr/bin/env bash
#
# post-pr-review.sh
#
# Posts the verdict from `agent-state.review` to the target PR using the
# review-actions contract: per-finding inline comments + an explicit approve
# / needs-work state. Spec: refs/channels/pr-review-actions.md.
#
# Provider switch:
#   - github           → gh CLI (gh api + gh pr review)
#   - bitbucket-server → curl + Bitbucket REST API (Basic auth from keychain)
#
# Usage:
#   post-pr-review.sh <task-id>
#
# Reads from agent-state.json:
#   .review.input              { kind, provider, host, orgRepo, prNumber,
#                                bbProjectKey, bbRepoSlug, bbServerHost }
#   .review.findings           [{ severity, status, file, line, issue, fix,
#                                ruleId }]
#   .review.iterationNumber    int (1-based fallback for the comment footer;
#                              on needs_work the live value is re-derived from
#                              the highest "iteration #N" already on the PR + 1)
#   .review.headCommitSha      string  (GitHub inline anchor needs commit_id)
#
# Reads from preferences:
#   prefs.global.outputLanguage  ("tr" | "en")
#
# Exit codes:
#   0  success  -  verdict + (any) inline comments posted
#   2  agent-state.json missing or .review absent
#   3  unknown provider; falls back to chat-only and exits 3
#   4  API call failed (network / auth / 4xx). Diagnostic line on stderr.

set -euo pipefail

# Basic-auth via a curl config fed through process substitution so the
# credential never appears in argv (argv is visible to `ps` / process audit).
bb_auth_cfg() { printf 'user = "%s:%s"\n' "$1" "$2"; }

TASK_ID="${1:-}"

if [ -z "$TASK_ID" ]; then
  echo "usage: post-pr-review.sh <task-id>" >&2
  exit 64
fi

# Resolve agent-state.json. multi-repo-pipeline.sh writes it under a
# per-project directory ($HOME/.claude/logs/multi-agent/{project}/{taskId}/)
# - not the ~/.claude/projects/*/state/ path this used to glob, which no
# writer in the pipeline ever populates.
AGENT_STATE=""
for candidate in "$HOME"/.claude/logs/multi-agent/*/"$TASK_ID"/agent-state.json; do
  if [ -f "$candidate" ]; then
    AGENT_STATE="$candidate"
    break
  fi
done

if [ -z "$AGENT_STATE" ]; then
  echo "post-pr-review: agent-state.json not found for task=$TASK_ID" >&2
  exit 2
fi

if ! jq -e '.review' "$AGENT_STATE" >/dev/null; then
  echo "post-pr-review: agent-state has no .review field  -  run /multi-agent:review first" >&2
  exit 2
fi

OUT_LANG=$(jq -r '.global.outputLanguage // "en"' "$HOME/.claude/multi-agent-preferences.json" 2>/dev/null || echo "en")
case "$OUT_LANG" in tr|en) ;; *) OUT_LANG=en ;; esac

PROVIDER=$(jq -r '.review.input.provider // empty' "$AGENT_STATE")
PR_NUM=$(jq -r '.review.input.prNumber // empty' "$AGENT_STATE")
ITERATION=$(jq -r '.review.iterationNumber // 1' "$AGENT_STATE")

# --- Decision ---------------------------------------------------------------

# Single jq pass  -  group accepted findings by severity into tab-separated counts.
# `.review.findings // []` so a null/absent findings array (clean review)
# cannot raise "Cannot iterate over null" under set -e.
read -r ACCEPTED_BLOCKING ACCEPTED_IMPORTANT < <(
  jq -r '
    [(.review.findings // [])[] | select(.status == "accepted")] as $a
    | [($a | map(select(.severity == "blocking")) | length),
       ($a | map(select(.severity == "important")) | length)]
    | @tsv
  ' "$AGENT_STATE"
)
ACCEPTED_BLOCKING=${ACCEPTED_BLOCKING:-0}
ACCEPTED_IMPORTANT=${ACCEPTED_IMPORTANT:-0}

if [ "$ACCEPTED_BLOCKING" -eq 0 ] && [ "$ACCEPTED_IMPORTANT" -eq 0 ]; then
  DECISION=approve
else
  DECISION=needs_work
fi

echo "post-pr-review: decision=$DECISION (blocking=$ACCEPTED_BLOCKING, important=$ACCEPTED_IMPORTANT)" >&2

# --- Per-finding comment body renderer (in $OUT_LANG) -----------------------

# Stable fingerprint for a finding  -  used by dedupe to recognize re-runs of
# the same finding without re-posting. Sha256 of "path|line|issue"  -  note
# that fix/ruleId/iterationNumber are excluded so a finding still dedupes
# when the suggested fix wording changes slightly between iterations.
finding_fingerprint() {
  local path="$1" line="$2" issue="$3"
  # sha256sum (coreutils) or shasum (perl), whichever this platform has. A bare
  # `shasum` produced an empty fingerprint on slim Linux images, which made every
  # finding look new and re-posted the whole review on each iteration.
  if command -v sha256sum >/dev/null 2>&1; then
    printf '%s|%s|%s' "$path" "$line" "$issue" | sha256sum | cut -c1-16
  else
    printf '%s|%s|%s' "$path" "$line" "$issue" | shasum -a 256 | cut -c1-16
  fi
}

render_inline_body() {
  local sev="$1" issue="$2" fix="$3" rule_id="$4" fingerprint="${5:-}"

  local label_word fix_label

  if [ "$OUT_LANG" = "tr" ]; then
    case "$sev" in
      blocking)  label_word="Blocker" ;;
      important) label_word="Önemli" ;;
    esac
    fix_label="Öneri"
  else
    case "$sev" in
      blocking)  label_word="Blocker" ;;
      important) label_word="Important" ;;
    esac
    fix_label="Suggestion"
  fi

  # The severity is the bold label, not a coloured dot. A review comment is
  # read as technical prose, and the glyph carried no information the word did
  # not already carry.
  printf '**%s**  -  %s\n\n' "$label_word" "$issue"
  if [ -n "$fix" ] && [ "$fix" != "null" ]; then
    printf '**%s:** %s\n\n' "$fix_label" "$fix"
  fi
  if [ -n "$rule_id" ] && [ "$rule_id" != "null" ]; then
    printf '_%s_\n\n' "$rule_id"
  fi
  printf -- '---\n_Multi-Agent Review · iteration #%s_\n' "$ITERATION"
  # Dedupe marker  -  dedupe-style. Re-runs of /multi-agent:review skip a
  # finding when an existing comment carries the same fingerprint.
  if [ -n "$fingerprint" ]; then
    printf '<!-- multi-agent-finding: %s -->\n' "$fingerprint"
  fi
}

# Returns 0 if a comment carrying this fingerprint already exists on the PR.
# Cached: the full comment list is fetched once per run and grepped per finding.
EXISTING_COMMENTS_CACHE=""
load_existing_comments_github() {
  local org_repo="$1" pr_num="$2"
  if [ -z "$EXISTING_COMMENTS_CACHE" ]; then
    # Combine review-comments (inline) + issue comments (top-level fallback).
    # Use --paginate so PRs with >30 comments still dedupe correctly.
    EXISTING_COMMENTS_CACHE=$(
      {
        gh api --paginate "/repos/$org_repo/pulls/$pr_num/comments" 2>/dev/null || echo "[]"
        gh api --paginate "/repos/$org_repo/issues/$pr_num/comments" 2>/dev/null || echo "[]"
      } | jq -s 'add // []' 2>/dev/null || echo "[]"
    )
  fi
}

comment_exists_with_fingerprint() {
  local fingerprint="$1"
  [ -z "$EXISTING_COMMENTS_CACHE" ] && return 1
  jq -e --arg fp "$fingerprint" '
    [.[]?.body // ""] | any(. | contains("multi-agent-finding: " + $fp))
  ' <<<"$EXISTING_COMMENTS_CACHE" >/dev/null 2>&1
}

# Next iteration number = highest "iteration #N" already stamped on the PR + 1.
# Standalone /multi-agent:review runs use a fresh TASK_ID each time, so the PR
# comments  -  not agent-state  -  are the only cross-run source of truth. Falls
# back to 1 when no prior multi-agent comment exists. Requires the comment cache
# to be loaded first.
compute_next_iteration() {
  local max
  # Only count "iteration #N" stamps inside our own footer ("Multi-Agent
  # Review · iteration #N"), so a human comment that happens to mention
  # "iteration #99" cannot inflate the counter.
  max=$(jq -r '
    [ .[]? | .body // ""
      | select(test("Multi-Agent Review"))
      | scan("iteration #([0-9]+)") | .[0] | tonumber ]
    | (max // 0) + 1
  ' <<<"${EXISTING_COMMENTS_CACHE:-[]}" 2>/dev/null || echo "")
  case "$max" in (*[!0-9]*|"") max=1 ;; esac
  echo "$max"
}

# Bitbucket Server activities endpoint returns nested comment trees. Flatten
# them into the same `[{body: <text>}, ...]` shape that
# comment_exists_with_fingerprint expects so the dedupe gate is provider-agnostic.
load_existing_comments_bitbucket() {
  local host="$1" project="$2" slug="$3" pr_num="$4" auth_user="$5" auth_token="$6"
  if [ -z "$EXISTING_COMMENTS_CACHE" ]; then
    local base="https://$host/rest/api/1.0/projects/$project/repos/$slug/pull-requests/$pr_num"
    local raw
    raw=$(curl -sS -m 30 -K <(bb_auth_cfg "$auth_user" "$auth_token") \
      "$base/activities?fromType=COMMENT&limit=100" 2>/dev/null || echo "{}")
    EXISTING_COMMENTS_CACHE=$(jq '
      def collect_texts(c):
        if c == null then []
        else [c.text] + ([ (c.comments // [])[] | collect_texts(.) ] | add // [])
        end;
      [ .values[]?.comment | collect_texts(.)[] | {body: .} ]
    ' <<<"$raw" 2>/dev/null || echo "[]")
  fi
}

# --- Provider dispatchers ---------------------------------------------------

post_github() {
  local org_repo head_sha
  org_repo=$(jq -r '.review.input.orgRepo' "$AGENT_STATE")
  head_sha=$(jq -r '.review.headCommitSha // empty' "$AGENT_STATE")

  # Fallback: head SHA may have been written to /tmp during diff fetch.
  if [ -z "$head_sha" ] && [ -n "${TASK_ID:-}" ]; then
    local sha_file="/tmp/multi-agent-review-${TASK_ID}-head.sha"
    [ -f "$sha_file" ] && head_sha=$(tr -d '[:space:]' < "$sha_file")
  fi

  local rc_body
  if [ "$OUT_LANG" = "tr" ]; then
    rc_body="Lütfen yukarıdaki inline yorumlara bakın."
  else
    rc_body="See inline comments above."
  fi

  # Dedupe gate  -  read pref, default ON (dedupe-style). Loads existing
  # comments once if needed; per-finding check is in-process.
  local DEDUPE_ENABLED
  DEDUPE_ENABLED=$(jq -r '.global.review.dedupeInlineComments // true' \
    "$HOME/.claude/multi-agent-preferences.json" 2>/dev/null || echo "true")
  # Load existing comments on any needs_work post: dedupe needs them when
  # enabled, and the iteration counter is always derived from them.
  if [ "$DECISION" = "needs_work" ]; then
    load_existing_comments_github "$org_repo" "$PR_NUM"
    ITERATION=$(compute_next_iteration)
  fi

  if [ "$DECISION" = "needs_work" ]; then
    jq -c '(.review.findings // [])[] | select(.status == "accepted" and (.severity == "blocking" or .severity == "important"))' "$AGENT_STATE" \
    | while read -r finding; do
        local sev path line issue_b64 fix_b64 rule_id_b64 issue fix rule_id body fingerprint
        IFS=$'\t' read -r sev path line issue_b64 fix_b64 rule_id_b64 < <(
          jq -r '[.severity, .file, (.line // 0),
                  (.issue | @base64),
                  ((.fix // "") | @base64),
                  ((.ruleId // "") | @base64)] | @tsv' <<<"$finding"
        )
        issue=$(printf '%s' "$issue_b64" | base64 --decode 2>/dev/null)
        fix=$(printf '%s' "$fix_b64" | base64 --decode 2>/dev/null)
        rule_id=$(printf '%s' "$rule_id_b64" | base64 --decode 2>/dev/null)
        fingerprint=$(finding_fingerprint "$path" "$line" "$issue")
        body=$(render_inline_body "$sev" "$issue" "$fix" "$rule_id" "$fingerprint")

        if [ "$DEDUPE_ENABLED" = "true" ] && comment_exists_with_fingerprint "$fingerprint"; then
          echo "post-pr-review: dedupe skip  -  finding $fingerprint already commented" >&2
          continue
        fi

        local inline_ok=0
        if [ -n "$head_sha" ] && [ "$line" -gt 0 ] 2>/dev/null; then
          if gh api -X POST "/repos/$org_repo/pulls/$PR_NUM/comments" \
              -f "body=$body" -f "commit_id=$head_sha" -f "path=$path" \
              -F "line=$line" -f "side=RIGHT" >/dev/null 2>&1; then
            inline_ok=1
          else
            echo "post-pr-review: inline anchor failed for $path:$line (out of diff?), falling back to top-level" >&2
          fi
        fi
        if [ "$inline_ok" -eq 0 ]; then
          gh pr comment "$PR_NUM" --repo "$org_repo" --body "$body" >/dev/null \
            || echo "post-pr-review: top-level comment failed for $path:$line" >&2
        fi
      done
  fi

  case "$DECISION" in
    approve)    gh pr review "$PR_NUM" --repo "$org_repo" --approve ;;
    needs_work) gh pr review "$PR_NUM" --repo "$org_repo" --request-changes --body "$rc_body" ;;
  esac
}

post_bitbucket_server() {
  local host project slug user_slug auth_user auth_token user_key token_key prefs
  host=$(jq -r '.review.input.bbServerHost' "$AGENT_STATE")
  project=$(jq -r '.review.input.bbProjectKey' "$AGENT_STATE")
  slug=$(jq -r '.review.input.bbRepoSlug' "$AGENT_STATE")

  # Resolve credential keys from prefs.keychainMapping  -  never hardcode.
  prefs="$HOME/.claude/multi-agent-preferences.json"
  user_key=$(jq -r '.global.keychainMapping.bitbucket_user // empty' "$prefs" 2>/dev/null || true)
  token_key=$(jq -r '.global.keychainMapping.bitbucket_token // empty' "$prefs" 2>/dev/null || true)

  if [ -z "$user_key" ] || [ -z "$token_key" ]; then
    echo "post-pr-review: global.keychainMapping.bitbucket_user / .bitbucket_token unset in $prefs" >&2
    return 4
  fi

  # Resolve via the cross-platform credential helper (macOS Keychain / Windows
  # Credential Manager / Linux libsecret)  -  sourced lazily so this script keeps
  # working when run from either ~/.claude/lib or ~/.copilot/lib.
  # shellcheck disable=SC1090
  # Existence check before sourcing: `. <missing>` aborts the shell under `set -e`,
  # `||` included, so a `.`-chain reaches neither its later candidates nor its error
  # branch. The loop also covers all three hosts - the chain it replaced knew only
  # .claude and .copilot, so a Codex-only install could not resolve at all.
  for _cred_resolver in \
    "$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/credential-store-resolver.sh" \
    "$HOME/.claude/lib/credential-store-resolver.sh" \
    "$HOME/.copilot/lib/credential-store-resolver.sh" \
    "$HOME/.codex/lib/credential-store-resolver.sh"; do
    [ -f "$_cred_resolver" ] || continue
    # shellcheck source=/dev/null
    . "$_cred_resolver" 2>/dev/null || true
    if [ -n "${CRED_STORE:-}" ]; then break; fi
  done
  unset _cred_resolver
  if [ -z "${CRED_STORE:-}" ]; then
    echo "post-pr-review: credential helper not found" >&2
    return 4
  fi
  resolve_credential_store || return 4

  auth_user=$("$CRED_STORE" get "$user_key" 2>/dev/null || true)
  auth_token=$("$CRED_STORE" get "$token_key" 2>/dev/null || true)
  user_slug="$auth_user"

  if [ -z "$auth_user" ] || [ -z "$auth_token" ]; then
    echo "post-pr-review: bitbucket creds not in credential store ($user_key / $token_key)" >&2
    return 4
  fi

  local base="https://$host/rest/api/1.0/projects/$project/repos/$slug/pull-requests/$PR_NUM"

  # Dedupe gate  -  read pref, default ON. Bitbucket uses the activities feed.
  local DEDUPE_ENABLED
  DEDUPE_ENABLED=$(jq -r '.global.review.dedupeInlineComments // true' \
    "$HOME/.claude/multi-agent-preferences.json" 2>/dev/null || echo "true")
  # Load existing comments on any needs_work post: dedupe needs them when
  # enabled, and the iteration counter is always derived from them.
  if [ "$DECISION" = "needs_work" ]; then
    load_existing_comments_bitbucket "$host" "$project" "$slug" "$PR_NUM" "$auth_user" "$auth_token"
    ITERATION=$(compute_next_iteration)
  fi

  if [ "$DECISION" = "needs_work" ]; then
    jq -c '(.review.findings // [])[] | select(.status == "accepted" and (.severity == "blocking" or .severity == "important"))' "$AGENT_STATE" \
    | while read -r finding; do
        local sev path line issue_b64 fix_b64 rule_id_b64 issue fix rule_id body payload http fingerprint
        IFS=$'\t' read -r sev path line issue_b64 fix_b64 rule_id_b64 < <(
          jq -r '[.severity, .file, (.line // 0),
                  (.issue | @base64),
                  ((.fix // "") | @base64),
                  ((.ruleId // "") | @base64)] | @tsv' <<<"$finding"
        )
        issue=$(printf '%s' "$issue_b64" | base64 --decode 2>/dev/null)
        fix=$(printf '%s' "$fix_b64" | base64 --decode 2>/dev/null)
        rule_id=$(printf '%s' "$rule_id_b64" | base64 --decode 2>/dev/null)
        fingerprint=$(finding_fingerprint "$path" "$line" "$issue")
        body=$(render_inline_body "$sev" "$issue" "$fix" "$rule_id" "$fingerprint")

        if [ "$DEDUPE_ENABLED" = "true" ] && comment_exists_with_fingerprint "$fingerprint"; then
          echo "post-pr-review: dedupe skip  -  finding $fingerprint already commented (bitbucket)" >&2
          continue
        fi

        local inline_ok=0
        if [ "$line" -gt 0 ] 2>/dev/null; then
          payload=$(jq -n --arg t "$body" --arg p "$path" --argjson l "$line" \
            '{text: $t, anchor: {path: $p, line: $l, lineType: "ADDED", fileType: "TO"}}')
          http=$(curl -sS -m 30 -K <(bb_auth_cfg "$auth_user" "$auth_token") -X POST \
            -H "Content-Type: application/json" "$base/comments" \
            -d "$payload" -o /dev/null -w '%{http_code}' || echo 000)
          if [ "$http" = "200" ] || [ "$http" = "201" ]; then
            inline_ok=1
          else
            echo "post-pr-review: bitbucket inline anchor failed ($http) for $path:$line, falling back to top-level" >&2
          fi
        fi

        if [ "$inline_ok" -eq 0 ]; then
          payload=$(jq -n --arg t "$body" '{text: $t}')
          curl -sS -m 30 -K <(bb_auth_cfg "$auth_user" "$auth_token") -X POST \
            -H "Content-Type: application/json" "$base/comments" \
            -d "$payload" >/dev/null \
            || echo "post-pr-review: bitbucket top-level comment failed for $path:$line" >&2
        fi
      done
  fi

  local status_payload
  case "$DECISION" in
    approve)    status_payload='{"status":"APPROVED"}' ;;
    needs_work) status_payload='{"status":"NEEDS_WORK"}' ;;
  esac

  curl -sS -m 30 -K <(bb_auth_cfg "$auth_user" "$auth_token") -X PUT \
    -H "Content-Type: application/json" "$base/participants/$user_slug" \
    -d "$status_payload" >/dev/null
}

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

case "$PROVIDER" in
  github)
    post_github
    ;;
  bitbucket-server)
    post_bitbucket_server
    ;;
  *)
    echo "post-pr-review: unknown provider '$PROVIDER'; chat-only fallback (no PR action)" >&2
    exit 3
    ;;
esac

echo "post-pr-review: $DECISION posted to $PROVIDER PR #$PR_NUM"
