#!/usr/bin/env bash
# Cleanup stale claims for autonomous multi-agent workflow.
#
# Освобождает issues застрявшие в `status/wip` дольше 4 часов:
# - убирает assignee
# - меняет label status/wip → status/ready (delete wip → add ready)
#
# Why: если worker crashes или window закрывается во время работы, issue
# остаётся wip+assigned forever. Без cleanup queue забивается.
#
# Run: cron каждые 30 минут (см. .forgejo/workflows/stale-claims.yml).
#
# Env vars (требуются):
#   FORGEJO_TOKEN — admin или с write:issue scope на repo
#   FORGEJO_URL   — https://git.gendsgn.ru (без /api/v1)
#   FORGEJO_REPO  — lekss361/gendesign
#   STALE_HOURS   — порог в часах (default 4)
#
# Assumption: issue.updated_at = последний touch (label/comment/assignee
# change). Worker делающий task >4h без касаний issue будет считаться stale.
# Для текущих small tasks 4h enough; в Phase 4 можно добавить heartbeat
# comment'ы от worker'а.
#
# Exit codes:
#   0 — success (even if 0 stale found)
#   1 — env vars missing / API error

set -euo pipefail

: "${FORGEJO_TOKEN:?FORGEJO_TOKEN required}"
: "${FORGEJO_URL:?FORGEJO_URL required}"
: "${FORGEJO_REPO:?FORGEJO_REPO required}"

export STALE_HOURS="${STALE_HOURS:-4}"
STALE_SECS=$((STALE_HOURS * 3600))

curl_forgejo() {
  curl -sS -H "Authorization: token $FORGEJO_TOKEN" \
       -H "Content-Type: application/json" \
       "$FORGEJO_URL/api/v1/$1" "${@:2}"
}

now_utc_epoch() {
  python3 -c 'import datetime; print(int(datetime.datetime.now(datetime.timezone.utc).timestamp()))'
}

# Pause-bots guard: если активен kill-switch — НЕ освобождать claims. Иначе worker,
# приостановленный mid-work (держит wip-claim до un-pause per _autonomous_pickup.md
# «Pause-bots поведение mid-work»), потеряет работу — cron снимет его claim.
PAUSE_CHECK=$(curl_forgejo "repos/$FORGEJO_REPO/issues?labels=pause-bots&state=open&limit=1")
PAUSE_COUNT=$(printf '%s' "$PAUSE_CHECK" | python3 -c 'import json,sys; print(len(json.load(sys.stdin)))')
if [ "$PAUSE_COUNT" != "0" ]; then
  echo "[$(date -Is)] cleanup-stale-claims: pause-bots active — skipping (workers may intentionally hold wip)"
  exit 0
fi

echo "[$(date -Is)] cleanup-stale-claims: scanning status/wip issues (stale threshold: ${STALE_HOURS}h)"

NOW=$(now_utc_epoch)
export CUTOFF=$((NOW - STALE_SECS))

# GET all open issues с label status/wip — pagination в Python (более надёжно чем
# склеивать JSON arrays в bash). Defensive cap 10 pages × 50 = 500.

ISSUES_TMP=$(mktemp)
trap 'rm -f "$ISSUES_TMP"' EXIT

for PAGE in 1 2 3 4 5 6 7 8 9 10; do
  PAGE_RESP=$(curl_forgejo "repos/$FORGEJO_REPO/issues?labels=status/wip&state=open&limit=50&page=$PAGE")
  echo "$PAGE_RESP" >> "$ISSUES_TMP"
  echo "---PAGE_END---" >> "$ISSUES_TMP"
  PAGE_LEN=$(echo "$PAGE_RESP" | python3 -c 'import json,sys; print(len(json.load(sys.stdin)))')
  if [ "$PAGE_LEN" -lt 50 ]; then
    break
  fi
done

# Парсим JSON — читаем файл прямо в Python через env var (не через pipe `cat | python3`).
# При pipe + sys.exit(0) после print, cat получает SIGPIPE → script exits 141 под
# set -o pipefail. Передача path через env var не использует pipe → no SIGPIPE.
ISSUES_FILE="$ISSUES_TMP" python3 <<'PYEOF'
import json, os, subprocess, sys, datetime

cutoff = int(os.environ.get("CUTOFF", "0"))
if cutoff == 0:
    print("[ERROR] CUTOFF env var не выставлен или равен 0 — abort", file=sys.stderr)
    sys.exit(1)

# Read pagination output — multiple JSON arrays separated by "---PAGE_END---"
with open(os.environ["ISSUES_FILE"]) as f:
    raw = f.read()
pages = [p.strip() for p in raw.split("---PAGE_END---") if p.strip()]
data = []
for p in pages:
    try:
        data.extend(json.loads(p))
    except json.JSONDecodeError:
        print(f"[WARN] failed to parse page (len={len(p)}), skipping", file=sys.stderr)

if not data:
    print("[OK] 0 wip issues — nothing to do")
    sys.exit(0)

released = 0
errors = 0
now_epoch = int(datetime.datetime.now(datetime.timezone.utc).timestamp())

for issue in data:
    issue_num = issue["number"]
    updated_at = issue["updated_at"]
    title = issue["title"]
    assignees = [a["login"] for a in (issue.get("assignees") or [])]

    updated_epoch = int(datetime.datetime.fromisoformat(updated_at.replace("Z", "+00:00")).timestamp())
    age_secs = now_epoch - updated_epoch
    age_hours = age_secs // 3600

    if updated_epoch >= cutoff:
        print(f"  · #{issue_num} '{title[:50]}' — age {age_hours}h, fresh, skip")
        continue

    print(f"  ⚠ #{issue_num} '{title[:50]}' — age {age_hours}h, STALE")
    print(f"    assignees: {assignees}")
    print(f"    → releasing claim")

    repo = os.environ["FORGEJO_REPO"]
    base = os.environ["FORGEJO_URL"] + "/api/v1"
    token = os.environ["FORGEJO_TOKEN"]
    auth = ["-H", f"Authorization: token {token}", "-H", "Content-Type: application/json"]

    try:
        # 1. Clear assignees
        subprocess.run(
            ["curl", "-sS", "-X", "PATCH", *auth, "-d", '{"assignees": []}',
             f"{base}/repos/{repo}/issues/{issue_num}"],
            check=True, capture_output=True,
        )

        # 2. Remove status/wip label FIRST (если падает — issue в neutral state без status/*,
        #    что менее опасно чем оба status/ready+status/wip одновременно)
        subprocess.run(
            ["curl", "-sS", "-X", "DELETE", *auth,
             f"{base}/repos/{repo}/issues/{issue_num}/labels/status%2Fwip"],
            check=True, capture_output=True,
        )

        # 3. Add status/ready label
        subprocess.run(
            ["curl", "-sS", "-X", "POST", *auth, "-d", '{"labels": ["status/ready"]}',
             f"{base}/repos/{repo}/issues/{issue_num}/labels"],
            check=True, capture_output=True,
        )

        # 4. Post comment for trace
        comment_body = json.dumps({
            "body": f"⚠️ Stale claim released: issue stuck in status/wip for {age_hours}h "
                    f"(previously assigned to {', '.join(assignees) or 'none'}). "
                    f"Label transition: status/wip → status/ready. Worker likely crashed."
        })
        subprocess.run(
            ["curl", "-sS", "-X", "POST", *auth, "-d", comment_body,
             f"{base}/repos/{repo}/issues/{issue_num}/comments"],
            check=True, capture_output=True,
        )

        released += 1
    except subprocess.CalledProcessError as e:
        print(f"    ✗ FAILED: {e.stderr.decode() if e.stderr else 'unknown'}")
        errors += 1

print()
print(f"[OK] Released {released} stale claim(s), {errors} errors")
sys.exit(1 if errors else 0)
PYEOF
