#!/usr/bin/env bash
# SPDX-License-Identifier: MIT
# SPDX-FileCopyrightText: 2025-2026 Marcus Quinn
# =============================================================================
# Issue Sync Helper — Close & Reopen Commands
# =============================================================================
# Close helpers (_do_close, evidence checks, PR lookup) plus the cmd_close
# and cmd_reopen entry points.
#
# Usage: source "${SCRIPT_DIR}/issue-sync-helper-close.sh"
#
# Dependencies:
#   - shared-constants.sh (print_error, print_info, print_warning, print_success)
#   - issue-sync-lib.sh (_escape_ere, extract_task_block, strip_code_fences,
#     add_gh_ref_to_todo, add_pr_ref_to_todo, sed_inplace)
#   - issue-sync-helper-labels.sh (gh_create_label, _gh_edit_labels,
#     _mark_issue_done, gh_find_issue_by_title, gh_find_merged_pr, gh_list_issues)
#
# Part of aidevops framework: https://aidevops.sh

# Apply strict mode only when executed directly (not when sourced)
[[ "${BASH_SOURCE[0]}" == "${0}" ]] && set -euo pipefail

# Include guard
[[ -n "${_ISSUE_SYNC_HELPER_CLOSE_LOADED:-}" ]] && return 0
_ISSUE_SYNC_HELPER_CLOSE_LOADED=1

# Defensive SCRIPT_DIR fallback
if [[ -z "${SCRIPT_DIR:-}" ]]; then
	_lib_path="${BASH_SOURCE[0]%/*}"
	[[ "$_lib_path" == "${BASH_SOURCE[0]}" ]] && _lib_path="."
	SCRIPT_DIR="$(cd "$_lib_path" && pwd)"
	unset _lib_path
fi

# =============================================================================
# Close Helpers
# =============================================================================

# _is_cancelled_or_deferred: returns 0 if the task text indicates it was
# cancelled, deferred, or declined — these states require no PR/verified evidence.
_is_cancelled_or_deferred() {
	local text="$1"
	echo "$text" | grep -qiE 'cancelled:[0-9]{4}-[0-9]{2}-[0-9]{2}|deferred:[0-9]{4}-[0-9]{2}-[0-9]{2}|declined:[0-9]{4}-[0-9]{2}-[0-9]{2}|CANCELLED' && return 0
	return 1
}

_has_evidence() {
	local text="$1" task_id="$2" repo="$3"
	local task_line
	task_line=$(_task_line_from_block "$text" "$task_id")
	# Cancelled/deferred/declined tasks need no PR or verified: evidence
	_is_cancelled_or_deferred "$task_line" && return 0
	if _has_unresolved_blocker "$text" "$task_id" "$task_line"; then
		return 1
	fi
	echo "$text" | grep -qE 'verified:[0-9]{4}-[0-9]{2}-[0-9]{2}|pr:#[0-9]+' && return 0
	echo "$text" | grep -qiE 'PR #[0-9]+ merged|PR.*merged' && return 0
	[[ -n "$repo" && -n "$task_id" ]] && return 1
	return 1
}

_task_line_from_block() {
	local text="$1" task_id="${2:-}"
	local task_line
	if [[ -n "$task_id" ]]; then
		task_line=$(printf '%s\n' "$text" | grep -F " $task_id " | grep -E '^[[:space:]]*- \[.\] ' | head -1 || true)
	fi
	[[ -z "$task_line" ]] && task_line=$(printf '%s\n' "$text" | grep -E '^[[:space:]]*- \[.\] ' | head -1 || true)
	[[ -z "$task_line" ]] && task_line=$(printf '%s\n' "$text" | head -1)
	printf '%s\n' "$task_line"
	return 0
}

_has_unresolved_blocker() {
	local text="$1"
	local task_id="${2:-}"
	local task_line="${3:-}"
	local candidate
	candidate="${task_line:-$(_task_line_from_block "$text" "$task_id")}"
	printf '%s\n' "$candidate" | grep -qE '(^|[[:space:]])blocked-by:[^[:space:]]+' && return 0
	return 1
}

_find_closing_pr() {
	local text="$1" task_id="$2" repo="$3"
	local pr
	pr=$(echo "$text" | grep -oE 'pr:#[0-9]+|PR #[0-9]+' | head -1 | grep -oE '[0-9]+' || echo "")
	[[ -n "$pr" ]] && {
		echo "${pr}|https://github.com/${repo}/pull/${pr}"
		return 0
	}
	[[ -n "$repo" && -n "$task_id" ]] && return 1
	return 1
}

# t3204: append signature footer to close-comment bodies. Without it the
# auto-close path (gh issue close --comment from _do_close, which bypasses the
# `gh` PATH shim because `issue:close` is not in the shim's intercept list)
# emitted bare one-liners with no runtime/version/model/token metadata —
# in stark contrast to the rich + signed comments produced by interactive
# agents using gh_issue_comment / the shim. See AGENTS.md "Signature footer
# hallucination (t2685)" for the canonical rule.
_close_comment() {
	local task_id="$1" text="$2" pr_num="$3" pr_url="$4" repo="${5:-}" issue_number="${6:-}"
	local body
	# Cancelled/deferred/declined: produce a not-planned comment (no PR needed)
	if _is_cancelled_or_deferred "$text"; then
		local reason
		reason=$(echo "$text" | grep -oiE 'cancelled:[0-9-]+|deferred:[0-9-]+|declined:[0-9-]+|CANCELLED' | head -1 | tr '[:upper:]' '[:lower:]')
		[[ -z "$reason" ]] && reason="cancelled"
		body="Closing as not planned ($reason). Task $task_id resolved in TODO.md."
	elif [[ -n "$pr_num" && -n "$pr_url" ]]; then
		body="Completed via [PR #${pr_num}](${pr_url}). Task $task_id done in TODO.md."
	elif [[ -n "$pr_num" ]]; then
		body="Completed via PR #${pr_num}. Task $task_id done in TODO.md."
	else
		local d
		d=$(echo "$text" | grep -oE 'verified:[0-9-]+' | head -1 | sed 's/verified://')
		if [[ -n "$d" ]]; then
			body="Completed (verified: $d). Task $task_id done in TODO.md."
		else
			body="Completed. Task $task_id done in TODO.md."
		fi
	fi

	# t3204: append signature footer (--solved since this comment marks issue close).
	# When repo/issue_number are passed, the helper sums total session time + tokens
	# across all worker comments on this issue. Fail-open: missing args or helper
	# failure leaves the body un-signed rather than emitting nothing.
	local footer=""
	if [[ -n "$repo" && -n "$issue_number" ]]; then
		footer=$(gh-signature-helper.sh footer --solved --issue "${repo}#${issue_number}" 2>/dev/null || true)
	else
		footer=$(gh-signature-helper.sh footer --solved 2>/dev/null || true)
	fi
	printf '%s%s\n' "$body" "$footer"
	return 0
}

# t3204: reopen-comment composition extracted into a helper so the signature
# footer block and the body text are testable in isolation. Called by cmd_reopen
# below; the helper output replaces the previous inline --comment string.
_reopen_comment() {
	local repo="${1:-}" ref_num="${2:-}"
	local body="Reopened: TODO.md still has this as \`[ ]\` (open) and no merged PR was found. The issue was prematurely closed by a commit keyword. TODO.md is the source of truth for task state."
	local footer=""
	if [[ -n "$repo" && -n "$ref_num" ]]; then
		footer=$(gh-signature-helper.sh footer --issue "${repo}#${ref_num}" 2>/dev/null || true)
	else
		footer=$(gh-signature-helper.sh footer 2>/dev/null || true)
	fi
	printf '%s%s\n' "$body" "$footer"
	return 0
}

_reopen_ref_is_pull_request() {
	local repo="$1" ref_num="$2" argc="$#" prefetched_issue_json="${3-}"
	local issue_json
	if [[ "$argc" -ge 3 ]]; then
		issue_json="$prefetched_issue_json"
	else
		issue_json=$(gh api "repos/${repo}/issues/${ref_num}" 2>/dev/null) || return 1
	fi
	printf '%s\n' "$issue_json" | jq -e 'select(type == "object") | has("pull_request")' >/dev/null 2>&1 && return 0
	return 1
}

_has_prior_reopen_comment() {
	local repo="$1" ref_num="$2"
	local comments_json found
	comments_json=$(gh api "repos/${repo}/issues/${ref_num}/comments" 2>/dev/null || printf '[]')
	found=$(printf '%s\n' "$comments_json" | jq -r 'if type == "array" then [.[] | select(.body? | strings | contains("Reopened: TODO.md still has this as `[ ]`"))] | length else 0 end' 2>/dev/null || printf '0')
	[[ "$found" =~ ^[0-9]+$ ]] || found=0
	if [[ "$found" -gt 0 ]]; then
		return 0
	fi
	return 1
}

_is_not_planned_state_reason() {
	local reason="${1:-}"
	local normalized
	normalized=$(printf '%s' "$reason" | tr '[:upper:]' '[:lower:]' | tr '-' '_')
	[[ "$normalized" == "not_planned" ]] && return 0
	return 1
}

# Mark a TODO entry as done: [ ] -> [x] with completed: date.
# Also handles [-] (cancelled/declined) entries — leaves marker as [-].
_mark_todo_done() {
	local task_id="$1" todo_file="$2" proof_log="${3:-}"
	local task_id_ere
	task_id_ere=$(_escape_ere "$task_id")
	local today
	today=$(date -u +%Y-%m-%d)
	[[ -n "$proof_log" && "$proof_log" != " "* ]] && proof_log=" $proof_log"

	# Only flip [ ] -> [x]; skip if already [x] or [-]
	# Use [[:space:]] not \s for macOS sed compatibility (bash 3.2)
	if grep -qE "^[[:space:]]*- \[ \] ${task_id_ere} " "$todo_file" 2>/dev/null; then
		# Flip checkbox and append completed: date
		sed -i.bak -E "s/^([[:space:]]*- )\[ \] (${task_id_ere} .*)/\1[x] \2${proof_log} completed:${today}/" "$todo_file"
		rm -f "${todo_file}.bak"
		_dedupe_todo_task_lines "$task_id" "$todo_file" || true
		log_verbose "Marked $task_id as [x] in TODO.md"
	fi
	return 0
}

_closed_issue_worker_complete_date() {
	local repo="$1" issue_number="$2"
	local completed_at

	completed_at=$(gh api "repos/${repo}/issues/${issue_number}/comments" \
		--jq '[.[] | select((.body // "") | contains("CLAIM_RELEASED reason=worker_complete")) | .created_at][0] // ""' || true)
	if [[ -z "$completed_at" ]]; then
		return 1
	fi
	printf '%s\n' "${completed_at%%T*}"
	return 0
}

_closed_issue_aidevops_complete_date() {
	local repo="$1" issue_number="$2"
	local issue_json="" state_reason="" closed_at="" evidence=""

	issue_json=$(gh issue view "$issue_number" --repo "$repo" \
		--json state,stateReason,closedAt,body,comments 2>/dev/null) || return 1
	state_reason=$(printf '%s' "$issue_json" | jq -r '.stateReason // ""' 2>/dev/null) || state_reason=""
	closed_at=$(printf '%s' "$issue_json" | jq -r '.closedAt // ""' 2>/dev/null) || closed_at=""
	evidence=$(printf '%s' "$issue_json" | jq -r '[.body // "", (.comments[]?.body // "")] | join("\n---\n")' 2>/dev/null) || evidence=""

	[[ "$state_reason" == "COMPLETED" ]] || return 1
	if printf '%s' "$evidence" | grep -qE 'aidevops:sig|CLAIM_RELEASED reason=worker_complete|Task t[0-9]+(\.[0-9]+)* done in TODO\.md|Completed via (\[)?PR #[0-9]+'; then
		printf '%s\n' "${closed_at%%T*}"
		return 0
	fi
	return 1
}

_mark_reopen_completed_task() {
	local tid="$1" todo_file="$2" ref_num="$3" proof_date="$4" reason="$5"
	if [[ "$DRY_RUN" == "true" ]]; then
		print_info "[DRY-RUN] Would mark $tid [x] ($reason on #$ref_num)"
		return 0
	fi
	_mark_todo_done "$tid" "$todo_file" "verified:${proof_date}"
	log_verbose "#$ref_num ($tid) has $reason — marked TODO [x]"
	return 0
}

_mark_reopen_merged_pr_task() {
	local tid="$1" todo_file="$2" ref_num="$3" pr_num="$4"
	if [[ "$DRY_RUN" == "true" ]]; then
		print_info "[DRY-RUN] Would mark $tid [x] (merged PR #$pr_num)"
		return 0
	fi
	add_pr_ref_to_todo "$tid" "$pr_num" "$todo_file" 2>/dev/null || true
	_mark_todo_done "$tid" "$todo_file"
	log_verbose "#$ref_num ($tid) has merged PR #$pr_num — marked TODO [x]"
	return 0
}

_reopen_mark_if_completed() {
	local repo="$1" tid="$2" ref_num="$3" todo_file="$4"
	local pr_info
	pr_info=$(gh_find_merged_pr "$repo" "$tid" 2>/dev/null || echo "")
	if [[ -n "$pr_info" ]]; then
		local pr_num="${pr_info%%|*}"
		_mark_reopen_merged_pr_task "$tid" "$todo_file" "$ref_num" "$pr_num"
		return 0
	fi

	local completed_date
	completed_date=$(_closed_issue_worker_complete_date "$repo" "$ref_num" || true)
	if [[ -n "$completed_date" ]]; then
		_mark_reopen_completed_task "$tid" "$todo_file" "$ref_num" "$completed_date" "worker_complete evidence"
		return 0
	fi

	completed_date=$(_closed_issue_aidevops_complete_date "$repo" "$ref_num" || true)
	if [[ -n "$completed_date" ]]; then
		_mark_reopen_completed_task "$tid" "$todo_file" "$ref_num" "$completed_date" "aidevops close evidence"
		return 0
	fi
	return 1
}

_do_close() {
	local task_id="$1" issue_number="$2" todo_file="$3" repo="$4"
	local task_id_ere
	task_id_ere=$(_escape_ere "$task_id")
	local task_with_notes task_line pr_info pr_num="" pr_url=""
	task_with_notes=$(extract_task_block "$task_id" "$todo_file")
	task_line=$(strip_code_fences <"$todo_file" | grep -E "^\s*- \[.\] ${task_id_ere} " | head -1 || echo "")
	[[ -z "$task_with_notes" ]] && task_with_notes="$task_line"

	# GH#20828: probe parent-task label before closing. The workflow path's
	# title-fallback close was guarded by t2137 (issue-sync-reusable.yml:480-484);
	# this is the parallel guard for the bash-helper close path that runs from
	# TODO.md `[x]` pushes. A `parent-task`-labelled issue must stay open until
	# its terminal-phase PR merges with `Closes #NNN` (per the t2046 For/Ref
	# convention). Skip the close + skip status:done; the TODO entry is left
	# unchanged so a human can decide whether the `[x]` was premature or
	# whether the parent should be closed via terminal PR.
	local issue_labels
	issue_labels=$(gh api "repos/${repo}/issues/${issue_number}" --jq '[.labels[].name] | join(" ")' 2>/dev/null || echo "")
	if echo "$issue_labels" | grep -qw "parent-task"; then
		print_info "Skipping #$issue_number ($task_id): parent-task label set — parent issues close via terminal-phase PR with explicit Closes #NNN, not TODO [x] (GH#20828)"
		return 0
	fi
	if ! _is_cancelled_or_deferred "$task_line" && _has_unresolved_blocker "$task_line" "" "$task_line"; then
		print_info "Skipping #$issue_number ($task_id): unresolved blocked-by marker present — completion evidence must wait for dependencies (GH#23516)"
		return 0
	fi

	pr_info=$(_find_closing_pr "$task_with_notes" "$task_id" "$repo" 2>/dev/null || echo "")
	if [[ -n "$pr_info" ]]; then
		pr_num="${pr_info%%|*}"
		pr_url="${pr_info#*|}"
		[[ "$DRY_RUN" != "true" && -n "$pr_num" ]] && add_pr_ref_to_todo "$task_id" "$pr_num" "$todo_file"
		task_line=$(strip_code_fences <"$todo_file" | grep -E "^\s*- \[.\] ${task_id_ere} " | head -1 || echo "")
		task_with_notes=$(extract_task_block "$task_id" "$todo_file")
		[[ -z "$task_with_notes" ]] && task_with_notes="$task_line"
	fi

	if [[ "$FORCE_CLOSE" == "true" ]]; then
		print_info "FORCE_CLOSE active — bypassing evidence check for #$issue_number ($task_id) (GH#20146 audit)"
	fi
	if [[ "$FORCE_CLOSE" != "true" ]] && ! _has_evidence "$task_with_notes" "$task_id" "$repo"; then
		print_warning "Skipping #$issue_number ($task_id): no merged PR or verified: field"
		return 1
	fi

	local comment
	# t3204: pass repo + issue_number so _close_comment can ask the signature
	# helper for an issue-scoped footer (sums total session time and tokens
	# across all worker comments on this issue, not just this invocation).
	comment=$(_close_comment "$task_id" "$task_with_notes" "$pr_num" "$pr_url" "$repo" "$issue_number")
	if [[ "$DRY_RUN" == "true" ]]; then
		print_info "[DRY-RUN] Would close #$issue_number ($task_id)"
		return 0
	fi
	# Cancelled/deferred/declined tasks close as "not planned"; completed tasks use default reason
	local close_args=("issue" "close" "$issue_number" "--repo" "$repo" "--comment" "$comment")
	if _is_cancelled_or_deferred "$task_line"; then
		close_args+=("--reason" "not planned")
		gh_create_label "$repo" "not-planned" "E4E669" "Closed as not planned"
	fi
	if gh "${close_args[@]}" 2>/dev/null; then
		if _is_cancelled_or_deferred "$task_line"; then
			_gh_edit_labels "add" "$repo" "$issue_number" "not-planned"
		fi
		_mark_issue_done "$repo" "$issue_number"
		_mark_todo_done "$task_id" "$todo_file"
		print_success "Closed #$issue_number ($task_id)"
	else
		print_error "Failed to close #$issue_number ($task_id)"
		return 1
	fi
}

# =============================================================================
# cmd_close
# =============================================================================

cmd_close() {
	local target_task="${1:-}"
	_init_cmd || return 1
	local repo="$_CMD_REPO" todo_file="$_CMD_TODO"

	# Single-task mode
	if [[ -n "$target_task" ]]; then
		local target_ere
		target_ere=$(_escape_ere "$target_task")
		local task_line
		task_line=$(strip_code_fences <"$todo_file" | grep -E "^\s*- \[.\] ${target_ere} " | head -1 || echo "")
		local num
		num=$(echo "$task_line" | grep -oE 'ref:GH#[0-9]+' | head -1 | sed 's/ref:GH#//' || echo "")
		if [[ -z "$num" ]]; then
			num=$(gh_find_issue_by_title "$repo" "${target_task}:" "open" 500)
			[[ -n "$num" && "$DRY_RUN" != "true" ]] && add_gh_ref_to_todo "$target_task" "$num" "$todo_file"
		fi
		[[ -z "$num" ]] && {
			print_info "$target_task: no matching issue"
			return 0
		}
		local st
		st=$(gh issue view "$num" --repo "$repo" --json state --jq '.state' 2>/dev/null || echo "")
		[[ "$st" == "CLOSED" || "$st" == "closed" ]] && {
			log_verbose "#$num already closed"
			return 0
		}
		_do_close "$target_task" "$num" "$todo_file" "$repo" || true
		return 0
	fi

	# Bulk mode: fetch all open issues, build task->issue map
	local open_json
	open_json=$(gh_list_issues "$repo" "open" 500)
	local map=""
	while IFS='|' read -r n t; do
		[[ -z "$n" ]] && continue
		local tid
		tid=$(echo "$t" | grep -oE '^t[0-9]+(\.[0-9]+)*' || echo "")
		[[ -n "$tid" ]] && map="${map}${tid}|${n}"$'\n'
	done < <(echo "$open_json" | jq -r '.[] | "\(.number)|\(.title)"' 2>/dev/null || true)
	[[ -z "$map" ]] && {
		print_info "No open issues to close"
		return 0
	}

	local closed=0 skipped=0 ref_fixed=0
	while IFS= read -r line; do
		local task_id
		task_id=$(echo "$line" | grep -oE 't[0-9]+(\.[0-9]+)*' | head -1 || echo "")
		[[ -z "$task_id" ]] && continue
		local task_id_ere
		task_id_ere=$(_escape_ere "$task_id")
		local mapped
		mapped=$(echo "$map" | grep -E "^${task_id_ere}\|" | head -1 || echo "")
		[[ -z "$mapped" ]] && continue
		local issue_num="${mapped#*|}"
		local ref
		ref=$(echo "$line" | grep -oE 'ref:GH#[0-9]+' | head -1 | sed 's/ref:GH#//' || echo "")
		if [[ "$DRY_RUN" != "true" ]]; then
			if [[ -n "$ref" && "$ref" != "$issue_num" ]]; then
				fix_gh_ref_in_todo "$task_id" "$ref" "$issue_num" "$todo_file"
				ref_fixed=$((ref_fixed + 1))
			elif [[ -z "$ref" ]]; then
				add_gh_ref_to_todo "$task_id" "$issue_num" "$todo_file"
				ref_fixed=$((ref_fixed + 1))
			fi
		fi
		if _do_close "$task_id" "$issue_num" "$todo_file" "$repo"; then closed=$((closed + 1)); else skipped=$((skipped + 1)); fi
	done < <(strip_code_fences <"$todo_file" | grep -E '^\s*- \[(x|-)\] t[0-9]+' || true)
	print_info "Close: $closed closed, $skipped skipped, $ref_fixed refs fixed"
}

# =============================================================================
# cmd_reopen
# =============================================================================

# Reopen closed GitHub issues whose TODO entries are still open [ ].
# TODO.md is the source of truth: if a task is [ ], the work is not done,
# regardless of whether a commit message prematurely closed the issue.
#
# Decision tree per closed issue:
#   NOT_PLANNED         -> skip (deliberately declined)
#   COMPLETED + has PR  -> skip (work done, TODO needs marking [x] separately)
#   COMPLETED + no PR   -> reopen (premature closure from commit keyword)
cmd_reopen() {
	_init_cmd || return 1
	local repo="$_CMD_REPO" todo_file="$_CMD_TODO"

	# Build set of open issue numbers for fast lookup
	local open_json
	open_json=$(gh_list_issues "$repo" "open" 500)
	local open_numbers
	open_numbers=$(echo "$open_json" | jq -r '.[].number' 2>/dev/null | sort -n)

	local stripped
	stripped=$(strip_code_fences <"$todo_file")
	local reopened=0 skipped=0 not_planned=0 has_pr=0 pr_refs=0 duplicate_comments=0

	while IFS= read -r line; do
		local ref_num
		ref_num=$(echo "$line" | grep -oE 'ref:GH#[0-9]+' | head -1 | sed 's/ref:GH#//' || echo "")
		[[ -z "$ref_num" ]] && continue

		# Skip if already open
		echo "$open_numbers" | grep -qx "$ref_num" && continue

		local issue_json
		issue_json=$(gh api "repos/${repo}/issues/${ref_num}" 2>/dev/null || printf '{}')

		# GH#2888: GitHub shares the issue-number namespace with pull requests.
		# TODO refs occasionally point at PRs (or stale historical lines once did),
		# and `gh issue reopen --comment` can still append the reopen comment to a
		# merged PR even though it cannot turn that PR back into an open issue. That
		# produced repeated notification churn on closed/merged PR threads. Treat PR
		# refs as terminal artifacts, never reopen targets.
		if _reopen_ref_is_pull_request "$repo" "$ref_num" "$issue_json"; then
			log_verbose "#$ref_num is a pull request — skipping TODO reopen guard"
			pr_refs=$((pr_refs + 1))
			continue
		fi

		local tid
		tid=$(echo "$line" | grep -oE 't[0-9]+(\.[0-9]+)*' | head -1 || echo "")

		# Check closure reason — skip not_planned (deliberately declined).
		# GitHub GraphQL returns NOT_PLANNED while REST returns not_planned;
		# normalize before comparing so consolidated/not-planned tasks are not
		# accidentally reopened by the TODO source-of-truth guard.
		local reason
		reason=$(printf '%s\n' "$issue_json" | jq -r 'select(type == "object") | .state_reason // .stateReason // ""' 2>/dev/null || printf '')
		if _is_not_planned_state_reason "$reason"; then
			log_verbose "#$ref_num ($tid) closed as not_planned — skipping"
			not_planned=$((not_planned + 1))
			continue
		fi

		# If closure evidence exists, mark TODO done instead of reopening.
		if _reopen_mark_if_completed "$repo" "$tid" "$ref_num" "$todo_file"; then
			has_pr=$((has_pr + 1))
			continue
		fi

		if [[ "$DRY_RUN" == "true" ]]; then
			print_info "[DRY-RUN] Would reopen #$ref_num ($tid)"
			reopened=$((reopened + 1))
			continue
		fi

		# If a previous run already emitted the canonical reopen explanation, do
		# not post another identical notification. Valid issues are open after the
		# first successful run; still-closed refs are anomalous and should be quiet.
		if _has_prior_reopen_comment "$repo" "$ref_num"; then
			log_verbose "#$ref_num ($tid) already has a TODO-source reopen comment — suppressing duplicate notification"
			duplicate_comments=$((duplicate_comments + 1))
			continue
		fi

		# t3204: compose the reopen comment via _reopen_comment so the body
		# carries a signature footer like every other agent-authored comment.
		# `gh issue reopen --comment` is NOT intercepted by the gh PATH shim
		# (only issue:comment / issue:create / issue:edit / pr:* are), so the
		# footer must be injected here in the helper rather than relying on
		# the shim layer.
		local reopen_comment_body
		reopen_comment_body=$(_reopen_comment "$repo" "$ref_num")
		gh issue reopen "$ref_num" --repo "$repo" \
			--comment "$reopen_comment_body" 2>/dev/null && {
			reopened=$((reopened + 1))
			print_success "Reopened #$ref_num ($tid)"
		} || {
			skipped=$((skipped + 1))
			print_warning "Failed to reopen #$ref_num ($tid)"
		}
	done < <(echo "$stripped" | grep -E '^\s*- \[ \] t[0-9]+.*ref:GH#[0-9]+' || true)

	print_info "Reopen: $reopened reopened, $skipped failed, $not_planned not-planned, $has_pr have-merged-pr, $pr_refs pr-refs-skipped, $duplicate_comments duplicate-comments-skipped"
	return 0
}
