#!/usr/bin/env bash
#
# update-issue-progress.sh
#
# Synchronises the GitHub issue body's `### Progress` table with the run's
# state.flags. Idempotent  -  running twice with the same state produces the
# same body and a no-op edit.
#
# Replaces the python `update-issue-from-registry.py` referenced in the issue
# body comment legend. Same contract: the table is owned by the pipeline,
# never hand-edited.
#
# Usage:
#   update-issue-progress.sh <task-id>
#
# Inputs:
#   $HOME/.claude/logs/multi-agent/{project}/{taskId}/agent-state.json
#     .githubIssue.owner, .githubIssue.repo, .githubIssue.number
#     .flags.implementation, .flags.testing, .flags.codeConnect, .flags.wiki
#
# Side effects:
#   gh issue edit --body-file <updated body>
#
# Exit codes:
#   0 success (or no-op when body unchanged)
#   2 missing agent-state.json
#   3 missing required flags
#   4 GitHub edit failed

set -euo pipefail

# jq is not optional on this path. Without the guard below a missing binary
# renders as EMPTY DATA and the work continues on it; see lib/require-jq.sh.
for _rq in "$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/require-jq.sh" \
           "$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/../lib" 2>/dev/null && pwd)/require-jq.sh" \
           "$HOME/.claude/lib/require-jq.sh" \
           "$HOME/.copilot/lib/require-jq.sh" \
           "$HOME/.codex/lib/require-jq.sh"; do
  [ -f "$_rq" ] || continue
  # shellcheck source=/dev/null
  . "$_rq" && break
done
unset _rq
if ! command -v ma_require_jq >/dev/null 2>&1; then
  # The helper itself is missing, which is an install problem, not a jq one.
  ma_require_jq() { command -v jq >/dev/null 2>&1 || { echo "jq not found - cannot ${1:-continue}." >&2; return 1; }; }
fi
ma_require_jq "update the issue" || exit 3

# Outbound leak gate. Every byte below is composed at runtime out of command
# output, error text and file excerpts, any of which can carry a token that was
# on this machine a second earlier - and once it is in a comment it is in
# someone else's database. The repo's own leak scanner looks at FILES IN THE
# REPO and never sees this text. See lib/outbound-gate.mjs.
ma_outbound_gate() {
  local body_file="$1" og=""
  for c in "$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/outbound-gate.mjs" \
           "$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/../lib" 2>/dev/null && pwd)/outbound-gate.mjs" \
           "$HOME/.claude/lib/outbound-gate.mjs" \
           "$HOME/.copilot/lib/outbound-gate.mjs" \
           "$HOME/.codex/lib/outbound-gate.mjs"; do
    [ -f "$c" ] && { og="$c"; break; }
  done
  # Missing gate is NOT an open door: refusing to publish beats publishing
  # unchecked, and the only way this file is absent is a broken install.
  if [ -z "$og" ]; then
    echo "outbound-gate.mjs not found - refusing to publish unchecked text." >&2
    return 7
  fi
  node "$og" --file "$body_file"
}

# Run-state path resolution: pipeline/lib/run-paths.sh owns the two layouts
# (nested <root>/<project>/<id>/ and flat <root>/<id>/) and every id spelling.
_MA_RP_HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=/dev/null
if [ -f "$_MA_RP_HERE/../lib/run-paths.sh" ]; then . "$_MA_RP_HERE/../lib/run-paths.sh"; else . "$HOME/.claude/lib/run-paths.sh"; fi


# Rewrite the `### Progress` block of the body file ($2) with $1 and print the
# result to stdout. The replaced region is bounded at the FIRST of:
#   - a line starting with `<!--` (the legend comment, printed and kept), or
#   - the next heading line (^#, printed and kept), or
#   - EOF.
# Bounding at the next heading matters: bodies without the legend comment used
# to have everything from the table to EOF silently dropped and persisted.
rewrite_progress_body() {
  # Block passed via environment: BSD awk rejects literal newlines in -v
  # assignments ("newline in string"), so -v cannot carry the table.
  NEW_BLOCK_ENV="$1" awk '
    BEGIN { new_block = ENVIRON["NEW_BLOCK_ENV"] }
    BEGIN { in_progress = 0 }
    /^### Progress/ {
      print
      print ""
      print new_block
      print ""
      in_progress = 1
      next
    }
    in_progress && /^<!--/ { in_progress = 0; print; next }
    in_progress && /^#/    { in_progress = 0; print; next }
    in_progress { next }
    { print }
  ' "$2"
}

build_progress_block() {
  cat <<EOF
| Flag | State |
|---|---|
| Implementation | ${1} |
| Testing        | ${2} |
| Code Connect   | ${3} |
| Wiki           | ${4} |
EOF
}

TASK_ID="${1:-}"
if [ -z "$TASK_ID" ]; then
  echo "usage: update-issue-progress.sh <task-id>" >&2
  exit 64
fi

# Internal test seam: `--rewrite <body-file>` runs only the Progress-block
# rewrite (placeholder glyphs) and prints the result. No state, no gh.
if [ "$TASK_ID" = "--rewrite" ]; then
  BODY_IN="${2:-}"
  if [ -z "$BODY_IN" ] || [ ! -f "$BODY_IN" ]; then
    echo "usage: update-issue-progress.sh --rewrite <body-file>" >&2
    exit 64
  fi
  rewrite_progress_body "$(build_progress_block "pending" "pending" "pending" "pending")" "$BODY_IN"
  exit 0
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.
# This globbed ONLY the nested layout. phase-tracker.sh and most Phase 0 paths
# write flat, so on a real machine the majority of runs resolved to nothing and
# this script reported "no agent-state.json" for a run whose state existed.
AGENT_STATE="$(ma_resolve_run_file "$TASK_ID" agent-state.json 2>/dev/null || true)"

if [ -z "$AGENT_STATE" ]; then
  echo "update-issue-progress: no agent-state.json found for task=$TASK_ID" >&2
  exit 2
fi

# agent-state.schema.json has no `.issue`/`.repos` properties (additionalProperties
# is false) - the real GitHub-issue coordinates live at `.githubIssue.{owner,repo,number}`.
ISSUE_NUM=$(jq -r '.githubIssue.number // empty' "$AGENT_STATE")
ORG_REPO=$(jq -r 'if .githubIssue then (.githubIssue.owner + "/" + .githubIssue.repo) else empty end' "$AGENT_STATE")

if [ -z "$ORG_REPO" ] || [ -z "$ISSUE_NUM" ]; then
  echo "update-issue-progress: no githubIssue in state for task=$TASK_ID (Jira-originated task, or issue not yet linked)" >&2
  exit 3
fi

# Read the four flags (default false).
F_IMPL=$(jq -r '.flags.implementation // false' "$AGENT_STATE")
F_TEST=$(jq -r '.flags.testing       // false' "$AGENT_STATE")
F_CC=$(jq   -r '.flags.codeConnect   // false' "$AGENT_STATE")
F_WIKI=$(jq -r '.flags.wiki          // false' "$AGENT_STATE")

# Each flag may be: true | false | "yellow" (validated against older impl).
glyph_for() {
  case "$1" in
    true)   echo "done" ;;
    yellow) echo "partial" ;;
    *)      echo "pending" ;;
  esac
}

G_IMPL=$(glyph_for "$F_IMPL")
G_TEST=$(glyph_for "$F_TEST")
G_CC=$(glyph_for "$F_CC")
G_WIKI=$(glyph_for "$F_WIKI")

# Fetch current body, replace the Progress table, write back.
TMP_BODY=$(mktemp "/tmp/issue-progress-${TASK_ID}.XXXXXX")
TMP_NEW=$(mktemp "/tmp/issue-progress-${TASK_ID}.new.XXXXXX")
trap 'rm -f "$TMP_BODY" "$TMP_NEW"' EXIT

gh issue view "$ISSUE_NUM" --repo "$ORG_REPO" --json body --jq .body > "$TMP_BODY"

# Build the new Progress block.
NEW_BLOCK=$(build_progress_block "$G_IMPL" "$G_TEST" "$G_CC" "$G_WIKI")

# Replace the table between `### Progress` and the first terminator (legend
# comment, next heading, or EOF)  -  see rewrite_progress_body above.
rewrite_progress_body "$NEW_BLOCK" "$TMP_BODY" > "$TMP_NEW"

# Check if body actually changed (idempotent no-op).
if cmp -s "$TMP_BODY" "$TMP_NEW"; then
  echo "update-issue-progress: no change for #$ISSUE_NUM (idempotent no-op)" >&2
  exit 0
fi

# Apply.
if ! ma_outbound_gate "$TMP_NEW"; then
  echo "update-issue-progress: outbound gate refused the new body; #$ISSUE_NUM left unchanged" >&2
  exit 7
fi
if ! gh issue edit "$ISSUE_NUM" --repo "$ORG_REPO" --body-file "$TMP_NEW" >/dev/null; then
  echo "update-issue-progress: gh issue edit failed for #$ISSUE_NUM" >&2
  exit 4
fi

echo "update-issue-progress: updated #$ISSUE_NUM (Impl=$G_IMPL Test=$G_TEST CC=$G_CC Wiki=$G_WIKI)"
