// render-prs.js — PR tracker rendering functions extracted from dashboard.html

let allPrs = [];
let prPage = 0;
const PR_PER_PAGE = 25;

// Fetch each project's pull-requests.json straight off disk through the
// /state/projects/<name>/pull-requests.json static-file passthrough. This
// is the issue #2949 escape hatch — the engine writes PR status / review
// state / build state directly into pull-requests.json, and the mtime-ETag
// on /state/ guarantees a fresh read on the next 4 s poll regardless of
// whether the /api/status outer cache has busted.
//
// Engine-side enrichment is minimal: stamping `_project` from the project
// name (the filename context) and back-filling `url` from `prUrlBase` when
// missing. Both are applied here to keep parity with the legacy slice.
// Other engine-derived fields (build status from CI poll, reviewStatus,
// _buildStatusStale) are PERSISTED to disk by the engine's PR pollers, so
// they survive the round-trip naturally.
async function fetchPullRequestsFromDisk(projects) {
  if (!Array.isArray(projects) || !projects.length) return null;
  const results = await Promise.all(projects.map(async (p) => {
    const name = p && (p.name || p.path);
    if (!name) return [];
    const urlBase = (p && p.prUrlBase) || '';
    try {
      const r = await fetch('/state/projects/' + encodeURIComponent(name) + '/pull-requests.json');
      if (!r.ok) return [];
      const arr = await r.json();
      if (!Array.isArray(arr)) return [];
      return arr.map((pr) => {
        if (!pr || typeof pr !== 'object' || !pr.id) return null;
        const stamped = Object.assign({ _project: name }, pr);
        if (!stamped.url && urlBase && typeof stamped.number === 'number') {
          stamped.url = urlBase + stamped.number;
        }
        return stamped;
      }).filter(Boolean);
    } catch {
      return [];
    }
  }));
  const out = [];
  const seen = new Set();
  for (const arr of results) {
    for (const pr of arr) {
      if (seen.has(pr.id)) continue;
      seen.add(pr.id);
      out.push(pr);
    }
  }
  // Same sort the engine applies: most recent created first, fall back to id.
  out.sort((a, b) => {
    const ac = a.created || '';
    const bc = b.created || '';
    if (ac && bc) return bc.localeCompare(ac);
    return (b.id || '').localeCompare(a.id || '');
  });
  return out;
}

function _countPrFollowups(pr) {
  // PR follow-up chip (W-mpej3cox00099466) — counts WIs whose
  // meta.pr_followup.parent_pr_url or parent_pr_id matches this PR.
  if (!pr) return 0;
  var wis = (window._lastWorkItems) || [];
  if (!wis.length) return 0;
  var prUrl = pr.url || '';
  var prId = pr.id || '';
  var n = 0;
  for (var i = 0; i < wis.length; i++) {
    var f = wis[i] && wis[i].meta && wis[i].meta.pr_followup;
    if (!f) continue;
    if (prUrl && f.parent_pr_url === prUrl) { n++; continue; }
    if (prId && f.parent_pr_id === prId) { n++; }
  }
  return n;
}

// W-mpmwxkzm0009ba0b — Parse a canonical PR id like `github:octo/repo#123`
// or `ado:org/proj/repo#456` into the {host, slug, number} triple used by
// POST /api/pull-requests/observe. Returns null when the id doesn't match.
function _parseCanonicalPrId(id) {
  if (!id || typeof id !== 'string') return null;
  var m = id.match(/^(github|ado):(.+)#(\d+)$/);
  if (!m) return null;
  return { host: m[1], slug: m[2], number: parseInt(m[3], 10) };
}

function prRow(pr) {
  // Minions review (agent) state — separate from ADO human review
  const sq = pr.minionsReview || {};
  // If PR is merged/abandoned, treat 'waiting' review as resolved
  const effectiveReviewStatus = (pr.status === 'merged' || pr.status === 'abandoned') && pr.reviewStatus === 'waiting' ? (pr.status === 'merged' ? 'approved' : 'pending') : pr.reviewStatus;
  const reviewSource = sq.status || effectiveReviewStatus || 'pending';
  const reviewClass = reviewSource === 'approved' ? 'approved' : (reviewSource === 'changes-requested' || reviewSource === 'rejected') ? 'rejected' : reviewSource === 'waiting' ? 'building' : 'draft';
  const reviewLabel = sq.status === 'waiting' ? 'reviewing (minions)' : sq.status ? sq.status + ' (minions)' : (effectiveReviewStatus || 'pending');
  const reviewTitle = '';
  const buildClass = pr._buildStatusStale ? 'build-stale' : pr.buildStatus === 'passing' ? 'build-pass' : pr.buildStatus === 'failing' ? 'build-fail' : pr.buildStatus === 'running' ? 'building' : 'no-build';
  const buildLabel = (pr.buildStatus || 'none') + (pr._buildStatusStale ? ' (stale)' : '') + (pr.buildStaleMergeCommit ? ' (stale merge commit)' : '');
  const buildTitle = pr.buildStaleMergeCommit
    ? 'ADO has builds on this PR\'s merge ref, but none target the current merge commit (typically after a rebase or target-branch advance). Engine classified from the pre-rebase build set and queued a fresh build (rate limited 30m). See Issue #2747.'
    : (pr._buildStatusDetail || '');
  const statusClass = pr.status === 'merged' ? 'merged' : pr.status === 'abandoned' ? 'rejected' : pr.status === 'active' ? 'active' : 'draft';
  const statusLabel = pr.status || 'active';
  const branchError = pr._branchResolutionError?.reason || '';
  const branchLabel = pr.branch || (branchError ? 'missing branch' : '—');
  const branchClass = 'pr-branch' + (branchError ? ' branch-missing' : '');
  const url = pr.url || '#';
  const prId = pr.id || '—';
  const pendingReason = pr._pendingReason ? String(pr._pendingReason) : '';
  const pendingReasonHtml = pendingReason
    ? '<div style="font-size:var(--text-xs);color:var(--muted);margin-top:2px" title="Pending reason: ' + escapeHtml(pendingReason) + '">' + escapeHtml(pendingReason.replace(/_/g, ' ')) + '</div>'
    : '';
  var followupCount = _countPrFollowups(pr);
  var followupChip = followupCount > 0
    ? ' <span class="pr-badge draft" style="font-size:var(--text-xs)" title="' + followupCount + ' follow-up work item(s) dispatched from comments on this PR">+' + followupCount + ' follow-up' + (followupCount === 1 ? '' : 's') + '</span>'
    : '';
  // Issue #2969 — paused-cause chip(s). Prefer the API-enriched _pausedCauses
  // field; fall back to deriving from _noOpFixes for the /state/ disk-fetch
  // path (which bypasses queries.getPullRequests enrichment).
  var pausedCauses = Array.isArray(pr._pausedCauses) ? pr._pausedCauses : [];
  if (!pausedCauses.length && pr._noOpFixes && typeof pr._noOpFixes === 'object') {
    pausedCauses = Object.keys(pr._noOpFixes).filter(function(c) {
      var rec = pr._noOpFixes[c];
      return rec && typeof rec === 'object' && rec.paused === true;
    });
  }
  var pausedChips = '';
  if (pausedCauses.length) {
    pausedChips = pausedCauses.map(function(cause) {
      var rec = (pr._noOpFixes && pr._noOpFixes[cause]) || {};
      var titleAttr = 'PR-fix automation paused for "' + cause + '" after '
        + (rec.count || '?') + ' no-op attempt(s). Reason: '
        + (rec.reason || 'unknown') + '. Click to resume (clears _noOpFixes['
        + cause + '] so the next discovery pass can re-dispatch).';
      return ' <button class="pr-badge rejected pr-paused-chip" '
        + 'style="font-size:var(--text-xs);cursor:pointer;border:none" '
        + 'title="' + escapeHtml(titleAttr) + '" '
        + 'data-pr-id="' + escapeHtml(String(pr.id || '')) + '" '
        + 'data-pr-cause="' + escapeHtml(cause) + '" '
        + 'onclick="event.stopPropagation();resumePausedCause(this)">'
        + '⏸ paused: ' + escapeHtml(cause) + ' ▶</button>';
    }).join('');
  }
  const titleText = pr.title || 'Untitled';
  const agentText = pr.agent || '—';
  const reviewerCell = sq.reviewer && sq.status !== 'waiting'
    ? '<span class="pr-agent" title="' + escapeHtml(sq.note || sq.reviewer) + '">' + escapeHtml(sq.reviewer) + '</span>'
    : sq.reviewer && sq.status === 'waiting'
      ? '<span class="pr-agent" style="color:var(--muted)" title="Vote pending confirmation">' + escapeHtml(sq.reviewer) + '…</span>'
      : pr.reviewedBy && pr.reviewedBy.length
        ? '<span class="pr-agent" title="' + escapeHtml(pr.reviewedBy.join(', ')) + '">' + escapeHtml(pr.reviewedBy.join(', ')) + '</span>'
        : '<span style="color:var(--muted);font-size:var(--text-base)">—</span>';
  const createdLabel = (pr.created || '—').slice(0, 16).replace('T', ' ');
  // Per-row auto-observe toggle (W-mpmwxkzm0009ba0b; canonicalized in
  // W-mq5s5ttx000j7ab8-c). pr.contextOnly === true means the engine polls
  // status/comments but does NOT dispatch review/fix agents against this
  // PR. Default (undefined / false) = observed.
  var observe = pr.contextOnly !== true;
  var observeClass = observe ? 'pr-observe-on' : 'pr-observe-off';
  var observeLabel = observe ? 'observed' : 'context';
  var observeTitle = observe
    ? 'Auto-observe ON — engine reviews this PR and dispatches fix agents on review/build feedback. Click to switch to context-only.'
    : 'Context only — engine polls status and comments but will not dispatch review/fix agents. Click to enable auto-observe.';
  // pr.id is canonical: "<host>:<slug>#<number>" (engine/shared.js getCanonicalPrId).
  // Parse here so the toggle handler can post {host, slug, number} without
  // shipping additional fields per row.
  var observeBtn = '';
  var idMatch = typeof pr.id === 'string' ? pr.id.match(/^([^:]+):(.+)#(\d+)$/) : null;
  if (idMatch) {
    // Real toggle-switch (W-mpof8qam000h62fb). Semantic <input type=checkbox>
    // inside a <label> gives free keyboard + screen-reader support; clicks on
    // the track or knob flip the state via the wrapping label. Data-attrs stay
    // on the input so togglePrObserve(this) keeps working unchanged.
    observeBtn = '<label class="pr-observe-switch ' + observeClass + '" '
      + 'title="' + escapeHtml(observeTitle) + '" '
      + 'onclick="event.stopPropagation()">'
      + '<input type="checkbox" class="pr-observe-switch__input"'
      + (observe ? ' checked' : '') + ' '
      + 'aria-label="' + escapeHtml('Auto-observe: ' + observeLabel) + '" '
      + 'data-pr-host="' + escapeHtml(idMatch[1]) + '" '
      + 'data-pr-slug="' + escapeHtml(idMatch[2]) + '" '
      + 'data-pr-number="' + escapeHtml(idMatch[3]) + '" '
      + 'data-pr-observe="' + (observe ? '1' : '0') + '" '
      + 'onchange="togglePrObserve(this)">'
      + '<span class="pr-observe-switch__track" aria-hidden="true">'
      + '<span class="pr-observe-switch__knob"></span>'
      + '</span>'
      + '</label> ';
  }
  // Title attrs live on the inner element (link/span/badge) so hovering the
  // ellipsis-truncated content reveals the full text. Cell tags stay bare so
  // the header-to-cell count assertion in test/unit.test.js continues to
  // balance.
  return '<tr>' +
    '<td><span class="pr-id" title="' + escapeHtml(String(prId)) + '">' + escapeHtml(String(prId)) + '</span></td>' +
    '<td><a class="pr-title" title="' + escapeHtml(titleText) + '" href="' + escapeHtml(safeUrl(url)) + '" target="_blank" rel="noopener">' + escapeHtml(titleText) + '</a>' + followupChip + pausedChips + (pr.description ? '<div class="pr-desc" title="' + escapeHtml(pr.description) + '">' + escapeHtml(pr.description.length > 120 ? pr.description.slice(0, 120) + '...' : pr.description) + '</div>' : '') + '</td>' +
    '<td><span class="pr-agent" title="' + escapeHtml(agentText) + '">' + escapeHtml(agentText) + '</span></td>' +
    '<td><span class="' + branchClass + '" title="' + escapeHtml(branchError || branchLabel) + '">' + escapeHtml(branchLabel) + '</span>' + pendingReasonHtml + '</td>' +
    '<td><span class="pr-badge ' + reviewClass + '" title="' + escapeHtml(reviewTitle || reviewLabel) + '">' + escapeHtml(reviewLabel) + '</span></td>' +
    '<td>' + reviewerCell + '</td>' +
    '<td><span class="pr-badge ' + buildClass + '" title="' + escapeHtml(buildTitle || buildLabel) + '">' + escapeHtml(buildLabel) + '</span></td>' +
    '<td><span class="pr-badge ' + statusClass + '" title="' + escapeHtml(statusLabel) + '">' + escapeHtml(statusLabel) + '</span></td>' +
    '<td>' + (observeBtn || '<span style="color:var(--muted);font-size:var(--text-base)">—</span>') + '</td>' +
    '<td><span class="pr-date" title="' + escapeHtml(createdLabel) + '">' + escapeHtml(createdLabel) + '</span></td>' +
    '<td><button class="btn-destructive" style="padding:1px 5px" data-pr-id="' + escapeHtml(String(prId)) + '" onclick="event.stopPropagation();unlinkPr(this.dataset.prId)" title="Remove from tracking">x</button></td>' +
    '</tr>';
}

// Explicit per-column widths keep the PR table from ballooning when titles or
// branches are long. Total ≈1420px → table grows past viewport on narrow
// windows and the .pr-table-wrap--prs container scrolls horizontally inside
// the viewport (sticky scrollbar — see styles.css).
const PRS_COLGROUP =
  '<colgroup>' +
    '<col style="width:75px">' +    // PR id
    '<col style="width:320px">' +   // Title
    '<col style="width:140px">' +   // Agent
    '<col style="width:200px">' +   // Branch
    '<col style="width:130px">' +   // Review
    '<col style="width:140px">' +   // Signed Off By
    '<col style="width:130px">' +   // Build
    '<col style="width:110px">' +   // Status
    '<col style="width:100px">' +   // Observe
    '<col style="width:130px">' +   // Created
    '<col style="width:50px">' +    // Actions
  '</colgroup>';

function prTableHtml(rows) {
  return '<div class="pr-table-wrap pr-table-wrap--prs"><table class="pr-table pr-table--prs">' +
    PRS_COLGROUP +
    '<thead><tr>' +
    '<th>PR</th><th>Title</th><th>Agent</th><th>Branch</th><th>Review</th><th>Signed Off By</th><th>Build</th><th>Status</th><th>Observe</th><th>Created</th><th></th>' +
    '</tr></thead><tbody>' + rows + '</tbody></table></div>';
}

function _capturePrScrollState(el) {
  const tableWrap = el ? el.querySelector('.pr-table-wrap') : null;
  return tableWrap ? { left: tableWrap.scrollLeft || 0, top: tableWrap.scrollTop || 0 } : null;
}

function _restorePrScrollState(el, state) {
  if (!state) return;
  const tableWrap = el ? el.querySelector('.pr-table-wrap') : null;
  if (!tableWrap) return;
  tableWrap.scrollLeft = state.left || 0;
  tableWrap.scrollTop = state.top || 0;
}

function renderPrs(prs, opts) {
  opts = opts || {};
  prs = prs.filter(p => !isDeleted('pr:' + p.id));
  allPrs = prs;
  const el = document.getElementById('pr-content');
  const count = document.getElementById('pr-count');
  count.textContent = prs.length;
  if (!prs.length) {
    el.innerHTML = '<p class="pr-empty">No pull requests yet. PRs created by agents will appear here with review, build, and merge status.</p>';
    return;
  }
  const totalPages = Math.ceil(prs.length / PR_PER_PAGE);
  const previousPage = prPage;
  if (prPage >= totalPages) prPage = totalPages - 1;
  const start = prPage * PR_PER_PAGE;
  const pagePrs = prs.slice(start, start + PR_PER_PAGE);
  const rows = pagePrs.map(prRow).join('');

  let pager = '';
  if (prs.length > PR_PER_PAGE) {
    pager = '<div class="pr-pager">' +
      '<span class="pr-page-info">Showing ' + (start+1) + ' to ' + Math.min(start+PR_PER_PAGE, prs.length) + ' of ' + prs.length + '</span>' +
      '<div class="pr-pager-btns">' +
        '<button class="pr-pager-btn ' + (prPage === 0 ? 'disabled' : '') + '" onclick="prPrev()">Prev</button>' +
        '<button class="pr-pager-btn ' + (prPage >= totalPages-1 ? 'disabled' : '') + '" onclick="prNext()">Next</button>' +
        '<button class="pr-pager-btn see-all" onclick="openAllPrs()">See all ' + prs.length + ' PRs</button>' +
      '</div>' +
    '</div>';
  }

  const savedScroll = opts.preserveScroll === false || previousPage !== prPage
    ? null
    : _capturePrScrollState(el);
  // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all PR user data wrapped in escapeHtml() by prRow() (fields: PR id, title, description, agent, branch, review/build/status labels)
  el.innerHTML = prTableHtml(rows) + pager;
  _restorePrScrollState(el, savedScroll);
}

function prPrev() { if (prPage > 0) { prPage--; renderPrs(allPrs, { preserveScroll: false }); } }
function prNext() { const totalPages = Math.ceil(allPrs.length / PR_PER_PAGE); if (prPage < totalPages-1) { prPage++; renderPrs(allPrs, { preserveScroll: false }); } }

function openAllPrs() {
  const modalEl = document.querySelector('#modal .modal');
  if (modalEl) modalEl.classList.add('modal-wide');
  document.getElementById('modal-title').textContent = 'All Pull Requests (' + allPrs.length + ')';
  // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all PR user data wrapped in escapeHtml() by prRow() (fields: PR id, title, description, agent, branch, review/build/status labels)
  document.getElementById('modal-body').innerHTML = prTableHtml(allPrs.map(prRow).join(''));
  document.getElementById('modal-body').style.fontFamily = "'Segoe UI', system-ui, sans-serif";
  document.getElementById('modal-body').style.whiteSpace = 'normal';
  document.getElementById('modal').classList.add('open');
}

function openModal(i) {
  const item = inboxData[i];
  if (!item) return;
  document.getElementById('modal-title').textContent = item.name;
  // eslint-disable-next-line no-unsanitized/property -- reason: renderMd() escapes item.content before assembling HTML; item.name is wrapped in escapeHtml() (see dashboard/js/utils.js)
  document.getElementById('modal-body').innerHTML =
    '<div style="margin-bottom:12px"><button class="pr-pager-btn" style="font-size:var(--text-sm);padding:3px 10px" onclick="promoteToKB(\'' + escapeHtml(item.name) + '\')">Add to Knowledge Base</button></div>' +
    '<div style="font-size:var(--text-md);line-height:1.7;color:var(--muted)">' + renderMd(item.content) + '</div>';
  _modalDocContext = { title: item.name, content: item.content, selection: '' };
  _modalFilePath = 'notes/inbox/' + item.name; showModalQa();
  // Clear notification badge when opening this document
  const card = findCardForFile(_modalFilePath);
  if (card) clearNotifBadge(card);
  document.getElementById('modal').classList.add('open');
}

function openAddPrModal() {
  const projects = (typeof cmdProjects !== 'undefined' ? cmdProjects : []) || [];
  const projOpts = projects.map(p => {
    const name = typeof p === 'object' ? p.name : p;
    return '<option value="' + escapeHtml(name) + '">' + escapeHtml(name) + '</option>';
  }).join('');
  const inputStyle = 'display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md);font-family:inherit';

  // No-projects warning (W-mpehchnt000w46da) — surfaced when the operator hasn't
  // added any project yet. We still allow the link (read-only tracking is valid),
  // but warn that auto-fix / auto-review can't dispatch without a project worktree.
  const noProjectsWarning = projects.length === 0
    ? '<div id="pr-link-no-projects-warning" style="padding:8px 12px;background:rgba(210,153,34,0.15);border:1px solid rgba(210,153,34,0.3);border-radius:var(--radius-sm);color:var(--yellow);font-size:var(--text-base);line-height:1.5;margin-bottom:4px">' +
        '<div style="font-weight:700;margin-bottom:2px">&#x26A0; No project configured.</div>' +
        '<div style="color:var(--text)">This PR will be tracked, but auto-fix and auto-review cannot dispatch against it &mdash; agents need a project worktree to operate in. Add a project first (Projects tab) to enable automation.</div>' +
      '</div>'
    : '';

  document.getElementById('modal-title').textContent = 'Link Pull Request';
  // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escapeHtml() (fields: project names)
  document.getElementById('modal-body').innerHTML =
    '<div style="display:flex;flex-direction:column;gap:10px">' +
      noProjectsWarning +
      '<label style="color:var(--text);font-size:var(--text-md)">PR URL <input id="pr-link-url" style="' + inputStyle + '" placeholder="https://github.com/org/repo/pull/123"></label>' +
      '<label style="color:var(--text);font-size:var(--text-md)">Title <input id="pr-link-title" style="' + inputStyle + '" placeholder="Short description (optional — auto-detected from URL)"></label>' +
      '<label style="color:var(--text);font-size:var(--text-md)">Project <select id="pr-link-project" style="' + inputStyle + '"><option value="">Auto-detect from URL (central if no unique match)</option>' + projOpts + '</select></label>' +
      '<label style="color:var(--text);font-size:var(--text-md)">Context <textarea id="pr-link-context" rows="3" style="' + inputStyle + ';resize:vertical" placeholder="Why are you linking this? What should agents know about it?"></textarea></label>' +
      '<label style="display:flex;align-items:center;gap:8px;color:var(--text);font-size:var(--text-md);margin-top:4px;cursor:pointer">' +
        '<input type="checkbox" id="pr-link-observe" style="width:16px;height:16px;accent-color:var(--blue)">' +
        '<span>Auto-observe <span style="color:var(--muted);font-weight:400">(monitor builds, resolve comments, fix failures)</span></span>' +
      '</label>' +
      '<div style="font-size:var(--text-base);color:var(--muted);margin-top:-4px;padding-left:24px">Off = context only (e.g. teammate\'s PR). On = agents actively monitor and fix issues.</div>' +
      '<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:8px">' +
        '<button onclick="closeModal()" class="pr-pager-btn">Cancel</button>' +
        '<button onclick="_submitLinkPr(event)" class="btn-primary-lg">Link PR</button>' +
      '</div>' +
    '</div>';
  document.getElementById('modal').classList.add('open');
  setTimeout(() => document.getElementById('pr-link-url')?.focus(), 100);
}

async function _submitLinkPr(e) {
  var btn = (e || window.event)?.target; if (btn) { btn.disabled = true; btn.textContent = 'Linking...'; }
  const url = document.getElementById('pr-link-url')?.value?.trim();
  if (!url) { if (btn) { btn.disabled = false; btn.textContent = 'Link PR'; } alert('PR URL is required'); return; }
  const title = document.getElementById('pr-link-title')?.value?.trim() || '';
  const project = document.getElementById('pr-link-project')?.value || '';
  const context = document.getElementById('pr-link-context')?.value || '';
  const autoObserve = document.getElementById('pr-link-observe')?.checked || false;

  try { closeModal(); } catch { /* expected */ }
  try {
    const res = await fetch('/api/pull-requests/link', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ url, title, project, context, autoObserve })
    });
    const data = await res.json();
    if (res.ok) {
      showToast('pr-toast', (data.message || 'PR linked') + (autoObserve ? ' (auto-observe on)' : ''), true, 6000);
      refresh();
    } else { alert('Failed: ' + (data.error || 'unknown')); openAddPrModal(); }
  } catch (e) { alert('Error: ' + e.message); openAddPrModal(); }
}

async function unlinkPr(id) {
  if (!confirm('Remove ' + id + ' from tracking?')) return;
  showToast('pr-toast', id + ' removed', true);
  markDeleted('pr:' + id);
  const escId = window.CSS && CSS.escape ? CSS.escape(id) : id;
  const row = document.querySelector('[data-pr-id="' + escId + '"]')?.closest('tr');
  if (row) row.remove();
  try {
    const res = await fetch('/api/pull-requests/delete', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ id })
    });
    if (!res.ok) { clearDeleted('pr:' + id); const d = await res.json().catch(() => ({})); showToast('pr-toast', 'Failed: ' + (d.error || 'unknown'), false); refresh(); return; }
  } catch (e) { clearDeleted('pr:' + id); showToast('pr-toast', 'Error: ' + e.message, false); refresh(); }
}

// Per-row auto-observe toggle (W-mpmwxkzm0009ba0b; toggle-switch UX in
// W-mpof8qam000h62fb; canonicalized in W-mq5s5ttx000j7ab8-c). Optimistic
// UI: flip the switch immediately, only revert on API error. The engine
// consumes canonical `contextOnly` to gate review/fix dispatch (engine/
// shared.js isAutoManagedPrRecord + discoverFromPrs).
//
// `btn` is the <input type=checkbox> inside .pr-observe-switch. Data-attrs
// live on the input so this handler keeps reading them directly; visual
// state is mirrored onto the wrapping <label> via the pr-observe-on/off
// class so CSS can paint the track and slide the knob.
async function togglePrObserve(btn) {
  if (!btn || btn.disabled) return;
  const host = btn.dataset.prHost;
  const slug = btn.dataset.prSlug;
  const number = parseInt(btn.dataset.prNumber, 10);
  if (!host || !slug || !Number.isFinite(number)) return;
  const wasObserve = btn.dataset.prObserve === '1';
  const next = !wasObserve;
  const wrapper = btn.closest('.pr-observe-switch');
  // Optimistic flip
  btn.dataset.prObserve = next ? '1' : '0';
  btn.checked = next;
  btn.setAttribute('aria-label', 'Auto-observe: ' + (next ? 'observed' : 'context'));
  if (wrapper) {
    wrapper.classList.toggle('pr-observe-on', next);
    wrapper.classList.toggle('pr-observe-off', !next);
  }
  btn.disabled = true;
  try {
    const res = await fetch('/api/pull-requests/observe', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ host, slug, number, observe: next })
    });
    if (!res.ok) {
      const d = await res.json().catch(() => ({}));
      // Revert
      btn.dataset.prObserve = wasObserve ? '1' : '0';
      btn.checked = wasObserve;
      btn.setAttribute('aria-label', 'Auto-observe: ' + (wasObserve ? 'observed' : 'context'));
      if (wrapper) {
        wrapper.classList.toggle('pr-observe-on', wasObserve);
        wrapper.classList.toggle('pr-observe-off', !wasObserve);
      }
      showToast('pr-toast', 'Failed: ' + (d.error || 'unknown'), false);
      return;
    }
    showToast('pr-toast', next ? 'Auto-observe enabled' : 'Switched to context only', true);
  } catch (e) {
    btn.dataset.prObserve = wasObserve ? '1' : '0';
    btn.checked = wasObserve;
    btn.setAttribute('aria-label', 'Auto-observe: ' + (wasObserve ? 'observed' : 'context'));
    if (wrapper) {
      wrapper.classList.toggle('pr-observe-on', wasObserve);
      wrapper.classList.toggle('pr-observe-off', !wasObserve);
    }
    showToast('pr-toast', 'Error: ' + e.message, false);
  } finally {
    btn.disabled = false;
  }
}

// Issue #2969 — resume a paused PR-fix cause. Optimistic UI: hide the chip
// immediately, show success toast, then POST. On API error, restore the chip
// via refresh().
async function resumePausedCause(btn) {
  if (!btn || btn.disabled) return;
  const prId = btn.dataset.prId;
  const cause = btn.dataset.prCause;
  if (!prId || !cause) return;
  if (!confirm('Resume paused cause "' + cause + '" on ' + prId + '?\n\nThis clears the pause flag so the engine can re-dispatch on the next discovery pass.')) return;
  btn.disabled = true;
  const prevDisplay = btn.style.display;
  btn.style.display = 'none';
  showToast('pr-toast', 'Resumed ' + cause + ' on ' + prId, true);
  try {
    const res = await fetch('/api/pull-requests/clear-paused-cause', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ prId, cause })
    });
    if (!res.ok) {
      const d = await res.json().catch(() => ({}));
      btn.style.display = prevDisplay;
      btn.disabled = false;
      showToast('pr-toast', 'Failed: ' + (d.error || 'unknown'), false);
      return;
    }
  } catch (e) {
    btn.style.display = prevDisplay;
    btn.disabled = false;
    showToast('pr-toast', 'Error: ' + e.message, false);
  }
}

window.MinionsPrs = { prRow, prTableHtml, renderPrs, prPrev, prNext, openAllPrs, openModal, openAddPrModal, unlinkPr, togglePrObserve, resumePausedCause };
